feat(audio-recommendation): 오디오 추천 점수 정책을 추가한다
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user