Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,17 @@
@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;
private final SeasonRepositoryPort seasonRepositoryPort;

/**
* 랭킹 기반 티어(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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

ROW_NUMBER()를 사용할 때 ORDER BY total_exp DESC만 지정하면, 경험치가 동일한 유저들 사이의 순위가 비결정적(non-deterministic)으로 정해질 수 있습니다. Java 서비스의 정렬 로직과 일관성을 유지하고 안정적인 결과를 보장하기 위해 id와 같은 고유한 컬럼을 추가 정렬 기준으로 사용하는 것을 권장합니다.

        ROW_NUMBER() OVER (ORDER BY total_exp DESC, id ASC) 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';
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand All @@ -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());

Expand All @@ -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());

Expand All @@ -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());

Expand All @@ -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());

Expand All @@ -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());

Expand All @@ -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());

Expand All @@ -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());

Expand All @@ -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)));

Expand All @@ -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());

Expand All @@ -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)));

Expand Down Expand Up @@ -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)));

Expand All @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading