feat(home-recommendation): 첫 오디오 제목 번역을 적용한다
This commit is contained in:
+8
-2
@@ -1,6 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.v2.api.home.application
|
||||
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryService
|
||||
@@ -26,7 +27,11 @@ import java.time.LocalDateTime
|
||||
|
||||
class HomeRecommendationFacadeTest {
|
||||
private val queryPort = FakeHomeRecommendationQueryPort()
|
||||
private val queryService = HomeRecommendationQueryService(queryPort, EmptyRecommendationSnapshotPort())
|
||||
private val queryService = HomeRecommendationQueryService(
|
||||
queryPort,
|
||||
EmptyRecommendationSnapshotPort(),
|
||||
langContext = LangContext()
|
||||
)
|
||||
private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
|
||||
private val facade = HomeRecommendationFacade(queryService, preferenceService, "https://cdn.test")
|
||||
|
||||
@@ -135,7 +140,8 @@ class HomeRecommendationFacadeTest {
|
||||
offset: Long,
|
||||
limit: Int,
|
||||
memberId: Long?,
|
||||
includeAdultContents: Boolean
|
||||
includeAdultContents: Boolean,
|
||||
locale: String
|
||||
): List<HomeFirstAudioContentRecord> = firstAudioContents
|
||||
|
||||
override fun findAiCharacterSnapshots(
|
||||
|
||||
+132
-13
@@ -21,6 +21,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.event.Event
|
||||
@@ -63,9 +66,10 @@ import javax.persistence.Query
|
||||
@Import(QueryDslConfig::class)
|
||||
class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
private val entityManager: EntityManager,
|
||||
queryFactory: JPAQueryFactory
|
||||
queryFactory: JPAQueryFactory,
|
||||
contentTranslationRepository: ContentTranslationRepository
|
||||
) {
|
||||
private val repository = DefaultHomeRecommendationQueryRepository(queryFactory, entityManager)
|
||||
private val repository = DefaultHomeRecommendationQueryRepository(queryFactory, entityManager, contentTranslationRepository)
|
||||
private val scorePolicy = RecommendationScorePolicy()
|
||||
|
||||
@Test
|
||||
@@ -1565,12 +1569,76 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
).forEach { (content, createdAt) -> updateCreatedAt("AudioContent", content.id!!, createdAt) }
|
||||
flushAndClear()
|
||||
|
||||
val contents = repository.findFirstAudioContents(now, limit = 10)
|
||||
val contents = repository.findFirstAudioContents(now, limit = 10, locale = Lang.KO.code)
|
||||
|
||||
assertEquals(listOf(eligibleActive.id), contents.map { it.contentId })
|
||||
assertEquals(12, contents.single().price)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("첫 오디오 native query는 빈 문자열, 공백, 누락, 다른 언어 번역을 원문으로 대체한다")
|
||||
fun shouldTranslateFirstAudioContentsByRequestedLocaleWithFallback() {
|
||||
val now = LocalDateTime.of(2026, 5, 31, 10, 0)
|
||||
val translated = saveAudioContent(
|
||||
saveMember("translated-first-audio", MemberRole.CREATOR),
|
||||
now.minusDays(1),
|
||||
isActive = true
|
||||
)
|
||||
val empty = saveAudioContent(
|
||||
saveMember("empty-first-audio", MemberRole.CREATOR),
|
||||
now.minusDays(2),
|
||||
isActive = true
|
||||
)
|
||||
val whitespace = saveAudioContent(
|
||||
saveMember("whitespace-first-audio", MemberRole.CREATOR),
|
||||
now.minusDays(3),
|
||||
isActive = true
|
||||
)
|
||||
val otherLocaleOnly = saveAudioContent(
|
||||
saveMember("other-locale-first-audio", MemberRole.CREATOR),
|
||||
now.minusDays(4),
|
||||
isActive = true
|
||||
)
|
||||
val missing = saveAudioContent(
|
||||
saveMember("missing-first-audio", MemberRole.CREATOR),
|
||||
now.minusDays(5),
|
||||
isActive = true
|
||||
)
|
||||
saveContentTranslation(translated, "en", "first-audio-en")
|
||||
saveContentTranslation(translated, "ja", "first-audio-ja")
|
||||
saveContentTranslation(empty, "en", "")
|
||||
saveContentTranslation(whitespace, "en", " ")
|
||||
saveContentTranslation(otherLocaleOnly, "en", "other-locale-first-audio-en")
|
||||
flushAndClear()
|
||||
|
||||
val englishRecords = repository.findFirstAudioContents(now, limit = 10, locale = Lang.EN.code)
|
||||
val japaneseRecords = repository.findFirstAudioContents(now, limit = 10, locale = Lang.JA.code)
|
||||
|
||||
assertEquals(englishRecords.map { it.contentId }, japaneseRecords.map { it.contentId })
|
||||
assertEquals(
|
||||
"first-audio-en",
|
||||
englishRecords.single { it.contentId == translated.id }.title
|
||||
)
|
||||
assertEquals(empty.title, englishRecords.single { it.contentId == empty.id }.title)
|
||||
assertEquals(whitespace.title, englishRecords.single { it.contentId == whitespace.id }.title)
|
||||
assertEquals(
|
||||
"other-locale-first-audio-en",
|
||||
englishRecords.single { it.contentId == otherLocaleOnly.id }.title
|
||||
)
|
||||
assertEquals(missing.title, englishRecords.single { it.contentId == missing.id }.title)
|
||||
assertEquals(
|
||||
"first-audio-ja",
|
||||
japaneseRecords.single { it.contentId == translated.id }.title
|
||||
)
|
||||
assertEquals(empty.title, japaneseRecords.single { it.contentId == empty.id }.title)
|
||||
assertEquals(whitespace.title, japaneseRecords.single { it.contentId == whitespace.id }.title)
|
||||
assertEquals(
|
||||
otherLocaleOnly.title,
|
||||
japaneseRecords.single { it.contentId == otherLocaleOnly.id }.title
|
||||
)
|
||||
assertEquals(missing.title, japaneseRecords.single { it.contentId == missing.id }.title)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("첫 오디오 콘텐츠는 예약/미공개 콘텐츠를 제외하고 releaseDate 최신성 점수순으로 조회한다")
|
||||
fun shouldFindFirstAudioContentsOrderedByReleaseDateRecencyScoreExcludingScheduledContents() {
|
||||
@@ -1585,7 +1653,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
updateCreatedAt("AudioContent", old.id!!, now.minusDays(20))
|
||||
flushAndClear()
|
||||
|
||||
val contents = repository.findFirstAudioContents(now, limit = 10)
|
||||
val contents = repository.findFirstAudioContents(now, limit = 10, locale = Lang.KO.code)
|
||||
|
||||
assertEquals(listOf(fresh.id, old.id), contents.map { it.contentId })
|
||||
}
|
||||
@@ -1602,8 +1670,20 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
val oldest = saveAudioContent(oldestCreator, now.minusDays(21), isActive = true, isAdult = false)
|
||||
flushAndClear()
|
||||
|
||||
val page0 = repository.findFirstAudioContents(now, offset = 0L, limit = 2, includeAdultContents = false)
|
||||
val page1 = repository.findFirstAudioContents(now, offset = 1L, limit = 2, includeAdultContents = false)
|
||||
val page0 = repository.findFirstAudioContents(
|
||||
now,
|
||||
offset = 0L,
|
||||
limit = 2,
|
||||
includeAdultContents = false,
|
||||
locale = Lang.KO.code
|
||||
)
|
||||
val page1 = repository.findFirstAudioContents(
|
||||
now,
|
||||
offset = 1L,
|
||||
limit = 2,
|
||||
includeAdultContents = false,
|
||||
locale = Lang.KO.code
|
||||
)
|
||||
|
||||
assertEquals(listOf(newest.id, oldest.id), page0.map { it.contentId })
|
||||
assertEquals(listOf(oldest.id), page1.map { it.contentId })
|
||||
@@ -1622,7 +1702,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
updateCreatedAt("AudioContent", content.id!!, now.minusDays(1))
|
||||
flushAndClear()
|
||||
|
||||
val contents = repository.findFirstAudioContents(now, limit = 10)
|
||||
val contents = repository.findFirstAudioContents(now, limit = 10, locale = Lang.KO.code)
|
||||
|
||||
assertEquals(false, contents.single().isAdult)
|
||||
assertEquals(true, contents.single().isOriginalSeries)
|
||||
@@ -1633,12 +1713,16 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
fun shouldMapNumericNativeBooleanFromFirstAudioContentRows() {
|
||||
val mockEntityManager = Mockito.mock(EntityManager::class.java)
|
||||
val mockQuery = Mockito.mock(Query::class.java)
|
||||
val mockContentTranslationRepository = Mockito.mock(ContentTranslationRepository::class.java)
|
||||
val repository = DefaultHomeRecommendationQueryRepository(
|
||||
JPAQueryFactory(mockEntityManager),
|
||||
mockEntityManager
|
||||
mockEntityManager,
|
||||
mockContentTranslationRepository
|
||||
)
|
||||
Mockito.`when`(mockEntityManager.createNativeQuery(anyString())).thenReturn(mockQuery)
|
||||
Mockito.`when`(mockQuery.setParameter(anyString(), any())).thenReturn(mockQuery)
|
||||
Mockito.`when`(mockContentTranslationRepository.findByContentIdInAndLocale(listOf(1L), Lang.KO.code))
|
||||
.thenReturn(emptyList())
|
||||
Mockito.`when`(mockQuery.resultList).thenReturn(
|
||||
listOf(
|
||||
arrayOf(
|
||||
@@ -1661,7 +1745,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
offset = 0L,
|
||||
limit = 1,
|
||||
memberId = null,
|
||||
includeAdultContents = false
|
||||
includeAdultContents = false,
|
||||
locale = Lang.KO.code
|
||||
)
|
||||
|
||||
assertEquals(true, contents.single().isOriginalSeries)
|
||||
@@ -1682,7 +1767,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
saveBlock(creatorBlockedViewer, viewer)
|
||||
flushAndClear()
|
||||
|
||||
val contents = repository.findFirstAudioContents(now, limit = 10, memberId = viewer.id)
|
||||
val contents = repository.findFirstAudioContents(now, limit = 10, memberId = viewer.id, locale = Lang.KO.code)
|
||||
|
||||
assertEquals(listOf(visibleContent.id), contents.map { it.contentId })
|
||||
}
|
||||
@@ -1699,9 +1784,27 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
val content3 = saveAudioContent(creator3, now.minusDays(5), isActive = true)
|
||||
flushAndClear()
|
||||
|
||||
val page0 = repository.findFirstAudioContents(now, offset = 0L, limit = 1, includeAdultContents = false)
|
||||
val page1 = repository.findFirstAudioContents(now, offset = 1L, limit = 1, includeAdultContents = false)
|
||||
val page2 = repository.findFirstAudioContents(now, offset = 2L, limit = 1, includeAdultContents = false)
|
||||
val page0 = repository.findFirstAudioContents(
|
||||
now,
|
||||
offset = 0L,
|
||||
limit = 1,
|
||||
includeAdultContents = false,
|
||||
locale = Lang.KO.code
|
||||
)
|
||||
val page1 = repository.findFirstAudioContents(
|
||||
now,
|
||||
offset = 1L,
|
||||
limit = 1,
|
||||
includeAdultContents = false,
|
||||
locale = Lang.KO.code
|
||||
)
|
||||
val page2 = repository.findFirstAudioContents(
|
||||
now,
|
||||
offset = 2L,
|
||||
limit = 1,
|
||||
includeAdultContents = false,
|
||||
locale = Lang.KO.code
|
||||
)
|
||||
|
||||
val pagedContentIds = (page0 + page1 + page2).map { it.contentId }
|
||||
assertEquals(setOf(content1.id, content2.id, content3.id), pagedContentIds.toSet())
|
||||
@@ -2621,6 +2724,22 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
return content
|
||||
}
|
||||
|
||||
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 saveTheme(name: String, isActive: Boolean = true): AudioContentTheme {
|
||||
val theme = AudioContentTheme(
|
||||
theme = name,
|
||||
|
||||
Reference in New Issue
Block a user