feat(content-ranking): 랭킹 점수 정책을 추가한다

This commit is contained in:
2026-06-24 12:37:55 +09:00
parent dc93f9845b
commit e4706d6699
2 changed files with 302 additions and 0 deletions
@@ -0,0 +1,171 @@
package kr.co.vividnext.sodalive.v2.content.ranking.domain
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import java.time.LocalDateTime
class AudioRankingScorePolicyTest {
private val policy = AudioRankingScorePolicy()
private val aggregationEndAt = LocalDateTime.of(2026, 6, 22, 0, 0)
@Test
@DisplayName("유료 주간 인기 원점수는 매출 45%, 판매량 35%, 좋아요 10%, 댓글 10%로 계산한다")
fun shouldCalculatePaidWeeklyPopularRawScore() {
assertEquals(0.45, AudioRankingScorePolicy.WEEKLY_PAID_REVENUE_WEIGHT, 0.0001)
assertEquals(0.35, AudioRankingScorePolicy.WEEKLY_PAID_SALES_COUNT_WEIGHT, 0.0001)
assertEquals(0.1, AudioRankingScorePolicy.WEEKLY_PAID_LIKE_COUNT_WEIGHT, 0.0001)
assertEquals(0.1, AudioRankingScorePolicy.WEEKLY_PAID_COMMENT_COUNT_WEIGHT, 0.0001)
val score = policy.calculateWeeklyPopularScore(
revenue = 1000,
salesCount = 100,
viewCount = 0,
likeCount = 30,
commentCount = 20,
isPaid = true
)
assertEquals(490.0, score, 0.0001)
}
@Test
@DisplayName("무료 주간 인기 원점수는 조회수 50%, 좋아요 25%, 댓글 25%로 계산한다")
fun shouldCalculateFreeWeeklyPopularRawScore() {
assertEquals(0.5, AudioRankingScorePolicy.WEEKLY_FREE_VIEW_COUNT_WEIGHT, 0.0001)
assertEquals(0.25, AudioRankingScorePolicy.WEEKLY_FREE_LIKE_COUNT_WEIGHT, 0.0001)
assertEquals(0.25, AudioRankingScorePolicy.WEEKLY_FREE_COMMENT_COUNT_WEIGHT, 0.0001)
val score = policy.calculateWeeklyPopularScore(
revenue = 0,
salesCount = 0,
viewCount = 200,
likeCount = 20,
commentCount = 8,
isPaid = false
)
assertEquals(107.0, score, 0.0001)
}
@Test
@DisplayName("정규화 점수는 그룹 최고 점수 기준 0~100으로 계산하고 최고 점수가 0 이하면 0으로 처리한다")
fun shouldNormalizeScoreToZeroToOneHundred() {
assertEquals(100.0, policy.normalizeScore(currentScore = 80.0, maxScore = 80.0), 0.0001)
assertEquals(25.0, policy.normalizeScore(currentScore = 20.0, maxScore = 80.0), 0.0001)
assertEquals(0.0, policy.normalizeScore(currentScore = 20.0, maxScore = 0.0), 0.0001)
assertEquals(0.0, policy.normalizeScore(currentScore = -20.0, maxScore = -10.0), 0.0001)
}
@Test
@DisplayName("유료 지금 뜨는 중 점수는 판매/조회 콘텐츠 성장 점수와 좋아요/댓글 증가율 및 신규 부스트로 계산한다")
fun shouldCalculatePaidRisingScore() {
assertEquals(0.5, AudioRankingScorePolicy.RISING_CONTENT_GROWTH_SCORE_WEIGHT, 0.0001)
assertEquals(0.25, AudioRankingScorePolicy.RISING_LIKE_GROWTH_WEIGHT, 0.0001)
assertEquals(0.25, AudioRankingScorePolicy.RISING_COMMENT_GROWTH_WEIGHT, 0.0001)
assertEquals(0.6, AudioRankingScorePolicy.RISING_PAID_SALES_GROWTH_WEIGHT, 0.0001)
assertEquals(0.4, AudioRankingScorePolicy.RISING_PAID_VIEW_GROWTH_WEIGHT, 0.0001)
val score = policy.calculateRisingScore(
recentSalesCount = 9,
previousSalesCount = 3,
recentViewCount = 30,
previousViewCount = 10,
recentLikeCount = 8,
previousLikeCount = 4,
recentCommentCount = 6,
previousCommentCount = 3,
releaseDate = aggregationEndAt.minusDays(2),
aggregationEndAt = aggregationEndAt,
isPaid = true
)
assertEquals(2.25, score, 0.0001)
}
@Test
@DisplayName("무료 지금 뜨는 중 점수는 조회/좋아요/댓글 콘텐츠 성장 점수와 좋아요/댓글 증가율 및 신규 부스트로 계산한다")
fun shouldCalculateFreeRisingScore() {
assertEquals(0.5, AudioRankingScorePolicy.RISING_FREE_VIEW_GROWTH_WEIGHT, 0.0001)
assertEquals(0.25, AudioRankingScorePolicy.RISING_FREE_LIKE_GROWTH_WEIGHT, 0.0001)
assertEquals(0.25, AudioRankingScorePolicy.RISING_FREE_COMMENT_GROWTH_WEIGHT, 0.0001)
val score = policy.calculateRisingScore(
recentSalesCount = 0,
previousSalesCount = 0,
recentViewCount = 30,
previousViewCount = 10,
recentLikeCount = 8,
previousLikeCount = 4,
recentCommentCount = 6,
previousCommentCount = 3,
releaseDate = aggregationEndAt.minusDays(8),
aggregationEndAt = aggregationEndAt,
isPaid = false
)
assertEquals(1.4375, score, 0.0001)
}
@Test
@DisplayName("최소 기준 미만 지표만 증가율 반영값을 0으로 처리한다")
fun shouldApplyMinimumThresholdPerMetric() {
assertEquals(10, AudioRankingScorePolicy.RISING_VIEW_COUNT_THRESHOLD)
assertEquals(3, AudioRankingScorePolicy.RISING_LIKE_COUNT_THRESHOLD)
assertEquals(3, AudioRankingScorePolicy.RISING_COMMENT_COUNT_THRESHOLD)
assertEquals(3, AudioRankingScorePolicy.RISING_SALES_COUNT_THRESHOLD)
assertEquals(0.0, policy.applyMinimumThreshold(growthRate = 5.0, recentCount = 9, minimumThreshold = 10), 0.0001)
assertEquals(5.0, policy.applyMinimumThreshold(growthRate = 5.0, recentCount = 10, minimumThreshold = 10), 0.0001)
val score = policy.calculateRisingScore(
recentSalesCount = 2,
previousSalesCount = 1,
recentViewCount = 9,
previousViewCount = 1,
recentLikeCount = 2,
previousLikeCount = 1,
recentCommentCount = 3,
previousCommentCount = 1,
releaseDate = aggregationEndAt.minusDays(20),
aggregationEndAt = aggregationEndAt,
isPaid = true
)
assertEquals(0.5, score, 0.0001)
}
@Test
@DisplayName("최소 기준을 통과한 지표의 음수 증가율은 지금 뜨는 중 점수에 그대로 반영한다")
fun shouldPreserveNegativeGrowthWhenThresholdPasses() {
val score = policy.calculateRisingScore(
recentSalesCount = 3,
previousSalesCount = 6,
recentViewCount = 10,
previousViewCount = 20,
recentLikeCount = 3,
previousLikeCount = 6,
recentCommentCount = 3,
previousCommentCount = 6,
releaseDate = aggregationEndAt.minusDays(20),
aggregationEndAt = aggregationEndAt,
isPaid = true
)
assertEquals(-0.5, score, 0.0001)
}
@Test
@DisplayName("신규 콘텐츠 부스트는 집계 종료일 기준 3일/7일/14일 경계를 포함해 적용한다")
fun shouldReturnReleaseBoostByBoundaries() {
assertEquals(1.5, AudioRankingScorePolicy.RELEASE_BOOST_WITHIN_THREE_DAYS, 0.0001)
assertEquals(1.3, AudioRankingScorePolicy.RELEASE_BOOST_WITHIN_SEVEN_DAYS, 0.0001)
assertEquals(1.15, AudioRankingScorePolicy.RELEASE_BOOST_WITHIN_FOURTEEN_DAYS, 0.0001)
assertEquals(1.0, AudioRankingScorePolicy.RELEASE_BOOST_DEFAULT, 0.0001)
assertEquals(1.5, policy.releaseBoost(aggregationEndAt.minusDays(3), aggregationEndAt), 0.0001)
assertEquals(1.3, policy.releaseBoost(aggregationEndAt.minusDays(7), aggregationEndAt), 0.0001)
assertEquals(1.15, policy.releaseBoost(aggregationEndAt.minusDays(14), aggregationEndAt), 0.0001)
assertEquals(1.0, policy.releaseBoost(aggregationEndAt.minusDays(15), aggregationEndAt), 0.0001)
}
}