Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

스탬프 추가 기능 구현 #65

Merged
merged 31 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8e5634c
feat: 스탬프 추가 기능 서비스 구현
jhsseonn Feb 11, 2024
4e81f5d
feat: 스탬프 추가 기능 컨트롤러 구현
jhsseonn Feb 11, 2024
50e338b
test: 스탬프 추가 기능 테스트
jhsseonn Feb 11, 2024
0aba065
refactor: 스탬프 영속성 컨텍스트 수정
jhsseonn Feb 11, 2024
32caa71
bug: 스탬프 테이블을 생성하지 못하는 오류 해결
jhsseonn Feb 11, 2024
3faacee
test: 골 참여자가 아닌 사용자가 생성한 스탬프 예외발생 테스트 통과
jhsseonn Feb 11, 2024
9b26fac
docs: api 문서 업데이트
jhsseonn Feb 11, 2024
ab7ee97
refactor: 예외처리 수정
jhsseonn Feb 11, 2024
873c22a
refactor: 스탬프 인증 메시지 제한 30자로 변경
jhsseonn Feb 15, 2024
db80635
test: 이미 스탬프를 추가한 사용자 검증 테스트 통과
jhsseonn Feb 15, 2024
66f10f8
refactor: 컨벤션에 따른 수정
jhsseonn Feb 15, 2024
4ef124a
feat: Stamp 도메인 생성
jhsseonn Feb 8, 2024
b7b9506
refactor: 불필요한 리스트 크기 초기화 삭제
jhsseonn Feb 16, 2024
f8b5696
refactor: 쿼리 Fetch join 반영
jhsseonn Feb 16, 2024
1251c78
refactor: 스탬프 생성시 생성된 스탬프 정보를 응답으로 반환하도록 수정
jhsseonn Feb 16, 2024
8a16b11
test: 스탬프 생성시 생성된 스탬프 정보 응답 반환 테스트
jhsseonn Feb 16, 2024
21c33ff
refactor: 골에 스탬프 리스트 저장하는 부분 로직 삭제
jhsseonn Feb 16, 2024
4026c19
refactor: 골 메시지 글자수 매직넘버 적용
jhsseonn Feb 16, 2024
045fe05
docs: api 문서 업데이트
jhsseonn Feb 16, 2024
2c14f2c
docs: api 문서 업데이트
jhsseonn Feb 16, 2024
402d92b
Merge branch 'develop' into feature/44
jhsseonn Feb 16, 2024
d70aea1
refactor: stamps toString 삭제
jhsseonn Feb 16, 2024
b6a9a36
Merge remote-tracking branch 'origin/feature/44' into feature/44
jhsseonn Feb 16, 2024
838a006
bug: 쿼리 오류 해결
jhsseonn Feb 16, 2024
8d43251
docs: api 문서 업데이트
jhsseonn Feb 16, 2024
7ea18cd
refactor: response 변경에 따른 수정
jhsseonn Feb 17, 2024
bde459f
refactor: day, message vo로 리팩토링
jhsseonn Feb 17, 2024
8d5662a
refactor: day, message vo로 리팩토링에 따른 수정
jhsseonn Feb 17, 2024
3d0bf15
test: day, message vo로 리팩토링에 따른 테스트 추가
jhsseonn Feb 17, 2024
3bbc1fb
docs: api 문서 업데이트
jhsseonn Feb 17, 2024
e136ca5
refactor: 컨벤션에 따른 수정
jhsseonn Feb 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/docs/asciidoc/stamp.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
=== 새로운 스탬프 추가
==== 요청
operation::stamp-controller-test/스탬프_생성을_요청하면_새로운_스탬프를_생성한다[snippets='http-request,request-headers,request-fields']
==== 응답
operation::stamp-controller-test/스탬프_생성을_요청하면_새로운_스탬프를_생성한다[snippets='http-response,response-fields']
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ public enum ExceptionMessage {
DELETE_GOAL_FORBIDDEN("골을 삭제할 권한이 없습니다."),
UPDATE_GOAL_FORBIDDEN("골을 수정할 권한이 없습니다."),
UPDATE_TEAMS_FORBIDDEN("골 참여자 목록은 비어있을 수 없습니다."),

// 스탬프
INVALID_STAMP_DAY("스탬프 날짜는 골 시작일 이전이거나 종료일 이후일 수 없습니다."),
INVALID_STAMP_DAY_FUTURE("오늘보다 이후의 스탬프는 추가할 수 없습니다."),
INVALID_STAMP_TO_CREATE("이미 해당 날짜의 스탬프가 존재합니다."),
CREATE_STAMP_FORBIDDEN("스탬프를 추가할 권한이 없습니다."),
INVALID_STAMP_MESSAGE("스탬프 인증 메시지는 비어있거나 50자 초과일 수 없습니다."),

// 관리자 페이지
INVALID_FRIEND_STATUS("잘못된 친구 상태입니다.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import com.backend.blooming.goal.application.exception.DeleteGoalForbiddenException;
import com.backend.blooming.goal.application.exception.InvalidGoalException;
import com.backend.blooming.goal.application.exception.NotFoundGoalException;
import com.backend.blooming.stamp.application.exception.CreateStampForbiddenException;
import com.backend.blooming.stamp.domain.exception.InvalidStampException;
import com.backend.blooming.goal.application.exception.UpdateGoalForbiddenException;
import com.backend.blooming.themecolor.domain.exception.UnsupportedThemeColorException;
import com.backend.blooming.user.application.exception.NotFoundUserException;
Expand Down Expand Up @@ -205,4 +207,24 @@ public ResponseEntity<ExceptionResponse> handleAlreadyRegisterBlackListTokenExce
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ExceptionResponse(exception.getMessage()));
}

@ExceptionHandler(InvalidStampException.class)
public ResponseEntity<ExceptionResponse> handleInvalidStampException(
final InvalidStampException exception
) {
logger.warn(String.format(LOG_MESSAGE_FORMAT, exception.getClass().getSimpleName(), exception.getMessage()));

return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ExceptionResponse(exception.getMessage()));
}

@ExceptionHandler(CreateStampForbiddenException.class)
public ResponseEntity<ExceptionResponse> handleCreateStampForbiddenException(
final CreateStampForbiddenException exception
) {
logger.warn(String.format(LOG_MESSAGE_FORMAT, exception.getClass().getSimpleName(), exception.getMessage()));

return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ExceptionResponse(exception.getMessage()));
}
}
12 changes: 4 additions & 8 deletions src/main/java/com/backend/blooming/goal/domain/GoalTerm.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,8 @@ public GoalTerm(final LocalDate startDate, final LocalDate endDate) {
this.endDate = endDate;
this.days = getValidGoalDays(startDate, endDate);
}

private void validateGoalDatePeriod(
final LocalDate startDate,
final LocalDate endDate) {

private void validateGoalDatePeriod(final LocalDate startDate, final LocalDate endDate) {
final LocalDate nowDate = LocalDate.now();

if (startDate.isBefore(nowDate)) {
Expand All @@ -54,10 +52,8 @@ private void validateGoalDatePeriod(
throw new InvalidGoalException.InvalidInvalidGoalPeriod();
}
}

private long getValidGoalDays(
final LocalDate startDate,
final LocalDate endDate) {

private long getValidGoalDays(final LocalDate startDate, final LocalDate endDate) {
final long goalDays = ChronoUnit.DAYS.between(startDate, endDate) + COUNT_GOAL_DAYS;

if (goalDays < GOAL_DAYS_MINIMUM || goalDays > GOAL_DAYS_MAXIMUM) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,13 @@ public interface GoalRepository extends JpaRepository<Goal, Long> {
ORDER BY g.goalTerm.startDate DESC
""")
List<Goal> findAllByUserIdAndFinished(final Long userId, final LocalDate now);

@Query("""
SELECT g
FROM Goal g
JOIN FETCH g.teams.goalTeams gt
JOIN FETCH gt.user gtu
Comment on lines +40 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

WHERE g.id = :goalId AND g.deleted = FALSE
""")
Optional<Goal> findByIdWithUserAndDeletedIsFalse(final Long goalId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.backend.blooming.stamp.application;

import com.backend.blooming.goal.application.exception.NotFoundGoalException;
import com.backend.blooming.goal.domain.Goal;
import com.backend.blooming.goal.infrastructure.repository.GoalRepository;
import com.backend.blooming.stamp.application.dto.CreateStampDto;
import com.backend.blooming.stamp.application.dto.ReadStampDto;
import com.backend.blooming.stamp.application.exception.CreateStampForbiddenException;
import com.backend.blooming.stamp.domain.Stamp;
import com.backend.blooming.stamp.domain.exception.InvalidStampException;
import com.backend.blooming.stamp.infrastructure.repository.StampRepository;
import com.backend.blooming.user.application.exception.NotFoundUserException;
import com.backend.blooming.user.domain.User;
import com.backend.blooming.user.infrastructure.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Transactional
@RequiredArgsConstructor
public class StampService {

private final GoalRepository goalRepository;
private final UserRepository userRepository;
private final StampRepository stampRepository;

public ReadStampDto createStamp(final CreateStampDto createStampDto) {
final Goal goal = getGoal(createStampDto.goalId());
final User user = getUser(createStampDto.userId());
validateUserInGoalTeams(goal, user.getId());
validateExistStamp(user.getId(), createStampDto.day());
final Stamp stamp = persistStamp(createStampDto, goal, user);

return ReadStampDto.from(stamp);
}

private Goal getGoal(final Long goalId) {
return goalRepository.findByIdWithUserAndDeletedIsFalse(goalId)
.orElseThrow(NotFoundGoalException::new);
}

private User getUser(final Long userId) {
return userRepository.findByIdAndDeletedIsFalse(userId)
.orElseThrow(NotFoundUserException::new);
}

private void validateUserInGoalTeams(final Goal goal, final Long userId) {
final List<Long> teamUserIds = goal.getTeams()
.getGoalTeams()
.stream()
.map(goalTeam -> goalTeam.getUser().getId())
.toList();
if (!teamUserIds.contains(userId)) {
throw new CreateStampForbiddenException();
}
}

private void validateExistStamp(final Long userId, final int day) {
final boolean isExistsStamp = stampRepository.existsByUserIdAndDayAndDeletedIsFalse(userId, day);
if (isExistsStamp) {
throw new InvalidStampException.InvalidStampToCreate();
}
}

private Stamp persistStamp(final CreateStampDto createStampDto, final Goal goal, final User user) {
final Stamp stamp = Stamp.builder()
.goal(goal)
.user(user)
.day(createStampDto.day())
.message(createStampDto.message())
.build();

return stampRepository.save(stamp);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.backend.blooming.stamp.application.dto;

import com.backend.blooming.stamp.presentation.dto.request.CreateStampRequest;

public record CreateStampDto(
Long goalId,
Long userId,
int day,
String message
) {

public static CreateStampDto of(final CreateStampRequest request, final Long userId) {
return new CreateStampDto(
request.goalId(),
userId,
request.day(),
request.message()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.backend.blooming.stamp.application.dto;

import com.backend.blooming.stamp.domain.Stamp;

public record ReadStampDto(
Long goalId,
int day,
String message
) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스탬프 아이디는 굳이 넘겨줄 필요가 없을까요?
오히려 골 아이디를 넘겨줘야 하는가에 대한 의문도 있습니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가로 사용자 정보는 넘길 필요가 없는지 궁금합니다!
저희가 아직 이미지를 신경 쓰지 않기로 하긴 했지만, 이미지가 없는 경우에는 사용자의 테마 색을 통해 기본 이미지로 만들기로 했던 걸로 기억해서요! (지난 기획 때)
또한, 스탬프 아래 메시지와 함께 사용자 아림이 떴던 걸로 기억하는 데 아닐까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용자 이름은 제가 빼먹었네요...! 추가하도록 하겠습니다.
골 아이디는 스탬프가 업로드 될 골 아이디가 필요하다고 생각해 넣었습니다만 응답을 받아 화면에 보여줄 때 골 아이디는 굳이 필요하지 않을수도 있겠네요! 오히려 스탬프 식별에는 스탬프 아이디가 더 맞을 것 같네요. 스탬프 아이디로 수정하도록 하겠습니다.
스탬프 이미지를 추가하지 않을 경우 저는 이미지 없이 사용자 이름과 메시지만 올라간다고 생각했습니다..! 안드로이드로 작업할 때 스탬프 담당이 아니었다보니 기억이 잘 나지 않았네요... 사용자 색상으로 기본 이미지를 생성해야할 경우 사용자 프로필 색상도 함께 넘겨주는 것이 좋겠네요!
그러면 골 아이디를 빼고 스탬프 아이디와 사용자 이름과 프로필 색상을 함께 넘겨주는 것으로 하겠습니다!


public static ReadStampDto from(final Stamp stamp) {
return new ReadStampDto(
stamp.getGoal().getId(),
stamp.getDay(),
stamp.getMessage()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.backend.blooming.stamp.application.exception;

import com.backend.blooming.exception.BloomingException;
import com.backend.blooming.exception.ExceptionMessage;

public class CreateStampForbiddenException extends BloomingException {

public CreateStampForbiddenException() {
super(ExceptionMessage.CREATE_STAMP_FORBIDDEN);
}
}
96 changes: 96 additions & 0 deletions src/main/java/com/backend/blooming/stamp/domain/Stamp.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.backend.blooming.stamp.domain;

import com.backend.blooming.common.entity.BaseTimeEntity;
import com.backend.blooming.goal.domain.Goal;
import com.backend.blooming.stamp.domain.exception.InvalidStampException;
import com.backend.blooming.user.domain.User;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@EqualsAndHashCode(of = "id", callSuper = false)
@ToString(exclude = {"goal", "user"})
public class Stamp extends BaseTimeEntity {

private static final int STAMP_DAY_MINIMUM = 1;
private static final int STAMP_MESSAGE_MAXIMUM = 30;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 클래스에서는 더 이상 사용하지 않으니 없어져도 괜찮겠네요!


@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "goal_id", foreignKey = @ForeignKey(name = "fk_stamp_goal"), nullable = false)
private Goal goal;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", foreignKey = @ForeignKey(name = "fk_stamp_user"), nullable = false)
private User user;

@Column(name = "stamp_day", nullable = false)
private int day;

@Column(columnDefinition = "text", nullable = false, length = STAMP_MESSAGE_MAXIMUM)
private String message;

@Column(name = "is_deleted", nullable = false)
private boolean deleted = false;

@Builder
private Stamp(
final Goal goal,
final User user,
final int day,
final String message
) {
this.goal = goal;
this.user = user;
this.day = validateDay(goal, day);
this.message = validateMessage(message);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

daymessage를 vo로 만드는 것에 대해 어떻게 생각하시나요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋네요! day와 message 모두 vo 반영해보겠습니다!

}

private int validateDay(final Goal goal, final int day) {
final long nowStampDay = ChronoUnit.DAYS.between(goal.getGoalTerm().getStartDate(), LocalDate.now()) + 1;

if (day > nowStampDay) {
throw new InvalidStampException.InvalidStampDayFuture();
}
if (day > goal.getGoalTerm().getDays()) {
throw new InvalidStampException.InvalidStampDay();
}
if (goal.getGoalTerm().getStartDate().isAfter(LocalDate.now())) {
throw new InvalidStampException.InvalidStampDay();
}

return day;
}

private String validateMessage(final String message) {
if (message == null || message.isEmpty()) {
throw new InvalidStampException.InvalidStampMessage();
}
if (message.length() > STAMP_MESSAGE_MAXIMUM) {
throw new InvalidStampException.InvalidStampMessage();
}

return message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.backend.blooming.stamp.domain.exception;

import com.backend.blooming.exception.BloomingException;
import com.backend.blooming.exception.ExceptionMessage;

public class InvalidStampException extends BloomingException {

public InvalidStampException(final ExceptionMessage exceptionMessage) {
super(exceptionMessage);
}

public static class InvalidStampDay extends InvalidStampException {

public InvalidStampDay() {
super(ExceptionMessage.INVALID_STAMP_DAY);
}
}

public static class InvalidStampDayFuture extends InvalidStampException {

public InvalidStampDayFuture() {
super(ExceptionMessage.INVALID_STAMP_DAY_FUTURE);
}
}

public static class InvalidStampToCreate extends InvalidStampException {

public InvalidStampToCreate() {
super(ExceptionMessage.INVALID_STAMP_TO_CREATE);
}
}

public static class InvalidStampMessage extends InvalidStampException {

public InvalidStampMessage() {
super(ExceptionMessage.INVALID_STAMP_MESSAGE);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.backend.blooming.stamp.infrastructure.repository;

import com.backend.blooming.stamp.domain.Stamp;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface StampRepository extends JpaRepository<Stamp, Long> {

@Query("""
SELECT EXISTS(
SELECT 1
FROM Stamp s
WHERE (s.user.id = :userId AND s.day = :day) AND s.deleted = FALSE
) as exist
""")
boolean existsByUserIdAndDayAndDeletedIsFalse(final Long userId, final int day);
}
Loading
Loading