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/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/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..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 @@ -11,6 +11,10 @@ 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.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; import im.toduck.domain.user.domain.service.UserService; import im.toduck.domain.user.persistence.entity.User; @@ -39,6 +43,51 @@ 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); + } + + @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)) @@ -46,7 +95,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/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/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, 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/main/java/im/toduck/domain/routine/persistence/repository/RoutineRepository.java b/src/main/java/im/toduck/domain/routine/persistence/repository/RoutineRepository.java index effb42d3..c79c145a 100644 --- a/src/main/java/im/toduck/domain/routine/persistence/repository/RoutineRepository.java +++ b/src/main/java/im/toduck/domain/routine/persistence/repository/RoutineRepository.java @@ -46,4 +46,9 @@ public interface RoutineRepository 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/builder/BuilderSupporter.java b/src/test/java/im/toduck/builder/BuilderSupporter.java index 58baebf2..75b9950e 100644 --- a/src/test/java/im/toduck/builder/BuilderSupporter.java +++ b/src/test/java/im/toduck/builder/BuilderSupporter.java @@ -5,6 +5,8 @@ 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; import im.toduck.domain.routine.persistence.repository.RoutineRecordRepository; @@ -77,12 +79,18 @@ public class BuilderSupporter { @Autowired private UserKeywordRepository userKeywordRepository; + @Autowired + private DiaryRepository diaryRepository; + @Autowired private BadgeRepository badgeRepository; @Autowired private UserBadgeRepository userBadgeRepository; + @Autowired + private ConcentrationRepository concentrationRepository; + public UserRepository userRepository() { return userRepository; } @@ -151,6 +159,10 @@ public UserKeywordRepository userKeywordRepository() { return userKeywordRepository; } + public DiaryRepository diaryRepository() { + return diaryRepository; + } + public BadgeRepository badgeRepository() { return badgeRepository; } @@ -158,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 147bac3c..b930ab40 100644 --- a/src/test/java/im/toduck/builder/TestFixtureBuilder.java +++ b/src/test/java/im/toduck/builder/TestFixtureBuilder.java @@ -9,6 +9,8 @@ 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; import im.toduck.domain.routine.persistence.entity.Routine; @@ -120,6 +122,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) @@ -148,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 new file mode 100644 index 00000000..fe1e690d --- /dev/null +++ b/src/test/java/im/toduck/domain/badge/domain/checker/CrowBadgeCheckerTest.java @@ -0,0 +1,89 @@ +package im.toduck.domain.badge.domain.checker; + +import static im.toduck.fixtures.user.UserFixtures.*; +import static org.assertj.core.api.Assertions.*; + +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.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.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.global.helper.DaysOfWeekBitmask; + +@Transactional +class CrowBadgeCheckerTest extends ServiceTest { + + @Autowired + private CrowBadgeChecker crowBadgeChecker; + + private User user; + + @BeforeEach + void setUp() { + user = testFixtureBuilder.buildUser(GENERAL_USER()); + } + + @Test + @DisplayName("뱃지 코드는 CROW여야 한다") + void getBadgeCode() { + assertThat(crowBadgeChecker.getBadgeCode()).isEqualTo(BadgeCode.CROW); + } + + @Test + @DisplayName("루틴 카테고리가 5개 이상이면 true를 반환한다") + void checkCondition_True() { + // given + // 5개의 서로 다른 카테고리 루틴 생성 + createRoutineWithCategory(PlanCategory.COMPUTER); + createRoutineWithCategory(PlanCategory.FOOD); + createRoutineWithCategory(PlanCategory.PENCIL); + createRoutineWithCategory(PlanCategory.RED_BOOK); + createRoutineWithCategory(PlanCategory.YELLOW_BOOK); + + // when + boolean result = crowBadgeChecker.checkCondition(user); + + // then + assertThat(result).isTrue(); + } + + @Test + @DisplayName("루틴 카테고리가 5개 미만이면 false를 반환한다") + void checkCondition_False() { + // given + // 4개의 서로 다른 카테고리 루틴 생성 (하나는 중복) + createRoutineWithCategory(PlanCategory.COMPUTER); + createRoutineWithCategory(PlanCategory.FOOD); + createRoutineWithCategory(PlanCategory.PENCIL); + createRoutineWithCategory(PlanCategory.RED_BOOK); + createRoutineWithCategory(PlanCategory.RED_BOOK); // 중복 카테고리 + + // when + boolean result = crowBadgeChecker.checkCondition(user); + + // 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/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/domain/badge/domain/checker/FocusGeniusBadgeCheckerTest.java b/src/test/java/im/toduck/domain/badge/domain/checker/FocusGeniusBadgeCheckerTest.java new file mode 100644 index 00000000..adc46184 --- /dev/null +++ b/src/test/java/im/toduck/domain/badge/domain/checker/FocusGeniusBadgeCheckerTest.java @@ -0,0 +1,75 @@ +package im.toduck.domain.badge.domain.checker; + +import static im.toduck.fixtures.user.UserFixtures.*; +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDate; + +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.concentration.persistence.entity.Concentration; +import im.toduck.domain.user.persistence.entity.User; + +@Transactional +class FocusGeniusBadgeCheckerTest extends ServiceTest { + + @Autowired + private FocusGeniusBadgeChecker focusGeniusBadgeChecker; + + private User user; + + @BeforeEach + void setUp() { + user = testFixtureBuilder.buildUser(GENERAL_USER()); + } + + @Test + @DisplayName("뱃지 코드는 FOCUS_GENIUS여야 한다") + void getBadgeCode() { + assertThat(focusGeniusBadgeChecker.getBadgeCode()).isEqualTo(BadgeCode.FOCUS_GENIUS); + } + + @Test + @DisplayName("타이머 달성 횟수 합계가 15회 이상이면 true를 반환한다") + void checkCondition_True() { + // given + // targetCount 15인 Concentration 생성 + createConcentrationWithTargetCount(15); + + // when + boolean result = focusGeniusBadgeChecker.checkCondition(user); + + // then + assertThat(result).isTrue(); + } + + @Test + @DisplayName("타이머 달성 횟수 합계가 15회 미만이면 false를 반환한다") + void checkCondition_False() { + // given + // targetCount 14인 Concentration 생성 + createConcentrationWithTargetCount(14); + + // when + boolean result = focusGeniusBadgeChecker.checkCondition(user); + + // 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 new file mode 100644 index 00000000..beac3869 --- /dev/null +++ b/src/test/java/im/toduck/domain/badge/domain/checker/PerfectionistBadgeCheckerTest.java @@ -0,0 +1,65 @@ +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 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 PerfectionistBadgeCheckerTest extends ServiceTest { + + @Autowired + private PerfectionistBadgeChecker perfectionistBadgeChecker; + + private User user; + + @BeforeEach + void setUp() { + user = testFixtureBuilder.buildUser(GENERAL_USER()); + } + + @Test + @DisplayName("뱃지 코드는 PERFECTIONIST여야 한다") + void getBadgeCode() { + assertThat(perfectionistBadgeChecker.getBadgeCode()).isEqualTo(BadgeCode.PERFECTIONIST); + } + + @Test + @DisplayName("루틴이 10개 이상이면 true를 반환한다") + void checkCondition_True() { + // given + for (int i = 0; i < 10; i++) { + testFixtureBuilder.buildRoutineAndUpdateAuditFields(PUBLIC_MONDAY_MORNING_ROUTINE(user).build()); + } + + // when + boolean result = perfectionistBadgeChecker.checkCondition(user); + + // then + assertThat(result).isTrue(); + } + + @Test + @DisplayName("루틴이 10개 미만이면 false를 반환한다") + void checkCondition_False() { + // given + for (int i = 0; i < 9; i++) { + testFixtureBuilder.buildRoutineAndUpdateAuditFields(PUBLIC_MONDAY_MORNING_ROUTINE(user).build()); + } + + // 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..63f551fa --- /dev/null +++ b/src/test/java/im/toduck/domain/badge/domain/checker/QuackQuackBadgeCheckerTest.java @@ -0,0 +1,65 @@ +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 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 QuackQuackBadgeCheckerTest extends ServiceTest { + + @Autowired + private QuackQuackBadgeChecker quackQuackBadgeChecker; + + private User user; + + @BeforeEach + void setUp() { + user = testFixtureBuilder.buildUser(GENERAL_USER()); + } + + @Test + @DisplayName("뱃지 코드는 QUACK_QUACK이어야 한다") + void getBadgeCode() { + assertThat(quackQuackBadgeChecker.getBadgeCode()).isEqualTo(BadgeCode.QUACK_QUACK); + } + + @Test + @DisplayName("소셜 게시글이 15개 이상이면 true를 반환한다") + void checkCondition_True() { + // given + for (int i = 0; i < 15; i++) { + testFixtureBuilder.buildSocial(SINGLE_SOCIAL(user, false)); + } + + // when + boolean result = quackQuackBadgeChecker.checkCondition(user); + + // then + assertThat(result).isTrue(); + } + + @Test + @DisplayName("소셜 게시글이 15개 미만이면 false를 반환한다") + void checkCondition_False() { + // given + for (int i = 0; i < 14; i++) { + testFixtureBuilder.buildSocial(SINGLE_SOCIAL(user, false)); + } + + // when + boolean result = quackQuackBadgeChecker.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(); + } +}