feat(audio-recommendation): 추천 콘텐츠 제목 번역을 적용한다

This commit is contained in:
2026-09-09 23:20:21 +09:00
parent 44008c7623
commit dba70483ad
4 changed files with 319 additions and 23 deletions
@@ -10,6 +10,9 @@ import kr.co.vividnext.sodalive.content.main.banner.AudioContentBanner
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType
import kr.co.vividnext.sodalive.content.main.tab.AudioContentMainTab
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
import kr.co.vividnext.sodalive.content.translation.ContentTranslation
import kr.co.vividnext.sodalive.content.translation.ContentTranslationPayload
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
import kr.co.vividnext.sodalive.creator.admin.content.series.Series
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesContent
import kr.co.vividnext.sodalive.i18n.Lang
@@ -38,9 +41,15 @@ import javax.persistence.EntityManager
@Import(QueryDslConfig::class)
class DefaultAudioRecommendationQueryRepositoryTest @Autowired constructor(
private val entityManager: EntityManager,
queryFactory: JPAQueryFactory
queryFactory: JPAQueryFactory,
contentTranslationRepository: ContentTranslationRepository
) {
private val repository = DefaultAudioRecommendationQueryRepository(queryFactory, entityManager, "https://cdn.test")
private val repository = DefaultAudioRecommendationQueryRepository(
queryFactory,
entityManager,
contentTranslationRepository,
"https://cdn.test"
)
@Test
@DisplayName("배너는 콘텐츠 추천 탭과 성인 조회 가능 여부를 적용한다")
@@ -210,9 +219,27 @@ class DefaultAudioRecommendationQueryRepositoryTest @Autowired constructor(
}
flushAndClear()
val latestAudios = repository.findLatestAudios(12, viewer.id, canViewAdultContent = false, now = now)
val freeAudios = repository.findFreeAudios(10, viewer.id, canViewAdultContent = false, now = now)
val pointAudios = repository.findPointAudios(10, viewer.id, canViewAdultContent = false, now = now)
val latestAudios = repository.findLatestAudios(
12,
viewer.id,
canViewAdultContent = false,
now = now,
locale = Lang.KO.code
)
val freeAudios = repository.findFreeAudios(
10,
viewer.id,
canViewAdultContent = false,
now = now,
locale = Lang.KO.code
)
val pointAudios = repository.findPointAudios(
10,
viewer.id,
canViewAdultContent = false,
now = now,
locale = Lang.KO.code
)
assertEquals(12, latestAudios.size)
assertEquals(listOf(latest.id, paidPoint.id), latestAudios.take(2).map { it.audioContentId })
@@ -297,7 +324,8 @@ class DefaultAudioRecommendationQueryRepositoryTest @Autowired constructor(
val commented = repository.findCommentedAudiosByIds(
contentIds = listOf(first.id!!, second.id!!),
memberId = viewer.id,
canViewAdultContent = false
canViewAdultContent = false,
locale = Lang.KO.code
)
assertEquals(listOf(first.id), snapshots.map { it.targetId })
@@ -308,6 +336,101 @@ class DefaultAudioRecommendationQueryRepositoryTest @Autowired constructor(
assertEquals("https://cdn.test/commented.png", commented.first().imageUrl)
}
@Test
@DisplayName("오디오 카드 공통 QueryDSL 경로는 빈 문자열, 공백, 누락, 다른 언어 번역을 원문으로 대체한다")
fun shouldTranslateAudioCardsByRequestedLocaleWithFallbackAndInputOrder() {
val now = LocalDateTime.of(2026, 6, 23, 12, 0)
val creator = saveMember("translated-card-creator", MemberRole.CREATOR)
val theme = saveTheme()
val translated = saveAudio(creator, theme, "translated-source", now.minusDays(1))
val empty = saveAudio(creator, theme, "empty-source", now.minusDays(2))
val whitespace = saveAudio(creator, theme, "whitespace-source", now.minusDays(3))
val otherLocaleOnly = saveAudio(creator, theme, "other-locale-source", now.minusDays(4))
val missing = saveAudio(creator, theme, "missing-source", now.minusDays(5))
saveContentTranslation(translated, "en", "translated-en")
saveContentTranslation(translated, "ja", "translated-ja")
saveContentTranslation(empty, "en", "")
saveContentTranslation(whitespace, "en", " ")
saveContentTranslation(otherLocaleOnly, "ja", "other-locale-ja")
flushAndClear()
val contentIds = listOf(missing.id!!, empty.id!!, whitespace.id!!, otherLocaleOnly.id!!, translated.id!!)
val englishCards = repository.findAudioCardsByIds(contentIds, null, false, now, Lang.EN.code)
val japaneseCards = repository.findAudioCardsByIds(contentIds, null, false, now, Lang.JA.code)
assertEquals(contentIds, englishCards.map { it.audioContentId })
assertEquals(
listOf("missing-source", "empty-source", "whitespace-source", "other-locale-source", "translated-en"),
englishCards.map { it.title }
)
assertEquals(contentIds, japaneseCards.map { it.audioContentId })
assertEquals(
listOf("missing-source", "empty-source", "whitespace-source", "other-locale-ja", "translated-ja"),
japaneseCards.map { it.title }
)
}
@Test
@DisplayName("최근 댓글 많은 오디오 native query는 locale별 제목만 바꾸고 fallback과 댓글 필드를 보존한다")
fun shouldTranslateCommentedAudiosByRequestedLocaleWithFallbackAndPreservedFields() {
val now = LocalDateTime.now().minusHours(1)
val creator = saveMember("translated-commented-creator", MemberRole.CREATOR)
val writer = saveMember("translated-commented-writer", MemberRole.USER)
val theme = saveTheme()
val translated = saveAudio(creator, theme, "commented-translated-source", now.minusMinutes(1))
val empty = saveAudio(creator, theme, "commented-empty-source", now.minusMinutes(2))
val whitespace = saveAudio(creator, theme, "commented-whitespace-source", now.minusMinutes(3))
val otherLocaleOnly = saveAudio(creator, theme, "commented-other-locale-source", now.minusMinutes(4))
val missing = saveAudio(creator, theme, "commented-missing-source", now.minusMinutes(5))
saveComment(translated, writer, "translated-comment", now)
saveComment(empty, writer, "empty-comment", now)
saveComment(whitespace, writer, "whitespace-comment", now)
saveComment(otherLocaleOnly, writer, "other-locale-comment", now)
saveComment(missing, writer, "missing-comment", now)
saveContentTranslation(translated, "en", "commented-translated-en")
saveContentTranslation(translated, "ja", "commented-translated-ja")
saveContentTranslation(empty, "en", "")
saveContentTranslation(whitespace, "en", " ")
saveContentTranslation(otherLocaleOnly, "ja", "commented-other-locale-ja")
flushAndClear()
val contentIds = listOf(missing.id!!, empty.id!!, whitespace.id!!, otherLocaleOnly.id!!, translated.id!!)
val englishAudios = repository.findCommentedAudiosByIds(contentIds, null, false, Lang.EN.code)
val japaneseAudios = repository.findCommentedAudiosByIds(contentIds, null, false, Lang.JA.code)
assertEquals(contentIds, englishAudios.map { it.audioContentId })
assertEquals(contentIds, japaneseAudios.map { it.audioContentId })
assertEquals(
listOf(
"commented-missing-source",
"commented-empty-source",
"commented-whitespace-source",
"commented-other-locale-source",
"commented-translated-en"
),
englishAudios.map { it.title }
)
assertEquals(
listOf(
"commented-missing-source",
"commented-empty-source",
"commented-whitespace-source",
"commented-other-locale-ja",
"commented-translated-ja"
),
japaneseAudios.map { it.title }
)
assertEquals(
listOf("missing-comment", "empty-comment", "whitespace-comment", "other-locale-comment", "translated-comment"),
englishAudios.map { it.latestComment }
)
assertEquals(englishAudios.map { it.latestComment }, japaneseAudios.map { it.latestComment })
assertEquals(
englishAudios.map { it.latestCommentWriterProfileImageUrl },
japaneseAudios.map { it.latestCommentWriterProfileImageUrl }
)
}
@Test
@DisplayName("최근 댓글 많은 오디오 스냅샷은 같은 크리에이터 후보 중 최고 점수 1개만 반환한다")
fun shouldKeepOnlyHighestScoredMostCommentedSnapshotPerCreator() {
@@ -356,7 +479,8 @@ class DefaultAudioRecommendationQueryRepositoryTest @Autowired constructor(
val commented = repository.findCommentedAudiosByIds(
contentIds = listOf(audio.id!!),
memberId = viewer.id,
canViewAdultContent = false
canViewAdultContent = false,
locale = Lang.KO.code
)
assertEquals(1, commented.size)
@@ -381,7 +505,8 @@ class DefaultAudioRecommendationQueryRepositoryTest @Autowired constructor(
val commented = repository.findCommentedAudiosByIds(
contentIds = listOf(audio.id!!),
memberId = viewer.id,
canViewAdultContent = false
canViewAdultContent = false,
locale = Lang.KO.code
)
assertEquals(1, commented.size)
@@ -507,6 +632,22 @@ class DefaultAudioRecommendationQueryRepositoryTest @Autowired constructor(
return audio
}
private fun saveContentTranslation(content: AudioContent, locale: String, title: String) {
val translation = ContentTranslation(
contentId = content.id!!,
locale = locale,
renderedPayload = ContentTranslationPayload(title = title, detail = "", tags = "")
)
entityManager.persist(translation)
entityManager.flush()
val payload = "{\"title\":\"$title\",\"detail\":\"\",\"tags\":\"\"}"
entityManager.createNativeQuery(
"update content_translation set rendered_payload = '$payload' format json where id = :id"
)
.setParameter("id", translation.id)
.executeUpdate()
}
private fun saveView(audio: AudioContent, viewedAt: LocalDateTime) {
entityManager.persist(
CreatorContentViewHistory(memberId = 1L, contentId = audio.id!!, genreId = 1L, viewedAt = viewedAt)