feat(audio-recommendation): 오디오 추천 점수 정책을 추가한다

This commit is contained in:
2026-06-23 16:12:11 +09:00
parent cf7fea156b
commit 3df66d98ef
2 changed files with 127 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
package kr.co.vividnext.sodalive.v2.audio.recommendation.domain
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit
class AudioRecommendationScorePolicy {
fun calculateNewAndHotScore(
viewCount: Long,
likeCount: Long,
commentCount: Long,
releaseDate: LocalDateTime,
now: LocalDateTime
): Double {
return viewCount * NEW_AND_HOT_VIEW_WEIGHT +
likeCount * NEW_AND_HOT_LIKE_WEIGHT +
commentCount * NEW_AND_HOT_COMMENT_WEIGHT +
newAndHotRecencyMultiplier(releaseDate, now) * NEW_AND_HOT_RECENCY_WEIGHT
}
fun calculateRecommendedAudioScore(
viewCount: Long,
likeCount: Long,
commentCount: Long,
releaseDate: LocalDateTime,
now: LocalDateTime
): Double {
return viewCount * RECOMMENDED_VIEW_WEIGHT +
likeCount * RECOMMENDED_LIKE_WEIGHT +
commentCount * RECOMMENDED_COMMENT_WEIGHT +
recommendedAudioRecencyMultiplier(releaseDate, now) * RECOMMENDED_RECENCY_WEIGHT
}
fun calculateCommentScore(commentCount: Long, latestCommentAt: LocalDateTime, now: LocalDateTime): Double {
return commentCount * COMMENT_COUNT_WEIGHT + commentRecencyMultiplier(latestCommentAt, now) * COMMENT_RECENCY_WEIGHT
}
fun newAndHotRecencyMultiplier(releaseDate: LocalDateTime, now: LocalDateTime): Double {
val days = daysBetween(releaseDate, now)
return when {
days <= 3 -> 1.3
days <= 7 -> 1.15
days <= 14 -> 1.0
else -> 0.8
}
}
fun recommendedAudioRecencyMultiplier(releaseDate: LocalDateTime, now: LocalDateTime): Double {
val days = daysBetween(releaseDate, now)
return when {
days <= 3 -> 1.3
days <= 7 -> 1.15
days <= 30 -> 1.1
else -> 1.0
}
}
fun commentRecencyMultiplier(latestCommentAt: LocalDateTime, now: LocalDateTime): Double {
val days = daysBetween(latestCommentAt, now)
return when {
days <= 3 -> 1.3
days <= 7 -> 1.15
days <= 14 -> 1.0
else -> 0.0
}
}
private fun daysBetween(from: LocalDateTime, now: LocalDateTime): Long {
return ChronoUnit.DAYS.between(from.toLocalDate(), now.toLocalDate()).coerceAtLeast(0)
}
companion object {
const val NEW_AND_HOT_RECENCY_WEIGHT = 35.0
const val NEW_AND_HOT_VIEW_WEIGHT = 35.0
const val NEW_AND_HOT_LIKE_WEIGHT = 15.0
const val NEW_AND_HOT_COMMENT_WEIGHT = 15.0
const val RECOMMENDED_VIEW_WEIGHT = 45.0
const val RECOMMENDED_LIKE_WEIGHT = 25.0
const val RECOMMENDED_COMMENT_WEIGHT = 20.0
const val RECOMMENDED_RECENCY_WEIGHT = 10.0
const val COMMENT_COUNT_WEIGHT = 80.0
const val COMMENT_RECENCY_WEIGHT = 20.0
}
}

View File

@@ -0,0 +1,44 @@
package kr.co.vividnext.sodalive.v2.audio.recommendation.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 AudioRecommendationScorePolicyTest {
private val policy = AudioRecommendationScorePolicy()
private val now = LocalDateTime.of(2026, 6, 23, 12, 0)
@Test
@DisplayName("New & Hot 점수는 원본 count를 정규화하지 않고 가중합으로 계산한다")
fun shouldCalculateNewAndHotScoreWithoutNormalization() {
val score = policy.calculateNewAndHotScore(10, 4, 2, now.minusDays(2), now)
assertEquals(485.5, score)
}
@Test
@DisplayName("추천 오디오 점수는 원본 count와 최신성 배수를 가중합으로 계산한다")
fun shouldCalculateRecommendedAudioScoreWithoutNormalization() {
val score = policy.calculateRecommendedAudioScore(10, 4, 2, now.minusDays(10), now)
assertEquals(601.0, score)
}
@Test
@DisplayName("최신성 배수는 정책별 일수 경계를 적용한다")
fun shouldReturnRecencyMultipliersByPolicyBoundaries() {
assertEquals(1.3, policy.newAndHotRecencyMultiplier(now.minusDays(3), now))
assertEquals(1.15, policy.newAndHotRecencyMultiplier(now.minusDays(7), now))
assertEquals(1.0, policy.newAndHotRecencyMultiplier(now.minusDays(14), now))
assertEquals(0.8, policy.newAndHotRecencyMultiplier(now.minusDays(15), now))
assertEquals(1.3, policy.recommendedAudioRecencyMultiplier(now.minusDays(3), now))
assertEquals(1.15, policy.recommendedAudioRecencyMultiplier(now.minusDays(7), now))
assertEquals(1.1, policy.recommendedAudioRecencyMultiplier(now.minusDays(30), now))
assertEquals(1.0, policy.recommendedAudioRecencyMultiplier(now.minusDays(31), now))
assertEquals(1.3, policy.commentRecencyMultiplier(now.minusDays(3), now))
assertEquals(1.15, policy.commentRecencyMultiplier(now.minusDays(7), now))
assertEquals(1.0, policy.commentRecencyMultiplier(now.minusDays(14), now))
assertEquals(0.0, policy.commentRecencyMultiplier(now.minusDays(15), now))
}
}