From 43e5339346d290dd80fef48ea88891db25553606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EC=A0=95=EB=B9=88?= Date: Tue, 14 Apr 2026 23:42:42 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EC=9D=BC=EB=B3=84=20=EB=9E=AD?= =?UTF-8?q?=ED=82=B9=20=EC=88=9C=EC=9C=84=EA=B6=8C=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EC=97=90=EA=B2=8C=20=EC=BF=A0=ED=82=A4=20=EC=A7=80=EA=B8=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserExpHistoryJpaRepository.java | 11 ++++ .../UserExpHistoryPersistenceAdapter.java | 8 +++ .../DailyRankingRewardScheduler.java | 29 ++++++++++ .../service/DailyRankingRewardService.java | 55 +++++++++++++++++++ .../out/UserExpHistoryRepositoryPort.java | 2 + 5 files changed, 105 insertions(+) create mode 100644 src/main/java/com/process/clash/adapter/scheduler/DailyRankingRewardScheduler.java create mode 100644 src/main/java/com/process/clash/application/ranking/service/DailyRankingRewardService.java diff --git a/src/main/java/com/process/clash/adapter/persistence/user/userexphistory/UserExpHistoryJpaRepository.java b/src/main/java/com/process/clash/adapter/persistence/user/userexphistory/UserExpHistoryJpaRepository.java index 9145ae4dd..a9ea357e4 100644 --- a/src/main/java/com/process/clash/adapter/persistence/user/userexphistory/UserExpHistoryJpaRepository.java +++ b/src/main/java/com/process/clash/adapter/persistence/user/userexphistory/UserExpHistoryJpaRepository.java @@ -319,6 +319,17 @@ List findExpRankingByUserIdAndPeriod( """, nativeQuery = true) Set findUserIdsWithExpByDate(@Param("date") LocalDate date); + @Query(value = """ + SELECT fk_user_id, CAST(SUM(earn_exp) AS BIGINT) AS daily_exp + FROM user_exp_history + WHERE date = :date + AND acting_category <> 'SEASON_RESET' + GROUP BY fk_user_id + ORDER BY daily_exp DESC + LIMIT :limitCount + """, nativeQuery = true) + List findTopUserIdsByDailyExp(@Param("date") LocalDate date, @Param("limitCount") int limitCount); + @Query(value = """ SELECT u.id FROM users u diff --git a/src/main/java/com/process/clash/adapter/persistence/user/userexphistory/UserExpHistoryPersistenceAdapter.java b/src/main/java/com/process/clash/adapter/persistence/user/userexphistory/UserExpHistoryPersistenceAdapter.java index dcff13c57..1f344a586 100644 --- a/src/main/java/com/process/clash/adapter/persistence/user/userexphistory/UserExpHistoryPersistenceAdapter.java +++ b/src/main/java/com/process/clash/adapter/persistence/user/userexphistory/UserExpHistoryPersistenceAdapter.java @@ -153,4 +153,12 @@ public void saveAll(List userExpHistories) { .toList(); userExpHistoryJpaRepository.saveAll(entities); } + + @Override + public List findTopUserIdsByDailyExp(LocalDate date, int limitCount) { + return userExpHistoryJpaRepository.findTopUserIdsByDailyExp(date, limitCount) + .stream() + .map(row -> ((Number) row[0]).longValue()) + .toList(); + } } diff --git a/src/main/java/com/process/clash/adapter/scheduler/DailyRankingRewardScheduler.java b/src/main/java/com/process/clash/adapter/scheduler/DailyRankingRewardScheduler.java new file mode 100644 index 000000000..f1e8c89c2 --- /dev/null +++ b/src/main/java/com/process/clash/adapter/scheduler/DailyRankingRewardScheduler.java @@ -0,0 +1,29 @@ +package com.process.clash.adapter.scheduler; + +import com.process.clash.application.ranking.service.DailyRankingRewardService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class DailyRankingRewardScheduler { + + private final DailyRankingRewardService dailyRankingRewardService; + + /** + * 매일 KST 06:00에 전날 하루 EXP 기준 상위 3명에게 쿠키를 지급한다. + */ + @Scheduled(cron = "0 0 6 * * *", zone = "Asia/Seoul") + public void rewardDailyRanking() { + try { + log.info("일별 랭킹 보상 스케줄 실행 시작"); + dailyRankingRewardService.rewardDailyRankingWinners(); + log.info("일별 랭킹 보상 스케줄 실행 완료"); + } catch (Exception e) { + log.error("일별 랭킹 보상 스케줄 실행 실패", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/process/clash/application/ranking/service/DailyRankingRewardService.java b/src/main/java/com/process/clash/application/ranking/service/DailyRankingRewardService.java new file mode 100644 index 000000000..531e62c98 --- /dev/null +++ b/src/main/java/com/process/clash/application/ranking/service/DailyRankingRewardService.java @@ -0,0 +1,55 @@ +package com.process.clash.application.ranking.service; + +import com.process.clash.application.user.user.port.out.UserRepositoryPort; +import com.process.clash.application.user.userexphistory.port.out.UserExpHistoryRepositoryPort; +import com.process.clash.domain.user.user.entity.User; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class DailyRankingRewardService { + + private static final int[] COOKIE_REWARDS = {1000, 500, 300}; + private static final int REWARD_COUNT = 3; + + private final UserExpHistoryRepositoryPort userExpHistoryRepositoryPort; + private final UserRepositoryPort userRepositoryPort; + + /** + * 전날 하루 동안 쌓은 EXP 기준 상위 3명에게 쿠키 보상을 지급한다. + * 1위: 1000 쿠키, 2위: 500 쿠키, 3위: 300 쿠키 + */ + @Transactional + public void rewardDailyRankingWinners() { + LocalDate yesterday = LocalDate.now(ZoneId.of("Asia/Seoul")).minusDays(1); + + List topUserIds = userExpHistoryRepositoryPort.findTopUserIdsByDailyExp(yesterday, REWARD_COUNT); + + if (topUserIds.isEmpty()) { + log.info("일별 랭킹 보상 지급 대상 없음. date={}", yesterday); + return; + } + + for (int i = 0; i < topUserIds.size(); i++) { + Long userId = topUserIds.get(i); + int cookies = COOKIE_REWARDS[i]; + + User user = userRepositoryPort.findByIdForUpdate(userId).orElse(null); + if (user == null) { + log.warn("일별 랭킹 보상 지급 실패 - 유저 없음. rank={}, userId={}", i + 1, userId); + continue; + } + + userRepositoryPort.save(user.addCookie(cookies)); + log.info("일별 랭킹 보상 지급 완료. date={}, rank={}, userId={}, cookies={}", yesterday, i + 1, userId, cookies); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/process/clash/application/user/userexphistory/port/out/UserExpHistoryRepositoryPort.java b/src/main/java/com/process/clash/application/user/userexphistory/port/out/UserExpHistoryRepositoryPort.java index 386a1e746..0424beba3 100644 --- a/src/main/java/com/process/clash/application/user/userexphistory/port/out/UserExpHistoryRepositoryPort.java +++ b/src/main/java/com/process/clash/application/user/userexphistory/port/out/UserExpHistoryRepositoryPort.java @@ -37,4 +37,6 @@ public interface UserExpHistoryRepositoryPort { List findUserIdsWithoutExpOnDate(LocalDate date); void saveAll(List userExpHistories); + + List findTopUserIdsByDailyExp(LocalDate date, int limitCount); } From df0b5db6d2b5c50ac8003ae8d1f466ac8d732095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EC=A0=95=EB=B9=88?= Date: Tue, 14 Apr 2026 23:47:23 +0900 Subject: [PATCH 2/4] =?UTF-8?q?test:=20=EC=9D=BC=EB=B3=84=20=EB=9E=AD?= =?UTF-8?q?=ED=82=B9=20top=203=20=EC=9C=A0=EC=A0=80=20=EC=BF=A0=ED=82=A4?= =?UTF-8?q?=20=EC=A7=80=EA=B8=89=20=EB=B6=80=EB=B6=84=20=EB=8B=A8=EC=9C=84?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DailyRankingRewardSchedulerTest.java | 34 ++++ .../DailyRankingRewardServiceTest.java | 159 ++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 src/test/java/com/process/clash/adapter/scheduler/DailyRankingRewardSchedulerTest.java create mode 100644 src/test/java/com/process/clash/application/ranking/service/DailyRankingRewardServiceTest.java diff --git a/src/test/java/com/process/clash/adapter/scheduler/DailyRankingRewardSchedulerTest.java b/src/test/java/com/process/clash/adapter/scheduler/DailyRankingRewardSchedulerTest.java new file mode 100644 index 000000000..70f92d53d --- /dev/null +++ b/src/test/java/com/process/clash/adapter/scheduler/DailyRankingRewardSchedulerTest.java @@ -0,0 +1,34 @@ +package com.process.clash.adapter.scheduler; + +import com.process.clash.application.ranking.service.DailyRankingRewardService; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class DailyRankingRewardSchedulerTest { + + @Mock + private DailyRankingRewardService dailyRankingRewardService; + + private DailyRankingRewardScheduler scheduler; + + @BeforeEach + void setUp() { + scheduler = new DailyRankingRewardScheduler(dailyRankingRewardService); + } + + @Test + @DisplayName("스케줄러 실행 시 일별 랭킹 보상 서비스를 1회 호출한다") + void rewardDailyRanking_callsService() { + scheduler.rewardDailyRanking(); + + verify(dailyRankingRewardService, times(1)).rewardDailyRankingWinners(); + } +} \ No newline at end of file diff --git a/src/test/java/com/process/clash/application/ranking/service/DailyRankingRewardServiceTest.java b/src/test/java/com/process/clash/application/ranking/service/DailyRankingRewardServiceTest.java new file mode 100644 index 000000000..450b99e12 --- /dev/null +++ b/src/test/java/com/process/clash/application/ranking/service/DailyRankingRewardServiceTest.java @@ -0,0 +1,159 @@ +package com.process.clash.application.ranking.service; + +import com.process.clash.application.user.user.port.out.UserRepositoryPort; +import com.process.clash.application.user.userexphistory.port.out.UserExpHistoryRepositoryPort; +import com.process.clash.domain.common.enums.Major; +import com.process.clash.domain.user.user.entity.User; +import com.process.clash.domain.user.user.enums.Role; +import com.process.clash.domain.user.user.enums.UserStatus; +import com.process.clash.domain.user.userrankhistory.enums.ExpTier; +import com.process.clash.domain.user.userrankhistory.enums.RankTier; +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class DailyRankingRewardServiceTest { + + @Mock private UserExpHistoryRepositoryPort userExpHistoryRepositoryPort; + @Mock private UserRepositoryPort userRepositoryPort; + + private DailyRankingRewardService service; + + @BeforeEach + void setUp() { + service = new DailyRankingRewardService(userExpHistoryRepositoryPort, userRepositoryPort); + } + + @Test + @DisplayName("전날 EXP가 있는 유저가 없으면 쿠키를 지급하지 않는다") + void rewardDailyRankingWinners_noTopUsers_doesNotSaveAnyUser() { + when(userExpHistoryRepositoryPort.findTopUserIdsByDailyExp(any(), eq(3))) + .thenReturn(List.of()); + + service.rewardDailyRankingWinners(); + + verify(userRepositoryPort, never()).findByIdForUpdate(any()); + verify(userRepositoryPort, never()).save(any()); + } + + @Test + @DisplayName("어제 날짜로 EXP 랭킹을 조회한다") + void rewardDailyRankingWinners_queriesWithYesterdayDate() { + LocalDate yesterday = LocalDate.now(ZoneId.of("Asia/Seoul")).minusDays(1); + when(userExpHistoryRepositoryPort.findTopUserIdsByDailyExp(any(), eq(3))) + .thenReturn(List.of()); + + service.rewardDailyRankingWinners(); + + verify(userExpHistoryRepositoryPort).findTopUserIdsByDailyExp(eq(yesterday), eq(3)); + } + + @Test + @DisplayName("1위 유저에게 1000 쿠키를 지급한다") + void rewardDailyRankingWinners_rank1_receives1000Cookies() { + User rank1 = createUser(1L, 100); + when(userExpHistoryRepositoryPort.findTopUserIdsByDailyExp(any(), eq(3))) + .thenReturn(List.of(1L)); + when(userRepositoryPort.findByIdForUpdate(1L)).thenReturn(Optional.of(rank1)); + + service.rewardDailyRankingWinners(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(User.class); + verify(userRepositoryPort).save(captor.capture()); + assertThat(captor.getValue().totalCookie()).isEqualTo(100 + 1000); + } + + @Test + @DisplayName("2위 유저에게 500 쿠키를 지급한다") + void rewardDailyRankingWinners_rank2_receives500Cookies() { + User rank1 = createUser(1L, 0); + User rank2 = createUser(2L, 0); + when(userExpHistoryRepositoryPort.findTopUserIdsByDailyExp(any(), eq(3))) + .thenReturn(List.of(1L, 2L)); + when(userRepositoryPort.findByIdForUpdate(1L)).thenReturn(Optional.of(rank1)); + when(userRepositoryPort.findByIdForUpdate(2L)).thenReturn(Optional.of(rank2)); + + service.rewardDailyRankingWinners(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(User.class); + verify(userRepositoryPort, times(2)).save(captor.capture()); + assertThat(captor.getAllValues().get(1).totalCookie()).isEqualTo(500); + } + + @Test + @DisplayName("3위 유저에게 300 쿠키를 지급한다") + void rewardDailyRankingWinners_rank3_receives300Cookies() { + User rank1 = createUser(1L, 0); + User rank2 = createUser(2L, 0); + User rank3 = createUser(3L, 0); + when(userExpHistoryRepositoryPort.findTopUserIdsByDailyExp(any(), eq(3))) + .thenReturn(List.of(1L, 2L, 3L)); + when(userRepositoryPort.findByIdForUpdate(1L)).thenReturn(Optional.of(rank1)); + when(userRepositoryPort.findByIdForUpdate(2L)).thenReturn(Optional.of(rank2)); + when(userRepositoryPort.findByIdForUpdate(3L)).thenReturn(Optional.of(rank3)); + + service.rewardDailyRankingWinners(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(User.class); + verify(userRepositoryPort, times(3)).save(captor.capture()); + assertThat(captor.getAllValues().get(2).totalCookie()).isEqualTo(300); + } + + @Test + @DisplayName("상위 유저가 3명 미만이면 존재하는 유저에게만 쿠키를 지급한다") + void rewardDailyRankingWinners_fewerThan3Users_rewardsAllExisting() { + User rank1 = createUser(1L, 0); + when(userExpHistoryRepositoryPort.findTopUserIdsByDailyExp(any(), eq(3))) + .thenReturn(List.of(1L)); + when(userRepositoryPort.findByIdForUpdate(1L)).thenReturn(Optional.of(rank1)); + + service.rewardDailyRankingWinners(); + + verify(userRepositoryPort, times(1)).save(any()); + } + + @Test + @DisplayName("유저를 찾을 수 없으면 해당 순위를 스킵하고 다음 순위에 보상을 지급한다") + void rewardDailyRankingWinners_userNotFound_skipsAndContinues() { + User rank2 = createUser(2L, 0); + when(userExpHistoryRepositoryPort.findTopUserIdsByDailyExp(any(), eq(3))) + .thenReturn(List.of(1L, 2L)); + when(userRepositoryPort.findByIdForUpdate(1L)).thenReturn(Optional.empty()); + when(userRepositoryPort.findByIdForUpdate(2L)).thenReturn(Optional.of(rank2)); + + service.rewardDailyRankingWinners(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(User.class); + verify(userRepositoryPort, times(1)).save(captor.capture()); + assertThat(captor.getValue().totalCookie()).isEqualTo(500); + } + + // ===== helpers ===== + + private User createUser(Long id, int totalCookie) { + return new User( + id, Instant.now(), Instant.now(), + "user" + id, "user" + id + "@test.com", "User" + id, + "pw", Role.USER, "", 0, totalCookie, + Major.NONE, UserStatus.ACTIVE, null, false, RankTier.NONE, + ExpTier.UNRANKED, + 0 + ); + } +} \ No newline at end of file From fa71a3fcd7df2cf2094e0d143a737cc79dbfed3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EC=A0=95=EB=B9=88?= Date: Wed, 15 Apr 2026 00:34:24 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=EC=9D=BC=EB=B3=84=20=EB=9E=AD?= =?UTF-8?q?=ED=82=B9=20=EB=B3=B4=EC=83=81=20=EC=BF=BC=EB=A6=AC=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B9=84=ED=99=9C=EC=84=B1=C2=B7=ED=83=88=ED=87=B4?= =?UTF-8?q?=20=EC=9C=A0=EC=A0=80=20=EC=A0=9C=EC=99=B8=20=EB=B0=8F=20?= =?UTF-8?q?=EB=B0=B0=EC=97=B4=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EB=B0=A9?= =?UTF-8?q?=EC=96=B4=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../userexphistory/UserExpHistoryJpaRepository.java | 13 ++++++++----- .../ranking/service/DailyRankingRewardService.java | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/process/clash/adapter/persistence/user/userexphistory/UserExpHistoryJpaRepository.java b/src/main/java/com/process/clash/adapter/persistence/user/userexphistory/UserExpHistoryJpaRepository.java index a9ea357e4..e951c36ed 100644 --- a/src/main/java/com/process/clash/adapter/persistence/user/userexphistory/UserExpHistoryJpaRepository.java +++ b/src/main/java/com/process/clash/adapter/persistence/user/userexphistory/UserExpHistoryJpaRepository.java @@ -320,11 +320,14 @@ List findExpRankingByUserIdAndPeriod( Set findUserIdsWithExpByDate(@Param("date") LocalDate date); @Query(value = """ - SELECT fk_user_id, CAST(SUM(earn_exp) AS BIGINT) AS daily_exp - FROM user_exp_history - WHERE date = :date - AND acting_category <> 'SEASON_RESET' - GROUP BY fk_user_id + SELECT ueh.fk_user_id, CAST(SUM(ueh.earn_exp) AS BIGINT) AS daily_exp + FROM user_exp_history ueh + JOIN users u ON ueh.fk_user_id = u.id + WHERE ueh.date = :date + AND ueh.acting_category <> 'SEASON_RESET' + AND u.user_status = 'ACTIVE' + AND u.deleted_at IS NULL + GROUP BY ueh.fk_user_id ORDER BY daily_exp DESC LIMIT :limitCount """, nativeQuery = true) diff --git a/src/main/java/com/process/clash/application/ranking/service/DailyRankingRewardService.java b/src/main/java/com/process/clash/application/ranking/service/DailyRankingRewardService.java index 531e62c98..293048da0 100644 --- a/src/main/java/com/process/clash/application/ranking/service/DailyRankingRewardService.java +++ b/src/main/java/com/process/clash/application/ranking/service/DailyRankingRewardService.java @@ -38,7 +38,7 @@ public void rewardDailyRankingWinners() { return; } - for (int i = 0; i < topUserIds.size(); i++) { + for (int i = 0; i < Math.min(topUserIds.size(), COOKIE_REWARDS.length); i++) { Long userId = topUserIds.get(i); int cookies = COOKIE_REWARDS[i]; From efd305bfedae84eac440fed04dfcecac6fbe82ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EC=A0=95=EB=B9=88?= Date: Wed, 15 Apr 2026 09:50:30 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=EC=BF=A0=ED=82=A4=20=EC=A7=80?= =?UTF-8?q?=EA=B8=89=20=EA=B8=B0=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Conflicts: # src/main/java/com/process/clash/domain/common/enums/GoodsActingCategory.java --- .../service/DailyRankingRewardService.java | 5 ++ .../common/enums/GoodsActingCategory.java | 3 +- .../DailyRankingRewardServiceTest.java | 48 ++++++++++++++++++- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/process/clash/application/ranking/service/DailyRankingRewardService.java b/src/main/java/com/process/clash/application/ranking/service/DailyRankingRewardService.java index 293048da0..b8463039c 100644 --- a/src/main/java/com/process/clash/application/ranking/service/DailyRankingRewardService.java +++ b/src/main/java/com/process/clash/application/ranking/service/DailyRankingRewardService.java @@ -2,7 +2,10 @@ import com.process.clash.application.user.user.port.out.UserRepositoryPort; import com.process.clash.application.user.userexphistory.port.out.UserExpHistoryRepositoryPort; +import com.process.clash.application.user.usergoodshistory.port.out.UserGoodsHistoryRepositoryPort; +import com.process.clash.domain.common.enums.GoodsActingCategory; import com.process.clash.domain.user.user.entity.User; +import com.process.clash.domain.user.usergoodshistory.entity.UserGoodsHistory; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -22,6 +25,7 @@ public class DailyRankingRewardService { private final UserExpHistoryRepositoryPort userExpHistoryRepositoryPort; private final UserRepositoryPort userRepositoryPort; + private final UserGoodsHistoryRepositoryPort userGoodsHistoryRepositoryPort; /** * 전날 하루 동안 쌓은 EXP 기준 상위 3명에게 쿠키 보상을 지급한다. @@ -49,6 +53,7 @@ public void rewardDailyRankingWinners() { } userRepositoryPort.save(user.addCookie(cookies)); + userGoodsHistoryRepositoryPort.save(new UserGoodsHistory(null, null, GoodsActingCategory.DAILY_RANKING_REWARD, cookies, null, userId)); log.info("일별 랭킹 보상 지급 완료. date={}, rank={}, userId={}, cookies={}", yesterday, i + 1, userId, cookies); } } diff --git a/src/main/java/com/process/clash/domain/common/enums/GoodsActingCategory.java b/src/main/java/com/process/clash/domain/common/enums/GoodsActingCategory.java index ae020ebed..da3b04387 100644 --- a/src/main/java/com/process/clash/domain/common/enums/GoodsActingCategory.java +++ b/src/main/java/com/process/clash/domain/common/enums/GoodsActingCategory.java @@ -9,5 +9,6 @@ public enum GoodsActingCategory { BATTLE_LOSE_REWARD, BATTLE_DRAW_REWARD, ATTENDANCE_DAILY_REWARD, - ATTENDANCE_WEEKLY_REWARD + ATTENDANCE_WEEKLY_REWARD, + DAILY_RANKING_REWARD } diff --git a/src/test/java/com/process/clash/application/ranking/service/DailyRankingRewardServiceTest.java b/src/test/java/com/process/clash/application/ranking/service/DailyRankingRewardServiceTest.java index 450b99e12..09d1d75b3 100644 --- a/src/test/java/com/process/clash/application/ranking/service/DailyRankingRewardServiceTest.java +++ b/src/test/java/com/process/clash/application/ranking/service/DailyRankingRewardServiceTest.java @@ -2,6 +2,9 @@ import com.process.clash.application.user.user.port.out.UserRepositoryPort; import com.process.clash.application.user.userexphistory.port.out.UserExpHistoryRepositoryPort; +import com.process.clash.application.user.usergoodshistory.port.out.UserGoodsHistoryRepositoryPort; +import com.process.clash.domain.common.enums.GoodsActingCategory; +import com.process.clash.domain.user.usergoodshistory.entity.UserGoodsHistory; import com.process.clash.domain.common.enums.Major; import com.process.clash.domain.user.user.entity.User; import com.process.clash.domain.user.user.enums.Role; @@ -32,12 +35,13 @@ class DailyRankingRewardServiceTest { @Mock private UserExpHistoryRepositoryPort userExpHistoryRepositoryPort; @Mock private UserRepositoryPort userRepositoryPort; + @Mock private UserGoodsHistoryRepositoryPort userGoodsHistoryRepositoryPort; private DailyRankingRewardService service; @BeforeEach void setUp() { - service = new DailyRankingRewardService(userExpHistoryRepositoryPort, userRepositoryPort); + service = new DailyRankingRewardService(userExpHistoryRepositoryPort, userRepositoryPort, userGoodsHistoryRepositoryPort); } @Test @@ -50,6 +54,7 @@ void rewardDailyRankingWinners_noTopUsers_doesNotSaveAnyUser() { verify(userRepositoryPort, never()).findByIdForUpdate(any()); verify(userRepositoryPort, never()).save(any()); + verify(userGoodsHistoryRepositoryPort, never()).save(any()); } @Test @@ -144,6 +149,47 @@ void rewardDailyRankingWinners_userNotFound_skipsAndContinues() { assertThat(captor.getValue().totalCookie()).isEqualTo(500); } + @Test + @DisplayName("쿠키 지급 시 UserGoodsHistory에 DAILY_RANKING_REWARD 카테고리로 기록을 저장한다") + void rewardDailyRankingWinners_savesGoodsHistory() { + User rank1 = createUser(1L, 0); + when(userExpHistoryRepositoryPort.findTopUserIdsByDailyExp(any(), eq(3))) + .thenReturn(List.of(1L)); + when(userRepositoryPort.findByIdForUpdate(1L)).thenReturn(Optional.of(rank1)); + + service.rewardDailyRankingWinners(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserGoodsHistory.class); + verify(userGoodsHistoryRepositoryPort, times(1)).save(captor.capture()); + UserGoodsHistory history = captor.getValue(); + assertThat(history.goodsActingCategory()).isEqualTo(GoodsActingCategory.DAILY_RANKING_REWARD); + assertThat(history.variation()).isEqualTo(1000); + assertThat(history.userId()).isEqualTo(1L); + assertThat(history.productId()).isNull(); + } + + @Test + @DisplayName("3명 모두 쿠키를 받으면 UserGoodsHistory 기록도 3건 저장된다") + void rewardDailyRankingWinners_savesGoodsHistoryForEachWinner() { + User rank1 = createUser(1L, 0); + User rank2 = createUser(2L, 0); + User rank3 = createUser(3L, 0); + when(userExpHistoryRepositoryPort.findTopUserIdsByDailyExp(any(), eq(3))) + .thenReturn(List.of(1L, 2L, 3L)); + when(userRepositoryPort.findByIdForUpdate(1L)).thenReturn(Optional.of(rank1)); + when(userRepositoryPort.findByIdForUpdate(2L)).thenReturn(Optional.of(rank2)); + when(userRepositoryPort.findByIdForUpdate(3L)).thenReturn(Optional.of(rank3)); + + service.rewardDailyRankingWinners(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserGoodsHistory.class); + verify(userGoodsHistoryRepositoryPort, times(3)).save(captor.capture()); + List histories = captor.getAllValues(); + assertThat(histories.get(0).variation()).isEqualTo(1000); + assertThat(histories.get(1).variation()).isEqualTo(500); + assertThat(histories.get(2).variation()).isEqualTo(300); + } + // ===== helpers ===== private User createUser(Long id, int totalCookie) {