From e7795e31332bd272382ee042daeb9d134f2bc423 Mon Sep 17 00:00:00 2001 From: chaeyn Date: Sat, 2 May 2026 20:09:37 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=9E=AD=ED=82=B9=20=ED=8B=B0=EC=96=B4?= =?UTF-8?q?=20=EA=B8=B0=EC=A4=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RankingTierUpdateService.java | 8 +-- .../user/userrankhistory/enums/ExpTier.java | 6 +- .../V40__update_ranking_tier_thresholds.sql | 29 ++++++++ .../service/RankingTierUpdateServiceTest.java | 68 +++++++++---------- .../userrankhistory/enums/ExpTierTest.java | 12 ++-- 5 files changed, 76 insertions(+), 47 deletions(-) create mode 100644 src/main/resources/db/migration/V40__update_ranking_tier_thresholds.sql diff --git a/src/main/java/com/process/clash/application/ranking/service/RankingTierUpdateService.java b/src/main/java/com/process/clash/application/ranking/service/RankingTierUpdateService.java index 61a2b88a1..0674ba1a1 100644 --- a/src/main/java/com/process/clash/application/ranking/service/RankingTierUpdateService.java +++ b/src/main/java/com/process/clash/application/ranking/service/RankingTierUpdateService.java @@ -22,8 +22,8 @@ @Slf4j public class RankingTierUpdateService { - private static final int AURA_EXP_THRESHOLD = 200_000; - private static final int MASTER_EXP_THRESHOLD = 150_000; + private static final int AURA_EXP_THRESHOLD = 150_000; + private static final int MASTER_EXP_THRESHOLD = 100_000; private final UserRepositoryPort userRepositoryPort; private final UserRankHistoryRepositoryPort userRankHistoryRepositoryPort; @@ -31,8 +31,8 @@ public class RankingTierUpdateService { /** * 랭킹 기반 티어(Master, Aura) 업데이트 - * - Aura : 1위 && exp >= 200,000 - * - Master: 2~3위 && exp >= 150,000 (상위 3등 2명) + * - Aura : 1위 && exp >= 150,000 + * - Master: 1~3위 && exp >= 100,000 * - 그 외 : NONE */ @Transactional diff --git a/src/main/java/com/process/clash/domain/user/userrankhistory/enums/ExpTier.java b/src/main/java/com/process/clash/domain/user/userrankhistory/enums/ExpTier.java index 09cfaba41..d28fcafd2 100644 --- a/src/main/java/com/process/clash/domain/user/userrankhistory/enums/ExpTier.java +++ b/src/main/java/com/process/clash/domain/user/userrankhistory/enums/ExpTier.java @@ -4,11 +4,11 @@ public enum ExpTier { UNRANKED, // 0 ~ 999 BRONZE, // 1,000 ~ 9,999 SILVER, // 10,000 ~ 29,999 - GOLD, // 30,000 ~ 99,999 - DIAMOND; // 100,000+ + GOLD, // 30,000 ~ 49,999 + DIAMOND; // 50,000+ public static ExpTier fromExp(int exp) { - if (exp >= 100_000) return DIAMOND; + if (exp >= 50_000) return DIAMOND; if (exp >= 30_000) return GOLD; if (exp >= 10_000) return SILVER; if (exp >= 1_000) return BRONZE; diff --git a/src/main/resources/db/migration/V40__update_ranking_tier_thresholds.sql b/src/main/resources/db/migration/V40__update_ranking_tier_thresholds.sql new file mode 100644 index 000000000..9ecd9932d --- /dev/null +++ b/src/main/resources/db/migration/V40__update_ranking_tier_thresholds.sql @@ -0,0 +1,29 @@ +UPDATE users +SET current_exp_tier = CASE + WHEN total_exp >= 50000 THEN 'DIAMOND' + WHEN total_exp >= 30000 THEN 'GOLD' + WHEN total_exp >= 10000 THEN 'SILVER' + WHEN total_exp >= 1000 THEN 'BRONZE' + ELSE 'UNRANKED' +END; + +WITH ranked_users AS ( + SELECT + id, + total_exp, + ROW_NUMBER() OVER (ORDER BY total_exp DESC) AS rank + FROM users + WHERE user_status = 'ACTIVE' +) +UPDATE users u +SET current_rank_tier = CASE + WHEN ru.rank = 1 AND ru.total_exp >= 150000 THEN 'AURA' + WHEN ru.rank <= 3 AND ru.total_exp >= 100000 THEN 'MASTER' + ELSE 'NONE' +END +FROM ranked_users ru +WHERE u.id = ru.id; + +UPDATE users +SET current_rank_tier = 'NONE' +WHERE user_status <> 'ACTIVE'; diff --git a/src/test/java/com/process/clash/application/ranking/service/RankingTierUpdateServiceTest.java b/src/test/java/com/process/clash/application/ranking/service/RankingTierUpdateServiceTest.java index ec27a2f3b..b3cae0bf1 100644 --- a/src/test/java/com/process/clash/application/ranking/service/RankingTierUpdateServiceTest.java +++ b/src/test/java/com/process/clash/application/ranking/service/RankingTierUpdateServiceTest.java @@ -60,9 +60,9 @@ void updateRankTiers_noUsers_doesNothing() { } @Test - @DisplayName("1위이고 exp >= 200000이면 AURA를 부여한다") - void computeRankTier_rank1_expOver200k_returnsAura() { - User user = createUser(1L, 200_000, RankTier.NONE); + @DisplayName("1위이고 exp >= 150000이면 AURA를 부여한다") + void computeRankTier_rank1_expOver150k_returnsAura() { + User user = createUser(1L, 150_000, RankTier.NONE); when(userRepositoryPort.findAllOrderByTotalExpDesc()).thenReturn(List.of(user)); when(seasonRepositoryPort.findCurrentSeason()).thenReturn(Optional.empty()); @@ -74,10 +74,10 @@ void computeRankTier_rank1_expOver200k_returnsAura() { } @Test - @DisplayName("2위이고 exp >= 150000이면 MASTER를 부여한다") - void computeRankTier_rank2_expOver150k_returnsMaster() { - User rank1 = createUser(1L, 250_000, RankTier.NONE); - User rank2 = createUser(2L, 150_000, RankTier.NONE); + @DisplayName("2위이고 exp >= 100000이면 MASTER를 부여한다") + void computeRankTier_rank2_expOver100k_returnsMaster() { + User rank1 = createUser(1L, 150_000, RankTier.NONE); + User rank2 = createUser(2L, 100_000, RankTier.NONE); when(userRepositoryPort.findAllOrderByTotalExpDesc()).thenReturn(List.of(rank1, rank2)); when(seasonRepositoryPort.findCurrentSeason()).thenReturn(Optional.empty()); @@ -91,11 +91,11 @@ void computeRankTier_rank2_expOver150k_returnsMaster() { } @Test - @DisplayName("3위이고 exp >= 150000이면 MASTER를 부여한다") - void computeRankTier_rank3_expOver150k_returnsMaster() { - User rank1 = createUser(1L, 250_000, RankTier.NONE); - User rank2 = createUser(2L, 200_000, RankTier.NONE); - User rank3 = createUser(3L, 150_000, RankTier.NONE); + @DisplayName("3위이고 exp >= 100000이면 MASTER를 부여한다") + void computeRankTier_rank3_expOver100k_returnsMaster() { + User rank1 = createUser(1L, 150_000, RankTier.NONE); + User rank2 = createUser(2L, 120_000, RankTier.NONE); + User rank3 = createUser(3L, 100_000, RankTier.NONE); when(userRepositoryPort.findAllOrderByTotalExpDesc()).thenReturn(List.of(rank1, rank2, rank3)); when(seasonRepositoryPort.findCurrentSeason()).thenReturn(Optional.empty()); @@ -110,10 +110,10 @@ void computeRankTier_rank3_expOver150k_returnsMaster() { @Test @DisplayName("4위이면 exp에 관계없이 NONE을 부여한다") void computeRankTier_rank4_returnsNone() { - User rank1 = createUser(1L, 200_000, RankTier.AURA); - User rank2 = createUser(2L, 180_000, RankTier.MASTER); - User rank3 = createUser(3L, 150_000, RankTier.MASTER); - User rank4 = createUser(4L, 140_000, RankTier.NONE); + User rank1 = createUser(1L, 150_000, RankTier.AURA); + User rank2 = createUser(2L, 120_000, RankTier.MASTER); + User rank3 = createUser(3L, 100_000, RankTier.MASTER); + User rank4 = createUser(4L, 100_000, RankTier.NONE); when(userRepositoryPort.findAllOrderByTotalExpDesc()).thenReturn(List.of(rank1, rank2, rank3, rank4)); when(seasonRepositoryPort.findCurrentSeason()).thenReturn(Optional.empty()); @@ -123,9 +123,9 @@ void computeRankTier_rank4_returnsNone() { } @Test - @DisplayName("1위이지만 exp < 150000이면 NONE을 부여한다") - void computeRankTier_rank1_expUnder150k_returnsNone() { - User user = createUser(1L, 149_999, RankTier.AURA); + @DisplayName("1위이지만 exp < 100000이면 NONE을 부여한다") + void computeRankTier_rank1_expUnder100k_returnsNone() { + User user = createUser(1L, 99_999, RankTier.AURA); when(userRepositoryPort.findAllOrderByTotalExpDesc()).thenReturn(List.of(user)); when(seasonRepositoryPort.findCurrentSeason()).thenReturn(Optional.empty()); @@ -137,9 +137,9 @@ void computeRankTier_rank1_expUnder150k_returnsNone() { } @Test - @DisplayName("1위이고 exp가 150000~199999이면 MASTER를 부여한다") - void computeRankTier_rank1_expBetween150kAnd200k_returnsMaster() { - User user = createUser(1L, 199_999, RankTier.NONE); + @DisplayName("1위이고 exp가 100000~149999이면 MASTER를 부여한다") + void computeRankTier_rank1_expBetween100kAnd150k_returnsMaster() { + User user = createUser(1L, 149_999, RankTier.NONE); when(userRepositoryPort.findAllOrderByTotalExpDesc()).thenReturn(List.of(user)); when(seasonRepositoryPort.findCurrentSeason()).thenReturn(Optional.empty()); @@ -151,10 +151,10 @@ void computeRankTier_rank1_expBetween150kAnd200k_returnsMaster() { } @Test - @DisplayName("2위이지만 exp < 150000이면 NONE을 부여한다") - void computeRankTier_rank2_expUnder150k_returnsNone() { - User rank1 = createUser(1L, 250_000, RankTier.NONE); - User rank2 = createUser(2L, 149_999, RankTier.NONE); + @DisplayName("2위이지만 exp < 100000이면 NONE을 부여한다") + void computeRankTier_rank2_expUnder100k_returnsNone() { + User rank1 = createUser(1L, 150_000, RankTier.NONE); + User rank2 = createUser(2L, 99_999, RankTier.NONE); when(userRepositoryPort.findAllOrderByTotalExpDesc()).thenReturn(List.of(rank1, rank2)); when(seasonRepositoryPort.findCurrentSeason()).thenReturn(Optional.empty()); @@ -169,7 +169,7 @@ void computeRankTier_rank2_expUnder150k_returnsNone() { @Test @DisplayName("티어가 변경되지 않은 유저는 saveAll을 호출하지 않는다") void updateRankTiers_unchangedTier_doesNotSaveUser() { - User user = createUser(1L, 200_000, RankTier.AURA); + User user = createUser(1L, 150_000, RankTier.AURA); when(userRepositoryPort.findAllOrderByTotalExpDesc()).thenReturn(List.of(user)); when(seasonRepositoryPort.findCurrentSeason()).thenReturn(Optional.empty()); @@ -183,8 +183,8 @@ void updateRankTiers_unchangedTier_doesNotSaveUser() { @Test @DisplayName("활성 시즌이 있을 때 모든 유저의 RankHistory를 저장한다") void updateRankTiers_withActiveSeason_savesAllHistories() { - User rank1 = createUser(1L, 200_000, RankTier.NONE); - User rank2 = createUser(2L, 150_000, RankTier.NONE); + User rank1 = createUser(1L, 150_000, RankTier.NONE); + User rank2 = createUser(2L, 100_000, RankTier.NONE); when(userRepositoryPort.findAllOrderByTotalExpDesc()).thenReturn(List.of(rank1, rank2)); when(seasonRepositoryPort.findCurrentSeason()).thenReturn(Optional.of(createSeason(10L))); @@ -198,7 +198,7 @@ void updateRankTiers_withActiveSeason_savesAllHistories() { @Test @DisplayName("활성 시즌이 없을 때 RankHistory를 저장하지 않는다") void updateRankTiers_withoutActiveSeason_doesNotSaveHistory() { - User user = createUser(1L, 200_000, RankTier.NONE); + User user = createUser(1L, 150_000, RankTier.NONE); when(userRepositoryPort.findAllOrderByTotalExpDesc()).thenReturn(List.of(user)); when(seasonRepositoryPort.findCurrentSeason()).thenReturn(Optional.empty()); @@ -210,7 +210,7 @@ void updateRankTiers_withoutActiveSeason_doesNotSaveHistory() { @Test @DisplayName("저장되는 RankHistory의 날짜는 오늘이다") void updateRankTiers_historyDate_isToday() { - User user = createUser(1L, 200_000, RankTier.NONE); + User user = createUser(1L, 150_000, RankTier.NONE); when(userRepositoryPort.findAllOrderByTotalExpDesc()).thenReturn(List.of(user)); when(seasonRepositoryPort.findCurrentSeason()).thenReturn(Optional.of(createSeason(10L))); @@ -238,8 +238,8 @@ void updateRankTiers_expTier_isComputedFromTotalExp() { @Test @DisplayName("RankHistory에 순위와 rankTier가 올바르게 저장된다") void updateRankTiers_historyContainsCorrectRankAndTier() { - User rank1 = createUser(1L, 200_000, RankTier.NONE); - User rank2 = createUser(2L, 150_000, RankTier.NONE); + User rank1 = createUser(1L, 150_000, RankTier.NONE); + User rank2 = createUser(2L, 100_000, RankTier.NONE); when(userRepositoryPort.findAllOrderByTotalExpDesc()).thenReturn(List.of(rank1, rank2)); when(seasonRepositoryPort.findCurrentSeason()).thenReturn(Optional.of(createSeason(10L))); @@ -258,7 +258,7 @@ void updateRankTiers_historyContainsCorrectRankAndTier() { @Test @DisplayName("RankHistory의 seasonId는 현재 시즌 id로 저장된다") void updateRankTiers_historySeasonId_matchesCurrentSeason() { - User user = createUser(1L, 200_000, RankTier.NONE); + User user = createUser(1L, 150_000, RankTier.NONE); Season season = createSeason(42L); when(userRepositoryPort.findAllOrderByTotalExpDesc()).thenReturn(List.of(user)); when(seasonRepositoryPort.findCurrentSeason()).thenReturn(Optional.of(season)); diff --git a/src/test/java/com/process/clash/domain/user/userrankhistory/enums/ExpTierTest.java b/src/test/java/com/process/clash/domain/user/userrankhistory/enums/ExpTierTest.java index c24a0f204..89b93ddf1 100644 --- a/src/test/java/com/process/clash/domain/user/userrankhistory/enums/ExpTierTest.java +++ b/src/test/java/com/process/clash/domain/user/userrankhistory/enums/ExpTierTest.java @@ -8,14 +8,14 @@ class ExpTierTest { @Test - @DisplayName("exp가 100000 이상이면 DIAMOND다") - void fromExp_overOrEqual100k_returnsDiamond() { - assertThat(ExpTier.fromExp(100_000)).isEqualTo(ExpTier.DIAMOND); + @DisplayName("exp가 50000 이상이면 DIAMOND다") + void fromExp_overOrEqual50k_returnsDiamond() { + assertThat(ExpTier.fromExp(50_000)).isEqualTo(ExpTier.DIAMOND); } @Test - @DisplayName("exp가 99999 이하이면 GOLD다") - void fromExp_under100k_returnsGold() { - assertThat(ExpTier.fromExp(99_999)).isEqualTo(ExpTier.GOLD); + @DisplayName("exp가 49999 이하이면 GOLD다") + void fromExp_under50k_returnsGold() { + assertThat(ExpTier.fromExp(49_999)).isEqualTo(ExpTier.GOLD); } }