fix(content): 최근 댓글 오디오 후보를 크리에이터별로 제한한다

This commit is contained in:
2026-07-12 16:31:04 +09:00
parent fda51e6e52
commit e82afb5530
2 changed files with 56 additions and 6 deletions

View File

@@ -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
""".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 order by score desc, random_tie_breaker asc
limit :limit limit :limit
""".trimIndent() """.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)

View File

@@ -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() {