diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScorePolicy.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScorePolicy.kt new file mode 100644 index 00000000..3b9803b6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScorePolicy.kt @@ -0,0 +1,49 @@ +package kr.co.vividnext.sodalive.v2.ranking.domain + +class CreatorRankingScorePolicy { + fun calculateContentLiveScore( + liveCanAmount: Long, + contentPurchaseCanAmount: Long + ): Double { + return (liveCanAmount * CreatorRankingScoreSpec.CONTENT_LIVE_CAN_WEIGHT) + + (contentPurchaseCanAmount * CreatorRankingScoreSpec.CONTENT_PURCHASE_CAN_WEIGHT) + } + + fun calculateEngagementScore( + contentLikeCount: Long, + contentCommentCount: Long + ): Double { + return (contentLikeCount * CreatorRankingScoreSpec.CONTENT_LIKE_COUNT_WEIGHT) + + (contentCommentCount * CreatorRankingScoreSpec.CONTENT_COMMENT_COUNT_WEIGHT) + } + + fun calculateSupportScore( + channelDonationCanAmount: Long, + channelDonationCount: Long, + fanTalkCount: Long + ): Double { + return (channelDonationCanAmount * CreatorRankingScoreSpec.CHANNEL_DONATION_CAN_WEIGHT) + + (channelDonationCount * CreatorRankingScoreSpec.CHANNEL_DONATION_COUNT_WEIGHT) + + (fanTalkCount * CreatorRankingScoreSpec.FAN_TALK_COUNT_WEIGHT) + } + + fun calculateFanLoyaltyScore( + finalFollowerCount: Long, + followIncrease: Long + ): Double { + return (finalFollowerCount * CreatorRankingScoreSpec.FINAL_FOLLOWER_COUNT_WEIGHT) + + (followIncrease * CreatorRankingScoreSpec.FOLLOW_INCREASE_WEIGHT) + } + + fun calculateFinalScore( + contentLiveScore: Double, + engagementScore: Double, + supportScore: Double, + fanLoyaltyScore: Double + ): Double { + return (contentLiveScore * CreatorRankingScoreSpec.CONTENT_LIVE_SCORE_WEIGHT) + + (engagementScore * CreatorRankingScoreSpec.ENGAGEMENT_SCORE_WEIGHT) + + (supportScore * CreatorRankingScoreSpec.SUPPORT_SCORE_WEIGHT) + + (fanLoyaltyScore * CreatorRankingScoreSpec.FAN_LOYALTY_SCORE_WEIGHT) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScoreSpec.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScoreSpec.kt new file mode 100644 index 00000000..6e7a4997 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScoreSpec.kt @@ -0,0 +1,21 @@ +package kr.co.vividnext.sodalive.v2.ranking.domain + +object CreatorRankingScoreSpec { + const val CONTENT_LIVE_CAN_WEIGHT = 0.7 + const val CONTENT_PURCHASE_CAN_WEIGHT = 0.3 + + const val CONTENT_LIKE_COUNT_WEIGHT = 0.5 + const val CONTENT_COMMENT_COUNT_WEIGHT = 0.5 + + const val CHANNEL_DONATION_CAN_WEIGHT = 0.6 + const val CHANNEL_DONATION_COUNT_WEIGHT = 0.2 + const val FAN_TALK_COUNT_WEIGHT = 0.2 + + const val FINAL_FOLLOWER_COUNT_WEIGHT = 0.7 + const val FOLLOW_INCREASE_WEIGHT = 0.3 + + const val CONTENT_LIVE_SCORE_WEIGHT = 0.35 + const val ENGAGEMENT_SCORE_WEIGHT = 0.3 + const val SUPPORT_SCORE_WEIGHT = 0.25 + const val FAN_LOYALTY_SCORE_WEIGHT = 0.1 +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScorePolicyTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScorePolicyTest.kt new file mode 100644 index 00000000..e68e8679 --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScorePolicyTest.kt @@ -0,0 +1,76 @@ +package kr.co.vividnext.sodalive.v2.ranking.domain + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +class CreatorRankingScorePolicyTest { + private val policy = CreatorRankingScorePolicy() + + @Test + @DisplayName("콘텐츠/라이브 점수는 라이브 계열 캔 70%와 콘텐츠 구매 캔 30%를 raw value로 계산한다") + fun shouldCalculateContentLiveScore() { + assertEquals(0.7, CreatorRankingScoreSpec.CONTENT_LIVE_CAN_WEIGHT, 0.0001) + assertEquals(0.3, CreatorRankingScoreSpec.CONTENT_PURCHASE_CAN_WEIGHT, 0.0001) + + val score = policy.calculateContentLiveScore(liveCanAmount = 1000, contentPurchaseCanAmount = 200) + + assertEquals(760.0, score, 0.0001) + } + + @Test + @DisplayName("참여 반응 점수는 좋아요 수와 댓글 수를 각각 50% raw value로 계산한다") + fun shouldCalculateEngagementScore() { + assertEquals(0.5, CreatorRankingScoreSpec.CONTENT_LIKE_COUNT_WEIGHT, 0.0001) + assertEquals(0.5, CreatorRankingScoreSpec.CONTENT_COMMENT_COUNT_WEIGHT, 0.0001) + + val score = policy.calculateEngagementScore(contentLikeCount = 40, contentCommentCount = 20) + + assertEquals(30.0, score, 0.0001) + } + + @Test + @DisplayName("응원 점수는 채널 후원 캔/건수와 팬 Talk 수를 raw value로 계산한다") + fun shouldCalculateSupportScore() { + assertEquals(0.6, CreatorRankingScoreSpec.CHANNEL_DONATION_CAN_WEIGHT, 0.0001) + assertEquals(0.2, CreatorRankingScoreSpec.CHANNEL_DONATION_COUNT_WEIGHT, 0.0001) + assertEquals(0.2, CreatorRankingScoreSpec.FAN_TALK_COUNT_WEIGHT, 0.0001) + + val score = policy.calculateSupportScore( + channelDonationCanAmount = 1000, + channelDonationCount = 10, + fanTalkCount = 20 + ) + + assertEquals(606.0, score, 0.0001) + } + + @Test + @DisplayName("팬 충성도 점수는 음수 팔로우 증가 수를 최종 점수에 그대로 반영한다") + fun shouldCalculateFanLoyaltyScoreWithNegativeFollowIncrease() { + assertEquals(0.7, CreatorRankingScoreSpec.FINAL_FOLLOWER_COUNT_WEIGHT, 0.0001) + assertEquals(0.3, CreatorRankingScoreSpec.FOLLOW_INCREASE_WEIGHT, 0.0001) + + val score = policy.calculateFanLoyaltyScore(finalFollowerCount = 100, followIncrease = -10) + + assertEquals(67.0, score, 0.0001) + } + + @Test + @DisplayName("최종 점수는 카테고리별 점수에 PRD 가중치를 적용하고 0~100 정규화하지 않는다") + fun shouldCalculateFinalScoreWithoutNormalization() { + assertEquals(0.35, CreatorRankingScoreSpec.CONTENT_LIVE_SCORE_WEIGHT, 0.0001) + assertEquals(0.3, CreatorRankingScoreSpec.ENGAGEMENT_SCORE_WEIGHT, 0.0001) + assertEquals(0.25, CreatorRankingScoreSpec.SUPPORT_SCORE_WEIGHT, 0.0001) + assertEquals(0.1, CreatorRankingScoreSpec.FAN_LOYALTY_SCORE_WEIGHT, 0.0001) + + val score = policy.calculateFinalScore( + contentLiveScore = 760.0, + engagementScore = 30.0, + supportScore = 606.0, + fanLoyaltyScore = 67.0 + ) + + assertEquals(433.2, score, 0.0001) + } +}