From 6bbe196d66aec5bb55f1b755cc8269bd68b76e2c Mon Sep 17 00:00:00 2001 From: Junad-Park Date: Sun, 18 Jan 2026 16:30:19 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=ED=99=9C=EB=8F=99=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EB=B1=83=EC=A7=80=204=EC=A2=85=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(=EC=99=84=EB=B2=BD=EC=A3=BC=EC=9D=98,=20=EA=B9=8C?= =?UTF-8?q?=EB=A7=88=EA=B7=80,=20=EA=BD=A5=EA=BD=A5,=20=EC=A7=91=EC=A4=91?= =?UTF-8?q?=EC=B2=9C=EC=9E=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/checker/CrowBadgeChecker.java | 28 +++++++++ .../checker/FocusGeniusBadgeChecker.java | 28 +++++++++ .../checker/PerfectionistBadgeChecker.java | 28 +++++++++ .../checker/QuackQuackBadgeChecker.java | 28 +++++++++ .../domain/event/BadgeEventListener.java | 39 +++++++++++- .../domain/event/ConcentrationSavedEvent.java | 10 ++++ .../domain/service/ConcentrationService.java | 9 ++- .../repository/ConcentrationRepository.java | 3 + .../repository/RoutineRepository.java | 5 ++ .../domain/event/SocialCreatedEvent.java | 10 ++++ .../domain/service/SocialBoardService.java | 9 ++- .../domain/checker/CrowBadgeCheckerTest.java | 60 +++++++++++++++++++ .../checker/FocusGeniusBadgeCheckerTest.java | 60 +++++++++++++++++++ .../PerfectionistBadgeCheckerTest.java | 60 +++++++++++++++++++ .../checker/QuackQuackBadgeCheckerTest.java | 60 +++++++++++++++++++ 15 files changed, 434 insertions(+), 3 deletions(-) create mode 100644 src/main/java/im/toduck/domain/badge/domain/checker/CrowBadgeChecker.java create mode 100644 src/main/java/im/toduck/domain/badge/domain/checker/FocusGeniusBadgeChecker.java create mode 100644 src/main/java/im/toduck/domain/badge/domain/checker/PerfectionistBadgeChecker.java create mode 100644 src/main/java/im/toduck/domain/badge/domain/checker/QuackQuackBadgeChecker.java create mode 100644 src/main/java/im/toduck/domain/concentration/domain/event/ConcentrationSavedEvent.java create mode 100644 src/main/java/im/toduck/domain/social/domain/event/SocialCreatedEvent.java create mode 100644 src/test/java/im/toduck/domain/badge/domain/checker/CrowBadgeCheckerTest.java create mode 100644 src/test/java/im/toduck/domain/badge/domain/checker/FocusGeniusBadgeCheckerTest.java create mode 100644 src/test/java/im/toduck/domain/badge/domain/checker/PerfectionistBadgeCheckerTest.java create mode 100644 src/test/java/im/toduck/domain/badge/domain/checker/QuackQuackBadgeCheckerTest.java diff --git a/src/main/java/im/toduck/domain/badge/domain/checker/CrowBadgeChecker.java b/src/main/java/im/toduck/domain/badge/domain/checker/CrowBadgeChecker.java new file mode 100644 index 00000000..a1ed3da3 --- /dev/null +++ b/src/main/java/im/toduck/domain/badge/domain/checker/CrowBadgeChecker.java @@ -0,0 +1,28 @@ +package im.toduck.domain.badge.domain.checker; + +import org.springframework.stereotype.Component; + +import im.toduck.domain.badge.persistence.entity.BadgeCode; +import im.toduck.domain.routine.persistence.repository.RoutineRepository; +import im.toduck.domain.user.persistence.entity.User; +import lombok.RequiredArgsConstructor; + +/** + * 까마귀 뱃지 체커: 기억력 카테고리(Routine Category) 5개 이상 사용 시 지급 + */ +@Component +@RequiredArgsConstructor +public class CrowBadgeChecker implements BadgeConditionChecker { + + private final RoutineRepository routineRepository; + + @Override + public BadgeCode getBadgeCode() { + return BadgeCode.CROW; + } + + @Override + public boolean checkCondition(final User user) { + return routineRepository.countDistinctCategoryByUser(user) >= 5; + } +} diff --git a/src/main/java/im/toduck/domain/badge/domain/checker/FocusGeniusBadgeChecker.java b/src/main/java/im/toduck/domain/badge/domain/checker/FocusGeniusBadgeChecker.java new file mode 100644 index 00000000..2e655440 --- /dev/null +++ b/src/main/java/im/toduck/domain/badge/domain/checker/FocusGeniusBadgeChecker.java @@ -0,0 +1,28 @@ +package im.toduck.domain.badge.domain.checker; + +import org.springframework.stereotype.Component; + +import im.toduck.domain.badge.persistence.entity.BadgeCode; +import im.toduck.domain.concentration.persistence.repository.ConcentrationRepository; +import im.toduck.domain.user.persistence.entity.User; +import lombok.RequiredArgsConstructor; + +/** + * 집중천재 뱃지 체커: 타이머 15회 이상 사용 시 지급 + */ +@Component +@RequiredArgsConstructor +public class FocusGeniusBadgeChecker implements BadgeConditionChecker { + + private final ConcentrationRepository concentrationRepository; + + @Override + public BadgeCode getBadgeCode() { + return BadgeCode.FOCUS_GENIUS; + } + + @Override + public boolean checkCondition(final User user) { + return concentrationRepository.sumTargetCountByUser(user) >= 15; + } +} diff --git a/src/main/java/im/toduck/domain/badge/domain/checker/PerfectionistBadgeChecker.java b/src/main/java/im/toduck/domain/badge/domain/checker/PerfectionistBadgeChecker.java new file mode 100644 index 00000000..d9fd0074 --- /dev/null +++ b/src/main/java/im/toduck/domain/badge/domain/checker/PerfectionistBadgeChecker.java @@ -0,0 +1,28 @@ +package im.toduck.domain.badge.domain.checker; + +import org.springframework.stereotype.Component; + +import im.toduck.domain.badge.persistence.entity.BadgeCode; +import im.toduck.domain.routine.persistence.repository.RoutineRepository; +import im.toduck.domain.user.persistence.entity.User; +import lombok.RequiredArgsConstructor; + +/** + * 완벽주의 뱃지 체커: 루틴 10개 이상 등록 시 지급 + */ +@Component +@RequiredArgsConstructor +public class PerfectionistBadgeChecker implements BadgeConditionChecker { + + private final RoutineRepository routineRepository; + + @Override + public BadgeCode getBadgeCode() { + return BadgeCode.PERFECTIONIST; + } + + @Override + public boolean checkCondition(final User user) { + return routineRepository.countByUserAndDeletedAtIsNull(user) >= 10; + } +} diff --git a/src/main/java/im/toduck/domain/badge/domain/checker/QuackQuackBadgeChecker.java b/src/main/java/im/toduck/domain/badge/domain/checker/QuackQuackBadgeChecker.java new file mode 100644 index 00000000..ccc2c2b7 --- /dev/null +++ b/src/main/java/im/toduck/domain/badge/domain/checker/QuackQuackBadgeChecker.java @@ -0,0 +1,28 @@ +package im.toduck.domain.badge.domain.checker; + +import org.springframework.stereotype.Component; + +import im.toduck.domain.badge.persistence.entity.BadgeCode; +import im.toduck.domain.social.persistence.repository.SocialRepository; +import im.toduck.domain.user.persistence.entity.User; +import lombok.RequiredArgsConstructor; + +/** + * 꽥꽥 뱃지 체커: 소셜 글 15개 이상 작성 시 지급 + */ +@Component +@RequiredArgsConstructor +public class QuackQuackBadgeChecker implements BadgeConditionChecker { + + private final SocialRepository socialRepository; + + @Override + public BadgeCode getBadgeCode() { + return BadgeCode.QUACK_QUACK; + } + + @Override + public boolean checkCondition(final User user) { + return socialRepository.countByUserId(user.getId()) >= 15; + } +} diff --git a/src/main/java/im/toduck/domain/badge/domain/event/BadgeEventListener.java b/src/main/java/im/toduck/domain/badge/domain/event/BadgeEventListener.java index 2538d1f7..368f15c3 100644 --- a/src/main/java/im/toduck/domain/badge/domain/event/BadgeEventListener.java +++ b/src/main/java/im/toduck/domain/badge/domain/event/BadgeEventListener.java @@ -11,6 +11,9 @@ import im.toduck.domain.badge.domain.checker.BadgeConditionChecker; import im.toduck.domain.badge.domain.usecase.BadgeUseCase; import im.toduck.domain.badge.persistence.entity.BadgeCode; +import im.toduck.domain.concentration.domain.event.ConcentrationSavedEvent; +import im.toduck.domain.routine.domain.event.RoutineCreatedEvent; +import im.toduck.domain.social.domain.event.SocialCreatedEvent; import im.toduck.domain.user.domain.event.UserSignedUpEvent; import im.toduck.domain.user.domain.service.UserService; import im.toduck.domain.user.persistence.entity.User; @@ -39,6 +42,40 @@ public void handleUserSignedUp(final UserSignedUpEvent event) { checkAndGrantBadge(user, BadgeCode.BABY_DUCK); } + @Async + @EventListener + @Transactional + public void handleRoutineCreated(final RoutineCreatedEvent event) { + log.info("루틴 생성 이벤트 수신 - UserId: {}", event.getUserId()); + User user = userService.getUserById(event.getUserId()) + .orElseThrow(() -> CommonException.from(ExceptionCode.NOT_FOUND_USER)); + + checkAndGrantBadge(user, BadgeCode.PERFECTIONIST); + checkAndGrantBadge(user, BadgeCode.CROW); + } + + @Async + @EventListener + @Transactional + public void handleSocialCreated(final SocialCreatedEvent event) { + log.info("소셜 게시글 생성 이벤트 수신 - UserId: {}", event.getUserId()); + User user = userService.getUserById(event.getUserId()) + .orElseThrow(() -> CommonException.from(ExceptionCode.NOT_FOUND_USER)); + + checkAndGrantBadge(user, BadgeCode.QUACK_QUACK); + } + + @Async + @EventListener + @Transactional + public void handleConcentrationSaved(final ConcentrationSavedEvent event) { + log.info("집중 저장 이벤트 수신 - UserId: {}", event.getUserId()); + User user = userService.getUserById(event.getUserId()) + .orElseThrow(() -> CommonException.from(ExceptionCode.NOT_FOUND_USER)); + + checkAndGrantBadge(user, BadgeCode.FOCUS_GENIUS); + } + private void checkAndGrantBadge(final User user, final BadgeCode badgeCode) { findCheckerByBadgeCode(badgeCode) .filter(checker -> checker.checkCondition(user)) @@ -46,7 +83,7 @@ private void checkAndGrantBadge(final User user, final BadgeCode badgeCode) { badgeUseCase.grantBadge(user, badgeCode); }); - log.info("뱃지 부여 완료 - UserId: {}, BadgeCode: {}", user.getId(), badgeCode); + log.info("배지 부여 완료 - UserId: {}, BadgeCode: {}", user.getId(), badgeCode); } private Optional findCheckerByBadgeCode(final BadgeCode badgeCode) { diff --git a/src/main/java/im/toduck/domain/concentration/domain/event/ConcentrationSavedEvent.java b/src/main/java/im/toduck/domain/concentration/domain/event/ConcentrationSavedEvent.java new file mode 100644 index 00000000..ddbb517c --- /dev/null +++ b/src/main/java/im/toduck/domain/concentration/domain/event/ConcentrationSavedEvent.java @@ -0,0 +1,10 @@ +package im.toduck.domain.concentration.domain.event; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class ConcentrationSavedEvent { + private final Long userId; +} diff --git a/src/main/java/im/toduck/domain/concentration/domain/service/ConcentrationService.java b/src/main/java/im/toduck/domain/concentration/domain/service/ConcentrationService.java index dd73a28d..78b3164d 100644 --- a/src/main/java/im/toduck/domain/concentration/domain/service/ConcentrationService.java +++ b/src/main/java/im/toduck/domain/concentration/domain/service/ConcentrationService.java @@ -4,9 +4,11 @@ import java.time.YearMonth; import java.util.List; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import im.toduck.domain.concentration.common.mapper.ConcentrationMapper; +import im.toduck.domain.concentration.domain.event.ConcentrationSavedEvent; import im.toduck.domain.concentration.persistence.entity.Concentration; import im.toduck.domain.concentration.persistence.repository.ConcentrationRepository; import im.toduck.domain.concentration.presentation.dto.request.ConcentrationRequest; @@ -20,6 +22,7 @@ @RequiredArgsConstructor public class ConcentrationService { private final ConcentrationRepository concentrationRepository; + private final ApplicationEventPublisher eventPublisher; @Transactional public Concentration saveConcentration(User user, ConcentrationRequest request) { @@ -30,7 +33,11 @@ public Concentration saveConcentration(User user, ConcentrationRequest request) concentration.addSettingCount(request.settingCount()); concentration.addTime(request.time()); - return concentrationRepository.save(concentration); + Concentration saved = concentrationRepository.save(concentration); + + eventPublisher.publishEvent(new ConcentrationSavedEvent(user.getId())); + + return saved; } @Transactional diff --git a/src/main/java/im/toduck/domain/concentration/persistence/repository/ConcentrationRepository.java b/src/main/java/im/toduck/domain/concentration/persistence/repository/ConcentrationRepository.java index a57686ac..9168a257 100644 --- a/src/main/java/im/toduck/domain/concentration/persistence/repository/ConcentrationRepository.java +++ b/src/main/java/im/toduck/domain/concentration/persistence/repository/ConcentrationRepository.java @@ -22,4 +22,7 @@ public interface ConcentrationRepository extends JpaRepository, Routine @Query("SELECT COUNT(DISTINCT r.user) FROM Routine r WHERE r.deletedAt IS NULL") long countDistinctUsers(); + + long countByUserAndDeletedAtIsNull(User user); + + @Query("SELECT COUNT(DISTINCT r.category) FROM Routine r WHERE r.user = :user AND r.deletedAt IS NULL") + long countDistinctCategoryByUser(@Param("user") User user); } diff --git a/src/main/java/im/toduck/domain/social/domain/event/SocialCreatedEvent.java b/src/main/java/im/toduck/domain/social/domain/event/SocialCreatedEvent.java new file mode 100644 index 00000000..2f29407f --- /dev/null +++ b/src/main/java/im/toduck/domain/social/domain/event/SocialCreatedEvent.java @@ -0,0 +1,10 @@ +package im.toduck.domain.social.domain.event; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class SocialCreatedEvent { + private final Long userId; +} diff --git a/src/main/java/im/toduck/domain/social/domain/service/SocialBoardService.java b/src/main/java/im/toduck/domain/social/domain/service/SocialBoardService.java index 527b9cd0..0a3c347c 100644 --- a/src/main/java/im/toduck/domain/social/domain/service/SocialBoardService.java +++ b/src/main/java/im/toduck/domain/social/domain/service/SocialBoardService.java @@ -8,6 +8,7 @@ import java.util.Optional; import java.util.stream.Collectors; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,6 +18,7 @@ import im.toduck.domain.social.common.mapper.SocialCategoryLinkMapper; import im.toduck.domain.social.common.mapper.SocialImageFileMapper; import im.toduck.domain.social.common.mapper.SocialMapper; +import im.toduck.domain.social.domain.event.SocialCreatedEvent; import im.toduck.domain.social.persistence.entity.Comment; import im.toduck.domain.social.persistence.entity.CommentImageFile; import im.toduck.domain.social.persistence.entity.CommentLike; @@ -55,6 +57,7 @@ public class SocialBoardService { private final CommentLikeRepository commentLikeRepository; private final CommentImageFileRepository commentImageFileRepository; private final LikeRepository likeRepository; + private final ApplicationEventPublisher eventPublisher; @Transactional(readOnly = true) public Optional getSocialById(final Long socialId) { @@ -74,7 +77,11 @@ public Social createSocialBoard( request.content(), request.isAnonymous() ); - return socialRepository.save(socialBoard); + Social savedSocial = socialRepository.save(socialBoard); + + eventPublisher.publishEvent(new SocialCreatedEvent(user.getId())); + + return savedSocial; } @Transactional diff --git a/src/test/java/im/toduck/domain/badge/domain/checker/CrowBadgeCheckerTest.java b/src/test/java/im/toduck/domain/badge/domain/checker/CrowBadgeCheckerTest.java new file mode 100644 index 00000000..405a61b0 --- /dev/null +++ b/src/test/java/im/toduck/domain/badge/domain/checker/CrowBadgeCheckerTest.java @@ -0,0 +1,60 @@ +package im.toduck.domain.badge.domain.checker; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import im.toduck.domain.badge.persistence.entity.BadgeCode; +import im.toduck.domain.routine.persistence.repository.RoutineRepository; +import im.toduck.domain.user.persistence.entity.User; +import im.toduck.fixtures.user.UserFixtures; + +@ExtendWith(MockitoExtension.class) +class CrowBadgeCheckerTest { + + @InjectMocks + private CrowBadgeChecker crowBadgeChecker; + + @Mock + private RoutineRepository routineRepository; + + @Test + @DisplayName("뱃지 코드는 CROW여야 한다") + void getBadgeCode() { + assertThat(crowBadgeChecker.getBadgeCode()).isEqualTo(BadgeCode.CROW); + } + + @Test + @DisplayName("루틴 카테고리가 5개 이상이면 true를 반환한다") + void checkCondition_True() { + // given + User user = UserFixtures.GENERAL_USER(); + given(routineRepository.countDistinctCategoryByUser(user)).willReturn(5L); + + // when + boolean result = crowBadgeChecker.checkCondition(user); + + // then + assertThat(result).isTrue(); + } + + @Test + @DisplayName("루틴 카테고리가 5개 미만이면 false를 반환한다") + void checkCondition_False() { + // given + User user = UserFixtures.GENERAL_USER(); + given(routineRepository.countDistinctCategoryByUser(user)).willReturn(4L); + + // when + boolean result = crowBadgeChecker.checkCondition(user); + + // then + assertThat(result).isFalse(); + } +} diff --git a/src/test/java/im/toduck/domain/badge/domain/checker/FocusGeniusBadgeCheckerTest.java b/src/test/java/im/toduck/domain/badge/domain/checker/FocusGeniusBadgeCheckerTest.java new file mode 100644 index 00000000..9c39bf38 --- /dev/null +++ b/src/test/java/im/toduck/domain/badge/domain/checker/FocusGeniusBadgeCheckerTest.java @@ -0,0 +1,60 @@ +package im.toduck.domain.badge.domain.checker; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import im.toduck.domain.badge.persistence.entity.BadgeCode; +import im.toduck.domain.concentration.persistence.repository.ConcentrationRepository; +import im.toduck.domain.user.persistence.entity.User; +import im.toduck.fixtures.user.UserFixtures; + +@ExtendWith(MockitoExtension.class) +class FocusGeniusBadgeCheckerTest { + + @InjectMocks + private FocusGeniusBadgeChecker focusGeniusBadgeChecker; + + @Mock + private ConcentrationRepository concentrationRepository; + + @Test + @DisplayName("뱃지 코드는 FOCUS_GENIUS여야 한다") + void getBadgeCode() { + assertThat(focusGeniusBadgeChecker.getBadgeCode()).isEqualTo(BadgeCode.FOCUS_GENIUS); + } + + @Test + @DisplayName("타이머 달성 횟수 합계가 15회 이상이면 true를 반환한다") + void checkCondition_True() { + // given + User user = UserFixtures.GENERAL_USER(); + given(concentrationRepository.sumTargetCountByUser(user)).willReturn(15L); + + // when + boolean result = focusGeniusBadgeChecker.checkCondition(user); + + // then + assertThat(result).isTrue(); + } + + @Test + @DisplayName("타이머 달성 횟수 합계가 15회 미만이면 false를 반환한다") + void checkCondition_False() { + // given + User user = UserFixtures.GENERAL_USER(); + given(concentrationRepository.sumTargetCountByUser(user)).willReturn(14L); + + // when + boolean result = focusGeniusBadgeChecker.checkCondition(user); + + // then + assertThat(result).isFalse(); + } +} diff --git a/src/test/java/im/toduck/domain/badge/domain/checker/PerfectionistBadgeCheckerTest.java b/src/test/java/im/toduck/domain/badge/domain/checker/PerfectionistBadgeCheckerTest.java new file mode 100644 index 00000000..f083dcf2 --- /dev/null +++ b/src/test/java/im/toduck/domain/badge/domain/checker/PerfectionistBadgeCheckerTest.java @@ -0,0 +1,60 @@ +package im.toduck.domain.badge.domain.checker; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import im.toduck.domain.badge.persistence.entity.BadgeCode; +import im.toduck.domain.routine.persistence.repository.RoutineRepository; +import im.toduck.domain.user.persistence.entity.User; +import im.toduck.fixtures.user.UserFixtures; + +@ExtendWith(MockitoExtension.class) +class PerfectionistBadgeCheckerTest { + + @InjectMocks + private PerfectionistBadgeChecker perfectionistBadgeChecker; + + @Mock + private RoutineRepository routineRepository; + + @Test + @DisplayName("뱃지 코드는 PERFECTIONIST여야 한다") + void getBadgeCode() { + assertThat(perfectionistBadgeChecker.getBadgeCode()).isEqualTo(BadgeCode.PERFECTIONIST); + } + + @Test + @DisplayName("루틴이 10개 이상이면 true를 반환한다") + void checkCondition_True() { + // given + User user = UserFixtures.GENERAL_USER(); + given(routineRepository.countByUserAndDeletedAtIsNull(user)).willReturn(10L); + + // when + boolean result = perfectionistBadgeChecker.checkCondition(user); + + // then + assertThat(result).isTrue(); + } + + @Test + @DisplayName("루틴이 10개 미만이면 false를 반환한다") + void checkCondition_False() { + // given + User user = UserFixtures.GENERAL_USER(); + given(routineRepository.countByUserAndDeletedAtIsNull(user)).willReturn(9L); + + // when + boolean result = perfectionistBadgeChecker.checkCondition(user); + + // then + assertThat(result).isFalse(); + } +} diff --git a/src/test/java/im/toduck/domain/badge/domain/checker/QuackQuackBadgeCheckerTest.java b/src/test/java/im/toduck/domain/badge/domain/checker/QuackQuackBadgeCheckerTest.java new file mode 100644 index 00000000..0881e346 --- /dev/null +++ b/src/test/java/im/toduck/domain/badge/domain/checker/QuackQuackBadgeCheckerTest.java @@ -0,0 +1,60 @@ +package im.toduck.domain.badge.domain.checker; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import im.toduck.domain.badge.persistence.entity.BadgeCode; +import im.toduck.domain.social.persistence.repository.SocialRepository; +import im.toduck.domain.user.persistence.entity.User; +import im.toduck.fixtures.user.UserFixtures; + +@ExtendWith(MockitoExtension.class) +class QuackQuackBadgeCheckerTest { + + @InjectMocks + private QuackQuackBadgeChecker quackQuackBadgeChecker; + + @Mock + private SocialRepository socialRepository; + + @Test + @DisplayName("뱃지 코드는 QUACK_QUACK이어야 한다") + void getBadgeCode() { + assertThat(quackQuackBadgeChecker.getBadgeCode()).isEqualTo(BadgeCode.QUACK_QUACK); + } + + @Test + @DisplayName("소셜 게시글이 15개 이상이면 true를 반환한다") + void checkCondition_True() { + // given + User user = UserFixtures.GENERAL_USER(); + given(socialRepository.countByUserId(user.getId())).willReturn(15L); + + // when + boolean result = quackQuackBadgeChecker.checkCondition(user); + + // then + assertThat(result).isTrue(); + } + + @Test + @DisplayName("소셜 게시글이 15개 미만이면 false를 반환한다") + void checkCondition_False() { + // given + User user = UserFixtures.GENERAL_USER(); + given(socialRepository.countByUserId(user.getId())).willReturn(14L); + + // when + boolean result = quackQuackBadgeChecker.checkCondition(user); + + // then + assertThat(result).isFalse(); + } +} From 2ebf531c074d33a38fc4df3e65a556ebb02f800c Mon Sep 17 00:00:00 2001 From: Junad-Park Date: Sun, 18 Jan 2026 17:21:27 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=ED=95=98=EB=A3=A8=EC=9D=BC?= =?UTF-8?q?=EA=B8=B0=20=EB=B1=83=EC=A7=80=20=ED=9A=8D=EB=93=9D=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../checker/DailyDiaryBadgeChecker.java | 40 ++++++++++ .../domain/event/BadgeEventListener.java | 12 +++ .../badge/persistence/entity/BadgeCode.java | 2 +- .../diary/domain/event/DiaryCreatedEvent.java | 10 +++ .../diary/domain/service/DiaryService.java | 9 ++- .../repository/DiaryRepository.java | 4 + .../im/toduck/builder/BuilderSupporter.java | 8 ++ .../im/toduck/builder/TestFixtureBuilder.java | 5 ++ .../checker/DailyDiaryBadgeCheckerTest.java | 75 +++++++++++++++++++ .../toduck/fixtures/diary/DiaryFixtures.java | 20 +++++ 10 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 src/main/java/im/toduck/domain/badge/domain/checker/DailyDiaryBadgeChecker.java create mode 100644 src/main/java/im/toduck/domain/diary/domain/event/DiaryCreatedEvent.java create mode 100644 src/test/java/im/toduck/domain/badge/domain/checker/DailyDiaryBadgeCheckerTest.java create mode 100644 src/test/java/im/toduck/fixtures/diary/DiaryFixtures.java diff --git a/src/main/java/im/toduck/domain/badge/domain/checker/DailyDiaryBadgeChecker.java b/src/main/java/im/toduck/domain/badge/domain/checker/DailyDiaryBadgeChecker.java new file mode 100644 index 00000000..720cfe3b --- /dev/null +++ b/src/main/java/im/toduck/domain/badge/domain/checker/DailyDiaryBadgeChecker.java @@ -0,0 +1,40 @@ +package im.toduck.domain.badge.domain.checker; + +import java.time.LocalDate; +import java.time.YearMonth; + +import org.springframework.stereotype.Component; + +import im.toduck.domain.badge.persistence.entity.BadgeCode; +import im.toduck.domain.diary.persistence.repository.DiaryRepository; +import im.toduck.domain.user.persistence.entity.User; +import lombok.RequiredArgsConstructor; + +/** + * 하루일기 뱃지 체커: 감정일기 한 달 작성률 50% 이상 달성 시 지급 + */ +@Component +@RequiredArgsConstructor +public class DailyDiaryBadgeChecker implements BadgeConditionChecker { + private static final int FIRST_DAY_OF_MONTH = 1; + private static final double MINIMUM_WRITTEN_RATIO = 0.5; + + private final DiaryRepository diaryRepository; + + @Override + public BadgeCode getBadgeCode() { + return BadgeCode.DAILY_DIARY; + } + + @Override + public boolean checkCondition(final User user) { + YearMonth currentMonth = YearMonth.now(); + LocalDate startDate = currentMonth.atDay(FIRST_DAY_OF_MONTH); + LocalDate endDate = currentMonth.atEndOfMonth(); + int daysInMonth = currentMonth.lengthOfMonth(); + + long writtenDays = diaryRepository.countDistinctDateByUserAndDateBetween(user, startDate, endDate); + + return writtenDays >= daysInMonth * MINIMUM_WRITTEN_RATIO; + } +} diff --git a/src/main/java/im/toduck/domain/badge/domain/event/BadgeEventListener.java b/src/main/java/im/toduck/domain/badge/domain/event/BadgeEventListener.java index 368f15c3..7c285825 100644 --- a/src/main/java/im/toduck/domain/badge/domain/event/BadgeEventListener.java +++ b/src/main/java/im/toduck/domain/badge/domain/event/BadgeEventListener.java @@ -12,6 +12,7 @@ import im.toduck.domain.badge.domain.usecase.BadgeUseCase; import im.toduck.domain.badge.persistence.entity.BadgeCode; import im.toduck.domain.concentration.domain.event.ConcentrationSavedEvent; +import im.toduck.domain.diary.domain.event.DiaryCreatedEvent; import im.toduck.domain.routine.domain.event.RoutineCreatedEvent; import im.toduck.domain.social.domain.event.SocialCreatedEvent; import im.toduck.domain.user.domain.event.UserSignedUpEvent; @@ -76,6 +77,17 @@ public void handleConcentrationSaved(final ConcentrationSavedEvent event) { checkAndGrantBadge(user, BadgeCode.FOCUS_GENIUS); } + @Async + @EventListener + @Transactional + public void handleDiaryCreated(final DiaryCreatedEvent event) { + log.info("일기 생성 이벤트 수신 - UserId: {}", event.getUserId()); + User user = userService.getUserById(event.getUserId()) + .orElseThrow(() -> CommonException.from(ExceptionCode.NOT_FOUND_USER)); + + checkAndGrantBadge(user, BadgeCode.DAILY_DIARY); + } + private void checkAndGrantBadge(final User user, final BadgeCode badgeCode) { findCheckerByBadgeCode(badgeCode) .filter(checker -> checker.checkCondition(user)) diff --git a/src/main/java/im/toduck/domain/badge/persistence/entity/BadgeCode.java b/src/main/java/im/toduck/domain/badge/persistence/entity/BadgeCode.java index 3ed23932..65cbdc1d 100644 --- a/src/main/java/im/toduck/domain/badge/persistence/entity/BadgeCode.java +++ b/src/main/java/im/toduck/domain/badge/persistence/entity/BadgeCode.java @@ -10,7 +10,7 @@ public enum BadgeCode { CROW("까마귀"), // 기억력 카테고리 5개 QUACK_QUACK("꽥꽥"), // 소셜 글 15개 FOCUS_GENIUS("집중천재"), // 타이머 15회 - PAT_PAT("토닥토닥"), // 감정일기 월 50% + DAILY_DIARY("하루일기"), // 감정일기 월 50% // 출석 기반 THREE_DAYS_STREAK("작심삼일"), diff --git a/src/main/java/im/toduck/domain/diary/domain/event/DiaryCreatedEvent.java b/src/main/java/im/toduck/domain/diary/domain/event/DiaryCreatedEvent.java new file mode 100644 index 00000000..8a841344 --- /dev/null +++ b/src/main/java/im/toduck/domain/diary/domain/event/DiaryCreatedEvent.java @@ -0,0 +1,10 @@ +package im.toduck.domain.diary.domain.event; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class DiaryCreatedEvent { + private final Long userId; +} diff --git a/src/main/java/im/toduck/domain/diary/domain/service/DiaryService.java b/src/main/java/im/toduck/domain/diary/domain/service/DiaryService.java index bd81d514..13a18671 100644 --- a/src/main/java/im/toduck/domain/diary/domain/service/DiaryService.java +++ b/src/main/java/im/toduck/domain/diary/domain/service/DiaryService.java @@ -10,11 +10,13 @@ import java.util.Optional; import java.util.stream.Collectors; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import im.toduck.domain.diary.common.mapper.DiaryImageFileMapper; import im.toduck.domain.diary.common.mapper.DiaryMapper; +import im.toduck.domain.diary.domain.event.DiaryCreatedEvent; import im.toduck.domain.diary.persistence.entity.Diary; import im.toduck.domain.diary.persistence.entity.DiaryImage; import im.toduck.domain.diary.persistence.repository.DiaryImageRepository; @@ -35,6 +37,7 @@ public class DiaryService { private final DiaryRepository diaryRepository; private final DiaryImageRepository diaryImageRepository; + private final ApplicationEventPublisher eventPublisher; @Transactional public Diary createDiary( @@ -42,7 +45,11 @@ public Diary createDiary( final DiaryCreateRequest request ) { Diary diary = DiaryMapper.toDiary(user, request); - return diaryRepository.save(diary); + Diary savedDiary = diaryRepository.save(diary); + + eventPublisher.publishEvent(new DiaryCreatedEvent(user.getId())); + + return savedDiary; } @Transactional diff --git a/src/main/java/im/toduck/domain/diary/persistence/repository/DiaryRepository.java b/src/main/java/im/toduck/domain/diary/persistence/repository/DiaryRepository.java index bb58851e..03a3fd38 100644 --- a/src/main/java/im/toduck/domain/diary/persistence/repository/DiaryRepository.java +++ b/src/main/java/im/toduck/domain/diary/persistence/repository/DiaryRepository.java @@ -31,4 +31,8 @@ public interface DiaryRepository extends JpaRepository, DiaryReposi long countDistinctUsers(); Optional getDiaryByUserIdAndId(Long userId, Long diaryId); + + @Query("SELECT COUNT(DISTINCT d.date) FROM Diary d WHERE d.user = :user AND d.date BETWEEN :startDate AND :endDate") + long countDistinctDateByUserAndDateBetween(@Param("user") User user, @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); } diff --git a/src/test/java/im/toduck/builder/BuilderSupporter.java b/src/test/java/im/toduck/builder/BuilderSupporter.java index 58baebf2..f88e7aee 100644 --- a/src/test/java/im/toduck/builder/BuilderSupporter.java +++ b/src/test/java/im/toduck/builder/BuilderSupporter.java @@ -5,6 +5,7 @@ import im.toduck.domain.badge.persistence.repository.BadgeRepository; import im.toduck.domain.badge.persistence.repository.UserBadgeRepository; +import im.toduck.domain.diary.persistence.repository.DiaryRepository; import im.toduck.domain.diary.persistence.repository.MasterKeywordRepository; import im.toduck.domain.diary.persistence.repository.UserKeywordRepository; import im.toduck.domain.routine.persistence.repository.RoutineRecordRepository; @@ -77,6 +78,9 @@ public class BuilderSupporter { @Autowired private UserKeywordRepository userKeywordRepository; + @Autowired + private DiaryRepository diaryRepository; + @Autowired private BadgeRepository badgeRepository; @@ -151,6 +155,10 @@ public UserKeywordRepository userKeywordRepository() { return userKeywordRepository; } + public DiaryRepository diaryRepository() { + return diaryRepository; + } + public BadgeRepository badgeRepository() { return badgeRepository; } diff --git a/src/test/java/im/toduck/builder/TestFixtureBuilder.java b/src/test/java/im/toduck/builder/TestFixtureBuilder.java index 147bac3c..5d04f591 100644 --- a/src/test/java/im/toduck/builder/TestFixtureBuilder.java +++ b/src/test/java/im/toduck/builder/TestFixtureBuilder.java @@ -9,6 +9,7 @@ import im.toduck.domain.badge.persistence.entity.Badge; import im.toduck.domain.badge.persistence.entity.UserBadge; +import im.toduck.domain.diary.persistence.entity.Diary; import im.toduck.domain.diary.persistence.entity.KeywordCategory; import im.toduck.domain.diary.persistence.entity.MasterKeyword; import im.toduck.domain.routine.persistence.entity.Routine; @@ -120,6 +121,10 @@ public ScheduleRecord buildScheduleRecord(final ScheduleRecord scheduleRecord) { return bs.scheduleRecordRepository().save(scheduleRecord); } + public Diary buildDiary(final Diary diary) { + return bs.diaryRepository().save(diary); + } + public Follow buildFollow(final User follower, final User followed) { Follow follow = Follow.builder() .follower(follower) diff --git a/src/test/java/im/toduck/domain/badge/domain/checker/DailyDiaryBadgeCheckerTest.java b/src/test/java/im/toduck/domain/badge/domain/checker/DailyDiaryBadgeCheckerTest.java new file mode 100644 index 00000000..eefbf143 --- /dev/null +++ b/src/test/java/im/toduck/domain/badge/domain/checker/DailyDiaryBadgeCheckerTest.java @@ -0,0 +1,75 @@ +package im.toduck.domain.badge.domain.checker; + +import static im.toduck.fixtures.diary.DiaryFixtures.*; +import static im.toduck.fixtures.user.UserFixtures.*; +import static org.assertj.core.api.Assertions.*; + +import java.time.YearMonth; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +import im.toduck.ServiceTest; +import im.toduck.domain.badge.persistence.entity.BadgeCode; +import im.toduck.domain.user.persistence.entity.User; + +@Transactional +class DailyDiaryBadgeCheckerTest extends ServiceTest { + + @Autowired + private DailyDiaryBadgeChecker dailyDiaryBadgeChecker; + + private User user; + + @BeforeEach + void setUp() { + user = testFixtureBuilder.buildUser(GENERAL_USER()); + } + + @Test + @DisplayName("뱃지 코드는 DAILY_DIARY여야 한다") + void getBadgeCode() { + assertThat(dailyDiaryBadgeChecker.getBadgeCode()).isEqualTo(BadgeCode.DAILY_DIARY); + } + + @Test + @DisplayName("이번 달 일기 작성률이 50% 이상이면 true를 반환한다") + void checkCondition_True() { + // given + YearMonth currentMonth = YearMonth.now(); + int daysInMonth = currentMonth.lengthOfMonth(); + int requiredDays = (int)Math.ceil(daysInMonth * 0.5); + + for (int i = 1; i <= requiredDays; i++) { + testFixtureBuilder.buildDiary(DIARY(user, currentMonth.atDay(i))); + } + + // when + boolean result = dailyDiaryBadgeChecker.checkCondition(user); + + // then + assertThat(result).isTrue(); + } + + @Test + @DisplayName("이번 달 일기 작성률이 50% 미만이면 false를 반환한다") + void checkCondition_False() { + // given + YearMonth currentMonth = YearMonth.now(); + int daysInMonth = currentMonth.lengthOfMonth(); + int insufficientDays = (int)Math.ceil(daysInMonth * 0.5) - 1; + + for (int i = 1; i <= insufficientDays; i++) { + testFixtureBuilder.buildDiary(DIARY(user, currentMonth.atDay(i))); + } + + // when + boolean result = dailyDiaryBadgeChecker.checkCondition(user); + + // then + assertThat(result).isFalse(); + } +} diff --git a/src/test/java/im/toduck/fixtures/diary/DiaryFixtures.java b/src/test/java/im/toduck/fixtures/diary/DiaryFixtures.java new file mode 100644 index 00000000..592a839b --- /dev/null +++ b/src/test/java/im/toduck/fixtures/diary/DiaryFixtures.java @@ -0,0 +1,20 @@ +package im.toduck.fixtures.diary; + +import java.time.LocalDate; + +import im.toduck.domain.diary.persistence.entity.Diary; +import im.toduck.domain.user.persistence.entity.Emotion; +import im.toduck.domain.user.persistence.entity.User; + +public class DiaryFixtures { + + public static Diary DIARY(User user, LocalDate date) { + return Diary.builder() + .user(user) + .date(date) + .emotion(Emotion.HAPPY) + .title("오늘의 일기") + .memo("오늘은 정말 보람찬 하루였다.") + .build(); + } +} From 3dfd0d05a377fdad6bf3d6a0fe6f5c07d73335b7 Mon Sep 17 00:00:00 2001 From: Junad-Park Date: Sun, 18 Jan 2026 17:48:10 +0900 Subject: [PATCH 3/3] =?UTF-8?q?test:=20=EB=B1=83=EC=A7=80=20=EC=B2=B4?= =?UTF-8?q?=EC=BB=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../im/toduck/builder/BuilderSupporter.java | 8 +++ .../im/toduck/builder/TestFixtureBuilder.java | 5 ++ .../domain/checker/CrowBadgeCheckerTest.java | 61 ++++++++++++++----- .../checker/FocusGeniusBadgeCheckerTest.java | 47 +++++++++----- .../PerfectionistBadgeCheckerTest.java | 37 ++++++----- .../checker/QuackQuackBadgeCheckerTest.java | 37 ++++++----- 6 files changed, 131 insertions(+), 64 deletions(-) diff --git a/src/test/java/im/toduck/builder/BuilderSupporter.java b/src/test/java/im/toduck/builder/BuilderSupporter.java index f88e7aee..75b9950e 100644 --- a/src/test/java/im/toduck/builder/BuilderSupporter.java +++ b/src/test/java/im/toduck/builder/BuilderSupporter.java @@ -5,6 +5,7 @@ import im.toduck.domain.badge.persistence.repository.BadgeRepository; import im.toduck.domain.badge.persistence.repository.UserBadgeRepository; +import im.toduck.domain.concentration.persistence.repository.ConcentrationRepository; import im.toduck.domain.diary.persistence.repository.DiaryRepository; import im.toduck.domain.diary.persistence.repository.MasterKeywordRepository; import im.toduck.domain.diary.persistence.repository.UserKeywordRepository; @@ -87,6 +88,9 @@ public class BuilderSupporter { @Autowired private UserBadgeRepository userBadgeRepository; + @Autowired + private ConcentrationRepository concentrationRepository; + public UserRepository userRepository() { return userRepository; } @@ -166,4 +170,8 @@ public BadgeRepository badgeRepository() { public UserBadgeRepository userBadgeRepository() { return userBadgeRepository; } + + public ConcentrationRepository concentrationRepository() { + return concentrationRepository; + } } diff --git a/src/test/java/im/toduck/builder/TestFixtureBuilder.java b/src/test/java/im/toduck/builder/TestFixtureBuilder.java index 5d04f591..b930ab40 100644 --- a/src/test/java/im/toduck/builder/TestFixtureBuilder.java +++ b/src/test/java/im/toduck/builder/TestFixtureBuilder.java @@ -9,6 +9,7 @@ import im.toduck.domain.badge.persistence.entity.Badge; import im.toduck.domain.badge.persistence.entity.UserBadge; +import im.toduck.domain.concentration.persistence.entity.Concentration; import im.toduck.domain.diary.persistence.entity.Diary; import im.toduck.domain.diary.persistence.entity.KeywordCategory; import im.toduck.domain.diary.persistence.entity.MasterKeyword; @@ -153,4 +154,8 @@ public UserBadge buildUserBadge(final User user, final Badge badge, boolean isRe ReflectionTestUtils.setField(userBadge, "isRepresentative", isRepresentative); return bs.userBadgeRepository().save(userBadge); } + + public Concentration buildConcentration(final Concentration concentration) { + return bs.concentrationRepository().save(concentration); + } } diff --git a/src/test/java/im/toduck/domain/badge/domain/checker/CrowBadgeCheckerTest.java b/src/test/java/im/toduck/domain/badge/domain/checker/CrowBadgeCheckerTest.java index 405a61b0..fe1e690d 100644 --- a/src/test/java/im/toduck/domain/badge/domain/checker/CrowBadgeCheckerTest.java +++ b/src/test/java/im/toduck/domain/badge/domain/checker/CrowBadgeCheckerTest.java @@ -1,28 +1,37 @@ package im.toduck.domain.badge.domain.checker; +import static im.toduck.fixtures.user.UserFixtures.*; import static org.assertj.core.api.Assertions.*; -import static org.mockito.BDDMockito.*; +import java.time.DayOfWeek; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import im.toduck.ServiceTest; import im.toduck.domain.badge.persistence.entity.BadgeCode; -import im.toduck.domain.routine.persistence.repository.RoutineRepository; +import im.toduck.domain.person.persistence.entity.PlanCategory; +import im.toduck.domain.routine.persistence.entity.Routine; +import im.toduck.domain.routine.persistence.vo.PlanCategoryColor; import im.toduck.domain.user.persistence.entity.User; -import im.toduck.fixtures.user.UserFixtures; +import im.toduck.global.helper.DaysOfWeekBitmask; -@ExtendWith(MockitoExtension.class) -class CrowBadgeCheckerTest { +@Transactional +class CrowBadgeCheckerTest extends ServiceTest { - @InjectMocks + @Autowired private CrowBadgeChecker crowBadgeChecker; - @Mock - private RoutineRepository routineRepository; + private User user; + + @BeforeEach + void setUp() { + user = testFixtureBuilder.buildUser(GENERAL_USER()); + } @Test @DisplayName("뱃지 코드는 CROW여야 한다") @@ -34,8 +43,12 @@ void getBadgeCode() { @DisplayName("루틴 카테고리가 5개 이상이면 true를 반환한다") void checkCondition_True() { // given - User user = UserFixtures.GENERAL_USER(); - given(routineRepository.countDistinctCategoryByUser(user)).willReturn(5L); + // 5개의 서로 다른 카테고리 루틴 생성 + createRoutineWithCategory(PlanCategory.COMPUTER); + createRoutineWithCategory(PlanCategory.FOOD); + createRoutineWithCategory(PlanCategory.PENCIL); + createRoutineWithCategory(PlanCategory.RED_BOOK); + createRoutineWithCategory(PlanCategory.YELLOW_BOOK); // when boolean result = crowBadgeChecker.checkCondition(user); @@ -48,8 +61,12 @@ void checkCondition_True() { @DisplayName("루틴 카테고리가 5개 미만이면 false를 반환한다") void checkCondition_False() { // given - User user = UserFixtures.GENERAL_USER(); - given(routineRepository.countDistinctCategoryByUser(user)).willReturn(4L); + // 4개의 서로 다른 카테고리 루틴 생성 (하나는 중복) + createRoutineWithCategory(PlanCategory.COMPUTER); + createRoutineWithCategory(PlanCategory.FOOD); + createRoutineWithCategory(PlanCategory.PENCIL); + createRoutineWithCategory(PlanCategory.RED_BOOK); + createRoutineWithCategory(PlanCategory.RED_BOOK); // 중복 카테고리 // when boolean result = crowBadgeChecker.checkCondition(user); @@ -57,4 +74,16 @@ void checkCondition_False() { // then assertThat(result).isFalse(); } + + private void createRoutineWithCategory(PlanCategory category) { + Routine routine = Routine.builder() + .user(user) + .title("Test Routine") + .category(category) + .color(PlanCategoryColor.from("#FF0000")) + .isPublic(true) + .daysOfWeekBitmask(DaysOfWeekBitmask.createByDayOfWeek(List.of(DayOfWeek.MONDAY))) + .build(); + testFixtureBuilder.buildRoutine(routine); + } } diff --git a/src/test/java/im/toduck/domain/badge/domain/checker/FocusGeniusBadgeCheckerTest.java b/src/test/java/im/toduck/domain/badge/domain/checker/FocusGeniusBadgeCheckerTest.java index 9c39bf38..adc46184 100644 --- a/src/test/java/im/toduck/domain/badge/domain/checker/FocusGeniusBadgeCheckerTest.java +++ b/src/test/java/im/toduck/domain/badge/domain/checker/FocusGeniusBadgeCheckerTest.java @@ -1,28 +1,33 @@ package im.toduck.domain.badge.domain.checker; +import static im.toduck.fixtures.user.UserFixtures.*; import static org.assertj.core.api.Assertions.*; -import static org.mockito.BDDMockito.*; +import java.time.LocalDate; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import im.toduck.ServiceTest; import im.toduck.domain.badge.persistence.entity.BadgeCode; -import im.toduck.domain.concentration.persistence.repository.ConcentrationRepository; +import im.toduck.domain.concentration.persistence.entity.Concentration; import im.toduck.domain.user.persistence.entity.User; -import im.toduck.fixtures.user.UserFixtures; -@ExtendWith(MockitoExtension.class) -class FocusGeniusBadgeCheckerTest { +@Transactional +class FocusGeniusBadgeCheckerTest extends ServiceTest { - @InjectMocks + @Autowired private FocusGeniusBadgeChecker focusGeniusBadgeChecker; - @Mock - private ConcentrationRepository concentrationRepository; + private User user; + + @BeforeEach + void setUp() { + user = testFixtureBuilder.buildUser(GENERAL_USER()); + } @Test @DisplayName("뱃지 코드는 FOCUS_GENIUS여야 한다") @@ -34,8 +39,8 @@ void getBadgeCode() { @DisplayName("타이머 달성 횟수 합계가 15회 이상이면 true를 반환한다") void checkCondition_True() { // given - User user = UserFixtures.GENERAL_USER(); - given(concentrationRepository.sumTargetCountByUser(user)).willReturn(15L); + // targetCount 15인 Concentration 생성 + createConcentrationWithTargetCount(15); // when boolean result = focusGeniusBadgeChecker.checkCondition(user); @@ -48,8 +53,8 @@ void checkCondition_True() { @DisplayName("타이머 달성 횟수 합계가 15회 미만이면 false를 반환한다") void checkCondition_False() { // given - User user = UserFixtures.GENERAL_USER(); - given(concentrationRepository.sumTargetCountByUser(user)).willReturn(14L); + // targetCount 14인 Concentration 생성 + createConcentrationWithTargetCount(14); // when boolean result = focusGeniusBadgeChecker.checkCondition(user); @@ -57,4 +62,14 @@ void checkCondition_False() { // then assertThat(result).isFalse(); } + + private void createConcentrationWithTargetCount(int targetCount) { + Concentration concentration = Concentration.builder() + .user(user) + .date(LocalDate.now()) + .build(); + + concentration.addTargetCount(targetCount); + testFixtureBuilder.buildConcentration(concentration); + } } diff --git a/src/test/java/im/toduck/domain/badge/domain/checker/PerfectionistBadgeCheckerTest.java b/src/test/java/im/toduck/domain/badge/domain/checker/PerfectionistBadgeCheckerTest.java index f083dcf2..beac3869 100644 --- a/src/test/java/im/toduck/domain/badge/domain/checker/PerfectionistBadgeCheckerTest.java +++ b/src/test/java/im/toduck/domain/badge/domain/checker/PerfectionistBadgeCheckerTest.java @@ -1,28 +1,31 @@ package im.toduck.domain.badge.domain.checker; +import static im.toduck.fixtures.routine.RoutineFixtures.*; +import static im.toduck.fixtures.user.UserFixtures.*; import static org.assertj.core.api.Assertions.*; -import static org.mockito.BDDMockito.*; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import im.toduck.ServiceTest; import im.toduck.domain.badge.persistence.entity.BadgeCode; -import im.toduck.domain.routine.persistence.repository.RoutineRepository; import im.toduck.domain.user.persistence.entity.User; -import im.toduck.fixtures.user.UserFixtures; -@ExtendWith(MockitoExtension.class) -class PerfectionistBadgeCheckerTest { +@Transactional +class PerfectionistBadgeCheckerTest extends ServiceTest { - @InjectMocks + @Autowired private PerfectionistBadgeChecker perfectionistBadgeChecker; - @Mock - private RoutineRepository routineRepository; + private User user; + + @BeforeEach + void setUp() { + user = testFixtureBuilder.buildUser(GENERAL_USER()); + } @Test @DisplayName("뱃지 코드는 PERFECTIONIST여야 한다") @@ -34,8 +37,9 @@ void getBadgeCode() { @DisplayName("루틴이 10개 이상이면 true를 반환한다") void checkCondition_True() { // given - User user = UserFixtures.GENERAL_USER(); - given(routineRepository.countByUserAndDeletedAtIsNull(user)).willReturn(10L); + for (int i = 0; i < 10; i++) { + testFixtureBuilder.buildRoutineAndUpdateAuditFields(PUBLIC_MONDAY_MORNING_ROUTINE(user).build()); + } // when boolean result = perfectionistBadgeChecker.checkCondition(user); @@ -48,8 +52,9 @@ void checkCondition_True() { @DisplayName("루틴이 10개 미만이면 false를 반환한다") void checkCondition_False() { // given - User user = UserFixtures.GENERAL_USER(); - given(routineRepository.countByUserAndDeletedAtIsNull(user)).willReturn(9L); + for (int i = 0; i < 9; i++) { + testFixtureBuilder.buildRoutineAndUpdateAuditFields(PUBLIC_MONDAY_MORNING_ROUTINE(user).build()); + } // when boolean result = perfectionistBadgeChecker.checkCondition(user); diff --git a/src/test/java/im/toduck/domain/badge/domain/checker/QuackQuackBadgeCheckerTest.java b/src/test/java/im/toduck/domain/badge/domain/checker/QuackQuackBadgeCheckerTest.java index 0881e346..63f551fa 100644 --- a/src/test/java/im/toduck/domain/badge/domain/checker/QuackQuackBadgeCheckerTest.java +++ b/src/test/java/im/toduck/domain/badge/domain/checker/QuackQuackBadgeCheckerTest.java @@ -1,28 +1,31 @@ package im.toduck.domain.badge.domain.checker; +import static im.toduck.fixtures.social.SocialFixtures.*; +import static im.toduck.fixtures.user.UserFixtures.*; import static org.assertj.core.api.Assertions.*; -import static org.mockito.BDDMockito.*; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import im.toduck.ServiceTest; import im.toduck.domain.badge.persistence.entity.BadgeCode; -import im.toduck.domain.social.persistence.repository.SocialRepository; import im.toduck.domain.user.persistence.entity.User; -import im.toduck.fixtures.user.UserFixtures; -@ExtendWith(MockitoExtension.class) -class QuackQuackBadgeCheckerTest { +@Transactional +class QuackQuackBadgeCheckerTest extends ServiceTest { - @InjectMocks + @Autowired private QuackQuackBadgeChecker quackQuackBadgeChecker; - @Mock - private SocialRepository socialRepository; + private User user; + + @BeforeEach + void setUp() { + user = testFixtureBuilder.buildUser(GENERAL_USER()); + } @Test @DisplayName("뱃지 코드는 QUACK_QUACK이어야 한다") @@ -34,8 +37,9 @@ void getBadgeCode() { @DisplayName("소셜 게시글이 15개 이상이면 true를 반환한다") void checkCondition_True() { // given - User user = UserFixtures.GENERAL_USER(); - given(socialRepository.countByUserId(user.getId())).willReturn(15L); + for (int i = 0; i < 15; i++) { + testFixtureBuilder.buildSocial(SINGLE_SOCIAL(user, false)); + } // when boolean result = quackQuackBadgeChecker.checkCondition(user); @@ -48,8 +52,9 @@ void checkCondition_True() { @DisplayName("소셜 게시글이 15개 미만이면 false를 반환한다") void checkCondition_False() { // given - User user = UserFixtures.GENERAL_USER(); - given(socialRepository.countByUserId(user.getId())).willReturn(14L); + for (int i = 0; i < 14; i++) { + testFixtureBuilder.buildSocial(SINGLE_SOCIAL(user, false)); + } // when boolean result = quackQuackBadgeChecker.checkCondition(user);