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 30 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,80 @@
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.Day;
import com.backend.blooming.stamp.domain.Message;
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(new Day(goal, createStampDto.day()))
.message(new 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,23 @@
package com.backend.blooming.stamp.application.dto;

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

public record ReadStampDto(
Long id,
String userName,
ThemeColor userColor,
int day,
String message
) {

public static ReadStampDto from(final Stamp stamp) {
return new ReadStampDto(
stamp.getGoal().getId(),
stamp.getUser().getName(),
stamp.getUser().getColor(),
stamp.getDay().getDay(),
stamp.getMessage().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);
}
}
45 changes: 45 additions & 0 deletions src/main/java/com/backend/blooming/stamp/domain/Day.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.backend.blooming.stamp.domain;

import com.backend.blooming.goal.domain.Goal;
import com.backend.blooming.stamp.domain.exception.InvalidStampException;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

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

@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@EqualsAndHashCode
@ToString
public class Day {

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

public Day(final Goal goal, final int day) {
this.day = validateDay(goal, day);
}

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;
}
Copy link
Member

Choose a reason for hiding this comment

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

별건 아니고, GoalTerm만 사용할 거면, goal이 아닌 goalTerm을 넘겨줬어도 좋을 것 같아요!
중요한 부분은 아니라 효선님 편하신대로 하면 될 것 같습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

그렇네요! 수정해서 올리도록 하겠습니다!

}
38 changes: 38 additions & 0 deletions src/main/java/com/backend/blooming/stamp/domain/Message.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.backend.blooming.stamp.domain;

import com.backend.blooming.stamp.domain.exception.InvalidStampException;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@EqualsAndHashCode
@ToString
public class Message {

private static final int STAMP_MESSAGE_MAXIMUM = 30;

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

public Message(final String message) {
this.message = validateMessage(message);
}

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;
}
}
66 changes: 66 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,66 @@
package com.backend.blooming.stamp.domain;

import com.backend.blooming.common.entity.BaseTimeEntity;
import com.backend.blooming.goal.domain.Goal;
import com.backend.blooming.user.domain.User;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
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;

@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;

@Embedded
private Day day;

@Embedded
private Message message;

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

@Builder
private Stamp(
final Goal goal,
final User user,
final Day day,
final Message message
) {
this.goal = goal;
this.user = user;
this.day = day;
this.message = message;
}
}
Loading
Loading