From a7e17fede2240f9c08ba1e2c0c7ef3291ca7447b Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 31 May 2026 00:56:59 +0900 Subject: [PATCH] =?UTF-8?q?feat(recommend):=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EC=A0=90=EC=88=98=20=EC=82=B0=EC=8B=9D=20=EC=83=81=EC=88=98?= =?UTF-8?q?=EB=A5=BC=20=EB=B6=84=EB=A6=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/RecommendationScorePolicy.kt | 32 +++++++++++++------ .../domain/RecommendationScoreSpec.kt | 27 ++++++++++++++++ .../domain/RecommendationScorePolicyTest.kt | 23 +++++++++++-- 3 files changed, 70 insertions(+), 12 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScoreSpec.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScorePolicy.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScorePolicy.kt index d3a491dc..f50f68d3 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScorePolicy.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScorePolicy.kt @@ -18,16 +18,22 @@ class RecommendationScorePolicy { communicationScore: Long, newBoost: Double ): Double { - return ((followIncrease * 0.35) + (contentActivityScore * 0.3) + (communicationScore * 0.2)) * newBoost + return ( + (followIncrease * RecommendationScoreSpec.DEBUT_FOLLOW_INCREASE_WEIGHT) + + (contentActivityScore * RecommendationScoreSpec.DEBUT_CONTENT_ACTIVITY_WEIGHT) + + (communicationScore * RecommendationScoreSpec.DEBUT_COMMUNICATION_WEIGHT) + ) * newBoost } fun calculateAiChatScore( recentChatCount: Long, recentActiveUserCount: Long, - followIncrease: Long, newBoost: Double ): Double { - return ((0.45 * recentChatCount) + (0.35 * recentActiveUserCount) + (0.20 * followIncrease)) * newBoost + return ( + (RecommendationScoreSpec.AI_RECENT_CHAT_WEIGHT * recentChatCount) + + (RecommendationScoreSpec.AI_RECENT_ACTIVE_USER_WEIGHT * recentActiveUserCount) + ) * newBoost } fun calculateCheerScore( @@ -36,7 +42,11 @@ class RecommendationScorePolicy { donationCount: Long, newBoost: Double ): Double { - return ((0.6 * donationAmount) + (0.3 * fanTalkCount) + (0.1 * donationCount)) * newBoost + return ( + (RecommendationScoreSpec.CHEER_DONATION_AMOUNT_WEIGHT * donationAmount) + + (RecommendationScoreSpec.CHEER_FAN_TALK_WEIGHT * fanTalkCount) + + (RecommendationScoreSpec.CHEER_DONATION_COUNT_WEIGHT * donationCount) + ) * newBoost } fun calculateCommunityScore( @@ -45,7 +55,11 @@ class RecommendationScorePolicy { followerCount: Long, newBoost: Double ): Double { - return ((0.5 * likeCount) + (0.5 * commentCount) + (0.1 * followerCount)) * newBoost + return ( + (RecommendationScoreSpec.COMMUNITY_LIKE_WEIGHT * likeCount) + + (RecommendationScoreSpec.COMMUNITY_COMMENT_WEIGHT * commentCount) + + (RecommendationScoreSpec.COMMUNITY_FOLLOWER_WEIGHT * followerCount) + ) * newBoost } fun calculateFirstAudioRecencyScore(releaseDate: LocalDateTime, now: LocalDateTime): Int { @@ -63,10 +77,10 @@ class RecommendationScorePolicy { private fun calculateNewBoost(baseAt: LocalDateTime, now: LocalDateTime): Double { val days = ChronoUnit.DAYS.between(baseAt.toLocalDate(), now.toLocalDate()) return when { - days <= 10 -> 1.5 - days <= 20 -> 1.3 - days <= 30 -> 1.2 - else -> 1.0 + days <= RecommendationScoreSpec.NEW_BOOST_10_DAY_LIMIT -> RecommendationScoreSpec.NEW_BOOST_10_DAYS + days <= RecommendationScoreSpec.NEW_BOOST_20_DAY_LIMIT -> RecommendationScoreSpec.NEW_BOOST_20_DAYS + days <= RecommendationScoreSpec.NEW_BOOST_30_DAY_LIMIT -> RecommendationScoreSpec.NEW_BOOST_30_DAYS + else -> RecommendationScoreSpec.DEFAULT_NEW_BOOST } } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScoreSpec.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScoreSpec.kt new file mode 100644 index 00000000..86a0cdda --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScoreSpec.kt @@ -0,0 +1,27 @@ +package kr.co.vividnext.sodalive.v2.recommend.domain + +object RecommendationScoreSpec { + const val NEW_BOOST_10_DAY_LIMIT = 10L + const val NEW_BOOST_20_DAY_LIMIT = 20L + const val NEW_BOOST_30_DAY_LIMIT = 30L + + const val DEBUT_FOLLOW_INCREASE_WEIGHT = 0.35 + const val DEBUT_CONTENT_ACTIVITY_WEIGHT = 0.3 + const val DEBUT_COMMUNICATION_WEIGHT = 0.2 + + const val AI_RECENT_CHAT_WEIGHT = 0.45 + const val AI_RECENT_ACTIVE_USER_WEIGHT = 0.35 + + const val CHEER_DONATION_AMOUNT_WEIGHT = 0.6 + const val CHEER_FAN_TALK_WEIGHT = 0.3 + const val CHEER_DONATION_COUNT_WEIGHT = 0.1 + + const val COMMUNITY_LIKE_WEIGHT = 0.5 + const val COMMUNITY_COMMENT_WEIGHT = 0.5 + const val COMMUNITY_FOLLOWER_WEIGHT = 0.1 + + const val NEW_BOOST_10_DAYS = 1.5 + const val NEW_BOOST_20_DAYS = 1.3 + const val NEW_BOOST_30_DAYS = 1.2 + const val DEFAULT_NEW_BOOST = 1.0 +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScorePolicyTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScorePolicyTest.kt index 96ec92c6..25bf0cbc 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScorePolicyTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScorePolicyTest.kt @@ -13,6 +13,9 @@ class RecommendationScorePolicyTest { fun shouldApplyCreatorNewBoostByDebutDays() { val now = LocalDateTime.of(2026, 5, 30, 12, 0) + assertEquals(10L, RecommendationScoreSpec.NEW_BOOST_10_DAY_LIMIT) + assertEquals(20L, RecommendationScoreSpec.NEW_BOOST_20_DAY_LIMIT) + assertEquals(30L, RecommendationScoreSpec.NEW_BOOST_30_DAY_LIMIT) assertEquals(1.5, policy.calculateCreatorNewBoost(now.minusDays(10), now), 0.0001) assertEquals(1.3, policy.calculateCreatorNewBoost(now.minusDays(20), now), 0.0001) assertEquals(1.2, policy.calculateCreatorNewBoost(now.minusDays(30), now), 0.0001) @@ -33,6 +36,10 @@ class RecommendationScorePolicyTest { @Test @DisplayName("최근 데뷔 크리에이터 추천 점수는 PRD 가중치와 신규 부스트를 적용한다") fun shouldCalculateDebutCreatorScore() { + assertEquals(0.35, RecommendationScoreSpec.DEBUT_FOLLOW_INCREASE_WEIGHT, 0.0001) + assertEquals(0.3, RecommendationScoreSpec.DEBUT_CONTENT_ACTIVITY_WEIGHT, 0.0001) + assertEquals(0.2, RecommendationScoreSpec.DEBUT_COMMUNICATION_WEIGHT, 0.0001) + val score = policy.calculateDebutCreatorScore( followIncrease = 10, contentActivityScore = 20, @@ -44,21 +51,27 @@ class RecommendationScorePolicyTest { } @Test - @DisplayName("AI 채팅 추천 점수는 PRD 가중치와 신규 부스트를 적용한다") + @DisplayName("AI 채팅 추천 점수는 이번 스프린트에서 팔로우 증가량을 제외한다") fun shouldCalculateAiChatScore() { + assertEquals(0.45, RecommendationScoreSpec.AI_RECENT_CHAT_WEIGHT, 0.0001) + assertEquals(0.35, RecommendationScoreSpec.AI_RECENT_ACTIVE_USER_WEIGHT, 0.0001) + val score = policy.calculateAiChatScore( recentChatCount = 100, recentActiveUserCount = 20, - followIncrease = 10, newBoost = 1.3 ) - assertEquals(70.2, score, 0.0001) + assertEquals(67.6, score, 0.0001) } @Test @DisplayName("최근 응원 추천 점수는 후원 금액, 팬 Talk 수, 후원 수에 가중치를 적용한다") fun shouldCalculateCheerScore() { + assertEquals(0.6, RecommendationScoreSpec.CHEER_DONATION_AMOUNT_WEIGHT, 0.0001) + assertEquals(0.3, RecommendationScoreSpec.CHEER_FAN_TALK_WEIGHT, 0.0001) + assertEquals(0.1, RecommendationScoreSpec.CHEER_DONATION_COUNT_WEIGHT, 0.0001) + val score = policy.calculateCheerScore( donationAmount = 1000, fanTalkCount = 20, @@ -72,6 +85,10 @@ class RecommendationScorePolicyTest { @Test @DisplayName("인기 커뮤니티 점수는 좋아요 수, 댓글 수, 팔로우 수에 가중치를 적용한다") fun shouldCalculateCommunityScore() { + assertEquals(0.5, RecommendationScoreSpec.COMMUNITY_LIKE_WEIGHT, 0.0001) + assertEquals(0.5, RecommendationScoreSpec.COMMUNITY_COMMENT_WEIGHT, 0.0001) + assertEquals(0.1, RecommendationScoreSpec.COMMUNITY_FOLLOWER_WEIGHT, 0.0001) + val score = policy.calculateCommunityScore( likeCount = 40, commentCount = 20,