test #437
@@ -288,7 +288,8 @@ class DefaultAudioRecommendationQueryRepository(
|
|||||||
else 0.0
|
else 0.0
|
||||||
end * 20.0
|
end * 20.0
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
requireComments = true
|
requireComments = true,
|
||||||
|
dedupeByCreator = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,12 +326,13 @@ class DefaultAudioRecommendationQueryRepository(
|
|||||||
limit: Int,
|
limit: Int,
|
||||||
sectionType: RecommendedSectionType,
|
sectionType: RecommendedSectionType,
|
||||||
scoreExpression: String,
|
scoreExpression: String,
|
||||||
requireComments: Boolean = false
|
requireComments: Boolean = false,
|
||||||
|
dedupeByCreator: Boolean = false
|
||||||
): List<RecommendationSnapshotRecord> {
|
): List<RecommendationSnapshotRecord> {
|
||||||
val commentJoin = if (requireComments) "join" else "left join"
|
val commentJoin = if (requireComments) "join" else "left join"
|
||||||
val commentRequirement = if (requireComments) "and cm.comment_count is not null" else ""
|
val commentRequirement = if (requireComments) "and cm.comment_count is not null" else ""
|
||||||
val sql = """
|
val scoredSql = """
|
||||||
select c.id, ($scoreExpression) score, rand() random_tie_breaker
|
select c.id, c.member_id creator_id, ($scoreExpression) score, rand() random_tie_breaker
|
||||||
from content c
|
from content c
|
||||||
join member creator on creator.id = c.member_id
|
join member creator on creator.id = c.member_id
|
||||||
join content_theme theme on theme.id = c.theme_id
|
join content_theme theme on theme.id = c.theme_id
|
||||||
@@ -372,9 +374,29 @@ class DefaultAudioRecommendationQueryRepository(
|
|||||||
and theme.is_active = true
|
and theme.is_active = true
|
||||||
and (:includeAdult = true or c.is_adult = false)
|
and (:includeAdult = true or c.is_adult = false)
|
||||||
$commentRequirement
|
$commentRequirement
|
||||||
order by score desc, random_tie_breaker asc
|
|
||||||
limit :limit
|
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
val sql = if (dedupeByCreator) {
|
||||||
|
"""
|
||||||
|
select id, score, random_tie_breaker
|
||||||
|
from (
|
||||||
|
select scored.*, row_number() over (
|
||||||
|
partition by creator_id
|
||||||
|
order by score desc, random_tie_breaker asc
|
||||||
|
) creator_rank
|
||||||
|
from ($scoredSql) scored
|
||||||
|
) ranked
|
||||||
|
where creator_rank = 1
|
||||||
|
order by score desc, random_tie_breaker asc
|
||||||
|
limit :limit
|
||||||
|
""".trimIndent()
|
||||||
|
} else {
|
||||||
|
"""
|
||||||
|
select id, score, random_tie_breaker
|
||||||
|
from ($scoredSql) scored
|
||||||
|
order by score desc, random_tie_breaker asc
|
||||||
|
limit :limit
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
return entityManager.createNativeQuery(sql)
|
return entityManager.createNativeQuery(sql)
|
||||||
.setParameter("windowStart", windowStart)
|
.setParameter("windowStart", windowStart)
|
||||||
.setParameter("snapshotAt", snapshotAt)
|
.setParameter("snapshotAt", snapshotAt)
|
||||||
|
|||||||
@@ -232,6 +232,34 @@ class DefaultAudioRecommendationQueryRepositoryTest @Autowired constructor(
|
|||||||
assertEquals("https://cdn.test/commented.png", commented.first().imageUrl)
|
assertEquals("https://cdn.test/commented.png", commented.first().imageUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("최근 댓글 많은 오디오 스냅샷은 같은 크리에이터 후보 중 최고 점수 1개만 반환한다")
|
||||||
|
fun shouldKeepOnlyHighestScoredMostCommentedSnapshotPerCreator() {
|
||||||
|
val snapshotAt = LocalDateTime.of(2026, 7, 12, 23, 59, 59)
|
||||||
|
val windowStart = snapshotAt.minusDays(6).toLocalDate().atStartOfDay()
|
||||||
|
val sameCreator = saveMember("dedupe-comment-creator", MemberRole.CREATOR)
|
||||||
|
val otherCreator = saveMember("dedupe-other-creator", MemberRole.CREATOR)
|
||||||
|
val writer = saveMember("dedupe-comment-writer", MemberRole.USER)
|
||||||
|
val theme = saveTheme()
|
||||||
|
val lowerScore = saveAudio(sameCreator, theme, "same-creator-lower", snapshotAt.minusDays(2))
|
||||||
|
val higherScore = saveAudio(sameCreator, theme, "same-creator-higher", snapshotAt.minusDays(2))
|
||||||
|
val other = saveAudio(otherCreator, theme, "other-creator", snapshotAt.minusDays(2))
|
||||||
|
saveComment(lowerScore, writer, "lower", snapshotAt.minusHours(1))
|
||||||
|
saveComment(higherScore, writer, "higher-1", snapshotAt.minusHours(2))
|
||||||
|
saveComment(higherScore, writer, "higher-2", snapshotAt.minusHours(1))
|
||||||
|
saveComment(other, writer, "other", snapshotAt.minusHours(1))
|
||||||
|
flushAndClear()
|
||||||
|
|
||||||
|
val snapshots = repository.findMostCommentedSnapshots(
|
||||||
|
windowStart,
|
||||||
|
snapshotAt,
|
||||||
|
AudioRecommendationVisibility.SAFE,
|
||||||
|
5
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf(higherScore.id, other.id), snapshots.map { it.targetId })
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("댓글 상세는 공개 최상위 댓글만 노출하고 동일 시각이면 id가 큰 댓글 하나를 선택한다")
|
@DisplayName("댓글 상세는 공개 최상위 댓글만 노출하고 동일 시각이면 id가 큰 댓글 하나를 선택한다")
|
||||||
fun shouldFindLatestVisibleTopLevelCommentWithIdTieBreaker() {
|
fun shouldFindLatestVisibleTopLevelCommentWithIdTieBreaker() {
|
||||||
|
|||||||
Reference in New Issue
Block a user