test #449
+40
-12
@@ -11,6 +11,7 @@ import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
|
||||
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType
|
||||
import kr.co.vividnext.sodalive.content.main.banner.QAudioContentBanner.audioContentBanner
|
||||
import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme
|
||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent
|
||||
import kr.co.vividnext.sodalive.event.EventItem
|
||||
@@ -38,6 +39,7 @@ import javax.persistence.EntityManager
|
||||
class DefaultAudioRecommendationQueryRepository(
|
||||
private val queryFactory: JPAQueryFactory,
|
||||
private val entityManager: EntityManager,
|
||||
private val contentTranslationRepository: ContentTranslationRepository,
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val cloudFrontHost: String
|
||||
) : AudioRecommendationQueryRepository {
|
||||
@@ -115,32 +117,35 @@ class DefaultAudioRecommendationQueryRepository(
|
||||
limit: Int,
|
||||
memberId: Long?,
|
||||
canViewAdultContent: Boolean,
|
||||
now: LocalDateTime
|
||||
now: LocalDateTime,
|
||||
locale: String
|
||||
): List<AudioCard> {
|
||||
val rows = audioRows(memberId, canViewAdultContent, now) {
|
||||
orderBy(audioContent.releaseDate.desc(), audioContent.id.desc()).limit(limit.toLong())
|
||||
}
|
||||
return rows.toAudioCards(now, canViewAdultContent)
|
||||
return rows.toAudioCards(now, canViewAdultContent, locale)
|
||||
}
|
||||
|
||||
override fun findFreeAudios(
|
||||
limit: Int,
|
||||
memberId: Long?,
|
||||
canViewAdultContent: Boolean,
|
||||
now: LocalDateTime
|
||||
now: LocalDateTime,
|
||||
locale: String
|
||||
): List<AudioCard> {
|
||||
val randomTieBreaker = Expressions.numberTemplate(Double::class.java, "function('rand')")
|
||||
val rows = audioRows(memberId, canViewAdultContent, now, audioContent.price.eq(0)) {
|
||||
orderBy(randomTieBreaker.asc()).limit(limit.toLong())
|
||||
}
|
||||
return rows.toAudioCards(now, canViewAdultContent)
|
||||
return rows.toAudioCards(now, canViewAdultContent, locale)
|
||||
}
|
||||
|
||||
override fun findPointAudios(
|
||||
limit: Int,
|
||||
memberId: Long?,
|
||||
canViewAdultContent: Boolean,
|
||||
now: LocalDateTime
|
||||
now: LocalDateTime,
|
||||
locale: String
|
||||
): List<AudioCard> {
|
||||
val randomTieBreaker = Expressions.numberTemplate(Double::class.java, "function('rand')")
|
||||
val rows = audioRows(
|
||||
@@ -151,25 +156,27 @@ class DefaultAudioRecommendationQueryRepository(
|
||||
) {
|
||||
orderBy(randomTieBreaker.asc()).limit(limit.toLong())
|
||||
}
|
||||
return rows.toAudioCards(now, canViewAdultContent)
|
||||
return rows.toAudioCards(now, canViewAdultContent, locale)
|
||||
}
|
||||
|
||||
override fun findAudioCardsByIds(
|
||||
contentIds: List<Long>,
|
||||
memberId: Long?,
|
||||
canViewAdultContent: Boolean,
|
||||
now: LocalDateTime
|
||||
now: LocalDateTime,
|
||||
locale: String
|
||||
): List<AudioCard> {
|
||||
if (contentIds.isEmpty()) return emptyList()
|
||||
val orderById = contentIds.withIndex().associate { it.value to it.index }
|
||||
val rows = audioRows(memberId, canViewAdultContent, now, audioContent.id.`in`(contentIds)) { this }
|
||||
return rows.toAudioCards(now, canViewAdultContent).sortedBy { orderById[it.audioContentId] ?: Int.MAX_VALUE }
|
||||
return rows.toAudioCards(now, canViewAdultContent, locale).sortedBy { orderById[it.audioContentId] ?: Int.MAX_VALUE }
|
||||
}
|
||||
|
||||
override fun findCommentedAudiosByIds(
|
||||
contentIds: List<Long>,
|
||||
memberId: Long?,
|
||||
canViewAdultContent: Boolean
|
||||
canViewAdultContent: Boolean,
|
||||
locale: String
|
||||
): List<CommentedAudio> {
|
||||
if (contentIds.isEmpty()) return emptyList()
|
||||
val contentOrder = contentIds.withIndex().associate { it.value to it.index }
|
||||
@@ -235,7 +242,7 @@ class DefaultAudioRecommendationQueryRepository(
|
||||
)
|
||||
)
|
||||
""".trimIndent()
|
||||
return entityManager.createNativeQuery(sql)
|
||||
val commentedAudios = entityManager.createNativeQuery(sql)
|
||||
.setParameter("contentIds", contentIds)
|
||||
.setParameter("memberId", memberId)
|
||||
.setParameter("canViewAdultContent", canViewAdultContent)
|
||||
@@ -251,6 +258,11 @@ class DefaultAudioRecommendationQueryRepository(
|
||||
?: "$cloudFrontHost/profile/default-profile.png"
|
||||
)
|
||||
}
|
||||
val translatedTitleByContentId = translatedTitles(commentedAudios.map { it.audioContentId }, locale)
|
||||
return commentedAudios
|
||||
.map { audio ->
|
||||
translatedTitleByContentId[audio.audioContentId]?.let { audio.copy(title = it) } ?: audio
|
||||
}
|
||||
.sortedBy { contentOrder[it.audioContentId] ?: Int.MAX_VALUE }
|
||||
}
|
||||
|
||||
@@ -455,18 +467,23 @@ class DefaultAudioRecommendationQueryRepository(
|
||||
.fetch()
|
||||
}
|
||||
|
||||
private fun List<Tuple>.toAudioCards(now: LocalDateTime, canViewAdultContent: Boolean): List<AudioCard> {
|
||||
private fun List<Tuple>.toAudioCards(
|
||||
now: LocalDateTime,
|
||||
canViewAdultContent: Boolean,
|
||||
locale: String
|
||||
): List<AudioCard> {
|
||||
if (isEmpty()) return emptyList()
|
||||
val contentIds = map { it.get(audioContent.id)!! }
|
||||
val creatorIds = map { it.get(audioContent.member.id)!! }.distinct()
|
||||
val firstContentIdByCreatorId = firstAudioContentIds(creatorIds, now, canViewAdultContent)
|
||||
val isOriginalSeriesByContentId = originalSeriesFlags(contentIds)
|
||||
val translatedTitleByContentId = translatedTitles(contentIds, locale)
|
||||
return map { row ->
|
||||
val contentId = row.get(audioContent.id)!!
|
||||
val creatorId = row.get(audioContent.member.id)!!
|
||||
AudioCard(
|
||||
audioContentId = contentId,
|
||||
title = row.get(audioContent.title)!!,
|
||||
title = translatedTitleByContentId[contentId] ?: row.get(audioContent.title)!!,
|
||||
duration = row.get(audioContent.duration),
|
||||
imageUrl = row.get(audioContent.coverImage).toCdnUrl(cloudFrontHost),
|
||||
price = row.get(audioContent.price)!!,
|
||||
@@ -479,6 +496,17 @@ class DefaultAudioRecommendationQueryRepository(
|
||||
}
|
||||
}
|
||||
|
||||
private fun translatedTitles(contentIds: List<Long>, locale: String): Map<Long, String> {
|
||||
if (contentIds.isEmpty()) return emptyMap()
|
||||
return contentTranslationRepository.findByContentIdInAndLocale(contentIds, locale)
|
||||
.mapNotNull { translation ->
|
||||
translation.renderedPayload.title
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.let { translation.contentId to it }
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
private fun firstAudioContentIds(
|
||||
creatorIds: List<Long>,
|
||||
now: LocalDateTime,
|
||||
|
||||
+58
@@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.v2.api.content.overview.adapter.`in`.web
|
||||
|
||||
import kr.co.vividnext.sodalive.content.AudioContent
|
||||
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.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberAdapter
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
@@ -93,6 +95,44 @@ class ContentOverviewEndToEndTest @Autowired constructor(
|
||||
.andExpect(jsonPath("$.data.hasNext").value(false))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("콘텐츠 전체보기 New & Hot은 같은 요청에서 en-ja-en 제목 번역을 격리한다")
|
||||
fun shouldIsolateRequestedLocaleTitlesForNewAndHotAudioOverview() {
|
||||
val fixture = createNewAndHotFixture("content-overview-new-hot")
|
||||
saveContentTranslation(fixture.audioContentId, "en", "content-overview-new-hot-en-translated")
|
||||
saveContentTranslation(fixture.audioContentId, "ja", "content-overview-new-hot-ja-translated")
|
||||
|
||||
expectOverviewTitle(fixture, "NEW_AND_HOT_AUDIO", "en", "content-overview-new-hot-en-translated")
|
||||
expectOverviewTitle(fixture, "NEW_AND_HOT_AUDIO", "ja", "content-overview-new-hot-ja-translated")
|
||||
expectOverviewTitle(fixture, "NEW_AND_HOT_AUDIO", "en", "content-overview-new-hot-en-translated")
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("콘텐츠 전체보기 첫 오디오는 같은 요청에서 en-ja-en 제목 번역을 격리한다")
|
||||
fun shouldIsolateRequestedLocaleTitlesForFirstAudioContentOverview() {
|
||||
val fixture = createFirstAudioFixture("content-overview-first")
|
||||
saveContentTranslation(fixture.audioContentId, "en", "content-overview-first-en-translated")
|
||||
saveContentTranslation(fixture.audioContentId, "ja", "content-overview-first-ja-translated")
|
||||
|
||||
expectOverviewTitle(fixture, "FIRST_AUDIO_CONTENT", "en", "content-overview-first-en-translated")
|
||||
expectOverviewTitle(fixture, "FIRST_AUDIO_CONTENT", "ja", "content-overview-first-ja-translated")
|
||||
expectOverviewTitle(fixture, "FIRST_AUDIO_CONTENT", "en", "content-overview-first-en-translated")
|
||||
}
|
||||
|
||||
private fun expectOverviewTitle(fixture: Fixture, type: String, locale: String, title: String) {
|
||||
mockMvc.perform(
|
||||
get("/api/v2/contents")
|
||||
.param("type", type)
|
||||
.param("page", "0")
|
||||
.param("size", "20")
|
||||
.header("Accept-Language", locale)
|
||||
.with(user(MemberAdapter(fixture.viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.items[0].contentId").value(fixture.audioContentId))
|
||||
.andExpect(jsonPath("$.data.items[0].title").value(title))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("콘텐츠 전체보기 API는 유효하지 않은 type을 New & Hot으로 대체한다")
|
||||
fun shouldFallbackInvalidTypeToNewAndHotAudio() {
|
||||
@@ -197,6 +237,24 @@ class ContentOverviewEndToEndTest @Autowired constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveContentTranslation(contentId: Long, locale: String, title: String) {
|
||||
transactionTemplate.execute {
|
||||
val translation = ContentTranslation(
|
||||
contentId = contentId,
|
||||
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 data class Fixture(
|
||||
val viewer: Member,
|
||||
val audioContentId: Long
|
||||
|
||||
+72
-3
@@ -4,6 +4,8 @@ import kr.co.vividnext.sodalive.admin.content.series.genre.SeriesGenre
|
||||
import kr.co.vividnext.sodalive.content.AudioContent
|
||||
import kr.co.vividnext.sodalive.content.comment.AudioContentComment
|
||||
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.creator.admin.content.series.Series
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesContent
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
@@ -54,9 +56,11 @@ class AudioRecommendationEndToEndTest @Autowired constructor(
|
||||
.andExpect(jsonPath("$.data.latestAudios[0].isOriginalSeries").value(true))
|
||||
.andExpect(jsonPath("$.data.recommendedAudios").isArray)
|
||||
.andExpect(jsonPath("$.data.recommendedAudios[0].audioContentId").value(fixture.audioContentId))
|
||||
.andExpect(jsonPath("$.data.pointAudios.length()").value(1))
|
||||
.andExpect(jsonPath("$.data.pointAudios[0].audioContentId").value(fixture.paidPointAudioContentId))
|
||||
.andExpect(jsonPath("$.data.pointAudios[0].price").value(100))
|
||||
.andExpect(
|
||||
jsonPath(
|
||||
"$.data.pointAudios[?(@.audioContentId == ${fixture.paidPointAudioContentId} && @.price == 100)]"
|
||||
).isNotEmpty
|
||||
)
|
||||
.andExpect(jsonPath("$.data.mostCommentedAudios[0].latestComment").value("latest e2e comment"))
|
||||
.andExpect(
|
||||
jsonPath("$.data.mostCommentedAudios[0].latestCommentWriterProfileImageUrl")
|
||||
@@ -64,6 +68,51 @@ class AudioRecommendationEndToEndTest @Autowired constructor(
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("오디오 추천 API는 같은 요청에서 en-ja-en 제목 번역을 격리한다")
|
||||
fun shouldIsolateRequestedLocaleTitlesForAllRecommendationAudioCards() {
|
||||
val fixture = createFixture()
|
||||
transactionTemplate.execute {
|
||||
saveContentTranslation(fixture.audioContentId, "en", "audio-recommendation-e2e-en")
|
||||
saveContentTranslation(fixture.audioContentId, "ja", "audio-recommendation-e2e-ja")
|
||||
saveContentTranslation(fixture.paidPointAudioContentId, "en", "paid-point-en")
|
||||
saveContentTranslation(fixture.paidPointAudioContentId, "ja", "paid-point-ja")
|
||||
entityManager.flush()
|
||||
}
|
||||
|
||||
expectRecommendationTitles(fixture, "en", "audio-recommendation-e2e-en", "paid-point-en")
|
||||
expectRecommendationTitles(fixture, "ja", "audio-recommendation-e2e-ja", "paid-point-ja")
|
||||
expectRecommendationTitles(fixture, "en", "audio-recommendation-e2e-en", "paid-point-en")
|
||||
}
|
||||
|
||||
private fun expectRecommendationTitles(
|
||||
fixture: Fixture,
|
||||
locale: String,
|
||||
audioTitle: String,
|
||||
pointAudioTitle: String
|
||||
) {
|
||||
mockMvc.perform(get("/api/v2/audio/recommendations").header("Accept-Language", locale))
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(
|
||||
jsonPath(translatedTitlePath("latestAudios", fixture.audioContentId, audioTitle)).isNotEmpty
|
||||
)
|
||||
.andExpect(
|
||||
jsonPath(translatedTitlePath("newAndHotAudios", fixture.audioContentId, audioTitle)).isNotEmpty
|
||||
)
|
||||
.andExpect(
|
||||
jsonPath(translatedTitlePath("freeAudios", fixture.audioContentId, audioTitle)).isNotEmpty
|
||||
)
|
||||
.andExpect(
|
||||
jsonPath(translatedTitlePath("pointAudios", fixture.paidPointAudioContentId, pointAudioTitle)).isNotEmpty
|
||||
)
|
||||
.andExpect(
|
||||
jsonPath(translatedTitlePath("mostCommentedAudios", fixture.audioContentId, audioTitle)).isNotEmpty
|
||||
)
|
||||
.andExpect(
|
||||
jsonPath(translatedTitlePath("recommendedAudios", fixture.audioContentId, audioTitle)).isNotEmpty
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFixture(): Fixture {
|
||||
return transactionTemplate.execute {
|
||||
val now = LocalDateTime.now().minusHours(1)
|
||||
@@ -183,6 +232,26 @@ class AudioRecommendationEndToEndTest @Autowired constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveContentTranslation(contentId: Long, locale: String, title: String) {
|
||||
val translation = ContentTranslation(
|
||||
contentId = contentId,
|
||||
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 translatedTitlePath(collection: String, contentId: Long, title: String): String {
|
||||
return "$.data.$collection[?(@.audioContentId == $contentId && @.title == '$title')]"
|
||||
}
|
||||
|
||||
private data class Fixture(
|
||||
val seriesId: Long,
|
||||
val audioContentId: Long,
|
||||
|
||||
+149
-8
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user