test #449

Merged
klaus merged 29 commits from test into main 2026-09-10 07:58:32 +00:00
2 changed files with 190 additions and 17 deletions
Showing only changes of commit 12b9a912c0 - Show all commits
@@ -10,6 +10,8 @@ import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
import kr.co.vividnext.sodalive.content.order.OrderType import kr.co.vividnext.sodalive.content.order.OrderType
import kr.co.vividnext.sodalive.content.order.QOrder.order import kr.co.vividnext.sodalive.content.order.QOrder.order
import kr.co.vividnext.sodalive.content.pin.QPinContent.pinContent import kr.co.vividnext.sodalive.content.pin.QPinContent.pinContent
import kr.co.vividnext.sodalive.content.series.translation.QSeriesTranslation
import kr.co.vividnext.sodalive.content.translation.QContentTranslation
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series 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.creator.admin.content.series.QSeriesContent.seriesContent
import kr.co.vividnext.sodalive.explorer.profile.QCreatorCheers.creatorCheers import kr.co.vividnext.sodalive.explorer.profile.QCreatorCheers.creatorCheers
@@ -153,13 +155,15 @@ class DefaultCreatorChannelHomeQueryRepository(
creatorId: Long, creatorId: Long,
now: LocalDateTime, now: LocalDateTime,
canViewAdultContent: Boolean, canViewAdultContent: Boolean,
viewerId: Long? viewerId: Long?,
locale: String
): CreatorChannelAudioContentRecord? { ): CreatorChannelAudioContentRecord? {
val row = findAudioContentRows(creatorId, now, null, canViewAdultContent, 1).firstOrNull() ?: return null val row = findAudioContentRows(creatorId, now, null, canViewAdultContent, 1).firstOrNull() ?: return null
val contentId = itAudioId(row) val contentId = itAudioId(row)
return row.toAudioRecord( return row.toAudioRecord(
firstContentId = firstAudioContentId(creatorId, now, canViewAdultContent), firstContentId = firstAudioContentId(creatorId, now, canViewAdultContent),
seriesByContentId = audioSeriesByContentIds(listOf(contentId)), titlesByContentId = audioTitlesByContentIds(listOf(contentId), locale),
seriesByContentId = audioSeriesByContentIds(listOf(contentId), locale),
orderStatesByContentId = orderStatesByContentIds(viewerId, listOf(contentId), now) orderStatesByContentId = orderStatesByContentIds(viewerId, listOf(contentId), now)
) )
} }
@@ -209,6 +213,7 @@ class DefaultCreatorChannelHomeQueryRepository(
viewerId: Long?, viewerId: Long?,
isViewerCreator: Boolean, isViewerCreator: Boolean,
effectiveViewerGender: Gender?, effectiveViewerGender: Gender?,
locale: String,
limit: Int limit: Int
): List<CreatorChannelScheduleRecord> { ): List<CreatorChannelScheduleRecord> {
val liveSchedules = queryFactory val liveSchedules = queryFactory
@@ -235,18 +240,21 @@ class DefaultCreatorChannelHomeQueryRepository(
) )
.fetch() .fetch()
val contentTranslation = QContentTranslation("homeScheduleContentTranslation")
val audioSchedules = queryFactory val audioSchedules = queryFactory
.select( .select(
Projections.constructor(
CreatorChannelScheduleRecord::class.java,
audioContent.releaseDate, audioContent.releaseDate,
audioContent.title, audioContent.title,
Expressions.constant(CreatorActivityType.AUDIO), contentTranslation.renderedPayload,
audioContent.id, audioContent.id,
audioContent.isAdult audioContent.isAdult
) )
)
.from(audioContent) .from(audioContent)
.leftJoin(contentTranslation)
.on(
contentTranslation.contentId.eq(audioContent.id),
contentTranslation.locale.eq(locale)
)
.where( .where(
audioContent.member.id.eq(creatorId), audioContent.member.id.eq(creatorId),
audioContent.member.isActive.isTrue, audioContent.member.isActive.isTrue,
@@ -256,6 +264,17 @@ class DefaultCreatorChannelHomeQueryRepository(
adultAudioCondition(canViewAdultContent) adultAudioCondition(canViewAdultContent)
) )
.fetch() .fetch()
.map {
val originalTitle = it.get(audioContent.title)!!
val translatedTitle = it.get(contentTranslation.renderedPayload)?.title
CreatorChannelScheduleRecord(
scheduledAt = it.get(audioContent.releaseDate)!!,
title = translatedTitle.takeUnless(String?::isNullOrBlank) ?: originalTitle,
type = CreatorActivityType.AUDIO,
targetId = it.get(audioContent.id)!!,
isAdult = it.get(audioContent.isAdult)!!
)
}
return (liveSchedules + audioSchedules) return (liveSchedules + audioSchedules)
.sortedWith(compareBy<CreatorChannelScheduleRecord> { it.scheduledAt }.thenBy { it.type.sortOrder }) .sortedWith(compareBy<CreatorChannelScheduleRecord> { it.scheduledAt }.thenBy { it.type.sortOrder })
@@ -268,14 +287,16 @@ class DefaultCreatorChannelHomeQueryRepository(
latestAudioContentId: Long?, latestAudioContentId: Long?,
canViewAdultContent: Boolean, canViewAdultContent: Boolean,
viewerId: Long?, viewerId: Long?,
locale: String,
limit: Int limit: Int
): List<CreatorChannelAudioContentRecord> { ): List<CreatorChannelAudioContentRecord> {
val rows = findAudioContentRows(creatorId, now, latestAudioContentId, canViewAdultContent, limit, pinFirst = true) val rows = findAudioContentRows(creatorId, now, latestAudioContentId, canViewAdultContent, limit, pinFirst = true)
val contentIds = rows.map { itAudioId(it) } val contentIds = rows.map { itAudioId(it) }
val firstContentId = firstAudioContentId(creatorId, now, canViewAdultContent) val firstContentId = firstAudioContentId(creatorId, now, canViewAdultContent)
val seriesByContentId = audioSeriesByContentIds(contentIds) val titlesByContentId = audioTitlesByContentIds(contentIds, locale)
val seriesByContentId = audioSeriesByContentIds(contentIds, locale)
val orderStatesByContentId = orderStatesByContentIds(viewerId, contentIds, now) val orderStatesByContentId = orderStatesByContentIds(viewerId, contentIds, now)
return rows.map { it.toAudioRecord(firstContentId, seriesByContentId, orderStatesByContentId) } return rows.map { it.toAudioRecord(firstContentId, titlesByContentId, seriesByContentId, orderStatesByContentId) }
} }
override fun findSeries( override fun findSeries(
@@ -284,16 +305,24 @@ class DefaultCreatorChannelHomeQueryRepository(
now: LocalDateTime, now: LocalDateTime,
canViewAdultContent: Boolean, canViewAdultContent: Boolean,
contentType: ContentType, contentType: ContentType,
locale: String,
limit: Int limit: Int
): List<CreatorChannelSeriesRecord> { ): List<CreatorChannelSeriesRecord> {
val seriesTranslation = QSeriesTranslation("homeSeriesTranslation")
val seriesRows = queryFactory val seriesRows = queryFactory
.select( .select(
series.id, series.id,
series.title, series.title,
seriesTranslation.renderedPayload,
series.coverImage, series.coverImage,
series.isOriginal series.isOriginal
) )
.from(series) .from(series)
.leftJoin(seriesTranslation)
.on(
seriesTranslation.seriesId.eq(series.id),
seriesTranslation.locale.eq(locale)
)
.where( .where(
series.member.id.eq(creatorId), series.member.id.eq(creatorId),
series.member.isActive.isTrue, series.member.isActive.isTrue,
@@ -317,7 +346,8 @@ class DefaultCreatorChannelHomeQueryRepository(
val seriesId = seriesRow.get(series.id)!! val seriesId = seriesRow.get(series.id)!!
CreatorChannelSeriesRecord( CreatorChannelSeriesRecord(
seriesId = seriesId, seriesId = seriesId,
title = seriesRow.get(series.title)!!, title = seriesRow.get(seriesTranslation.renderedPayload)?.title.takeUnless(String?::isNullOrBlank)
?: seriesRow.get(series.title)!!,
coverImagePath = seriesRow.get(series.coverImage), coverImagePath = seriesRow.get(series.coverImage),
numberOfContent = stats.contentCount, numberOfContent = stats.contentCount,
isNew = seriesId in newSeriesIds, isNew = seriesId in newSeriesIds,
@@ -493,6 +523,7 @@ class DefaultCreatorChannelHomeQueryRepository(
private fun com.querydsl.core.Tuple.toAudioRecord( private fun com.querydsl.core.Tuple.toAudioRecord(
firstContentId: Long?, firstContentId: Long?,
titlesByContentId: Map<Long, String>,
seriesByContentId: Map<Long, AudioSeriesSummary>, seriesByContentId: Map<Long, AudioSeriesSummary>,
orderStatesByContentId: Map<Long, AudioOrderState> orderStatesByContentId: Map<Long, AudioOrderState>
): CreatorChannelAudioContentRecord { ): CreatorChannelAudioContentRecord {
@@ -501,7 +532,7 @@ class DefaultCreatorChannelHomeQueryRepository(
val orderState = orderStatesByContentId[audioContentId] val orderState = orderStatesByContentId[audioContentId]
return CreatorChannelAudioContentRecord( return CreatorChannelAudioContentRecord(
audioContentId = audioContentId, audioContentId = audioContentId,
title = get(audioContent.title)!!, title = titlesByContentId[audioContentId] ?: get(audioContent.title)!!,
duration = get(audioContent.duration), duration = get(audioContent.duration),
imagePath = get(audioContent.coverImage), imagePath = get(audioContent.coverImage),
price = get(audioContent.price)!!, price = get(audioContent.price)!!,
@@ -515,6 +546,24 @@ class DefaultCreatorChannelHomeQueryRepository(
) )
} }
private fun audioTitlesByContentIds(contentIds: List<Long>, locale: String): Map<Long, String> {
if (contentIds.isEmpty()) return emptyMap()
val contentTranslation = QContentTranslation("homeAudioContentTranslation")
return queryFactory
.select(contentTranslation.contentId, contentTranslation.renderedPayload)
.from(contentTranslation)
.where(
contentTranslation.contentId.`in`(contentIds),
contentTranslation.locale.eq(locale)
)
.fetch()
.mapNotNull {
val title = it.get(contentTranslation.renderedPayload)?.title.takeUnless(String?::isNullOrBlank)
title?.let { translatedTitle -> it.get(contentTranslation.contentId)!! to translatedTitle }
}
.toMap()
}
private fun orderStatesByContentIds( private fun orderStatesByContentIds(
viewerId: Long?, viewerId: Long?,
contentIds: List<Long>, contentIds: List<Long>,
@@ -542,17 +591,25 @@ class DefaultCreatorChannelHomeQueryRepository(
} }
} }
private fun audioSeriesByContentIds(contentIds: List<Long>): Map<Long, AudioSeriesSummary> { private fun audioSeriesByContentIds(contentIds: List<Long>, locale: String): Map<Long, AudioSeriesSummary> {
if (contentIds.isEmpty()) return emptyMap() if (contentIds.isEmpty()) return emptyMap()
val seriesTranslation = QSeriesTranslation("homeAudioSeriesTranslation")
return queryFactory return queryFactory
.select(seriesContent.content.id, series.title, series.isOriginal) .select(seriesContent.content.id, series.title, seriesTranslation.renderedPayload, series.isOriginal)
.from(seriesContent) .from(seriesContent)
.innerJoin(seriesContent.series, series) .innerJoin(seriesContent.series, series)
.leftJoin(seriesTranslation)
.on(
seriesTranslation.seriesId.eq(series.id),
seriesTranslation.locale.eq(locale)
)
.where(seriesContent.content.id.`in`(contentIds)) .where(seriesContent.content.id.`in`(contentIds))
.fetch() .fetch()
.associate { .associate {
val originalTitle = it.get(series.title)!!
val translatedTitle = it.get(seriesTranslation.renderedPayload)?.title
it.get(seriesContent.content.id)!! to AudioSeriesSummary( it.get(seriesContent.content.id)!! to AudioSeriesSummary(
title = it.get(series.title)!!, title = translatedTitle.takeUnless(String?::isNullOrBlank) ?: originalTitle,
isOriginal = it.get(series.isOriginal)!! isOriginal = it.get(series.isOriginal)!!
) )
} }
@@ -9,7 +9,11 @@ import kr.co.vividnext.sodalive.content.ContentType
import kr.co.vividnext.sodalive.content.order.Order import kr.co.vividnext.sodalive.content.order.Order
import kr.co.vividnext.sodalive.content.order.OrderType import kr.co.vividnext.sodalive.content.order.OrderType
import kr.co.vividnext.sodalive.content.pin.PinContent import kr.co.vividnext.sodalive.content.pin.PinContent
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslation
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationPayload
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme 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.Series
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesContent import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesContent
import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers
@@ -514,6 +518,84 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
assertTrue(records.last().isPointAvailable) assertTrue(records.last().isPointAvailable)
} }
@Test
@DisplayName("홈 오디오 제목과 시리즈명은 요청 locale 번역을 사용하고 공백 번역은 원문으로 fallback한다")
fun shouldFindHomeAudioContentsWithTranslatedTitleFallback() {
val now = LocalDateTime.of(2026, 6, 12, 12, 0)
val creator = saveMember("home-audio-i18n-creator", MemberRole.CREATOR)
val first = saveAudioContent(creator, now.minusDays(3), isAdult = false)
val listed = saveAudioContent(creator, now.minusDays(2), isAdult = false)
val latest = saveAudioContent(creator, now.minusDays(1), isAdult = false)
val series = saveSeries("home-origin-series", creator, isOriginal = true)
saveSeriesContent(series, listed)
saveContentTranslation(latest, "en", "Latest Audio")
saveContentTranslation(latest, "ja", "最新オーディオ")
saveContentTranslation(listed, "en", "Listed Audio")
saveContentTranslation(first, "en", " ")
saveSeriesTranslation(series, "en", "Home Series")
flushAndClear()
val latestRecord = repository.findLatestAudioContent(creator.id!!, now, false, locale = "en")
val records = repository.findAudioContents(
creator.id!!,
now,
latestRecord!!.audioContentId,
false,
locale = "en",
limit = 9
)
val jaLatestRecord = repository.findLatestAudioContent(creator.id!!, now, false, locale = "ja")
assertEquals("Latest Audio", latestRecord.title)
assertEquals("最新オーディオ", jaLatestRecord!!.title)
assertEquals(listOf(listed.id, first.id), records.map { it.audioContentId })
assertEquals("Listed Audio", records.first().title)
assertEquals("Home Series", records.first().seriesName)
assertEquals(first.title, records.last().title)
}
@Test
@DisplayName("홈 AUDIO 스케줄과 시리즈 제목은 요청 locale 번역을 사용하고 라이브 스케줄 제목은 유지한다")
fun shouldFindHomeSchedulesAndSeriesWithTranslatedTitleFallback() {
val now = LocalDateTime.of(2026, 6, 12, 12, 0)
val creator = saveMember("home-schedule-i18n-creator", MemberRole.CREATOR)
val liveSchedule = saveLiveRoom(creator, now.plusHours(1), channelName = null, isAdult = false)
val audioSchedule = saveAudioContent(creator, now.plusHours(2), isAdult = false)
val blankAudioSchedule = saveAudioContent(creator, now.plusHours(3), isAdult = false)
val series = saveSeries("home-origin-series-title", creator, isOriginal = true)
val blankSeries = saveSeries("home-blank-series-title", creator, isOriginal = true)
saveSeriesContent(series, saveAudioContent(creator, now.minusDays(1), isAdult = false))
saveSeriesContent(blankSeries, saveAudioContent(creator, now.minusDays(2), isAdult = false))
saveContentTranslation(audioSchedule, "en", "Scheduled Audio")
saveContentTranslation(audioSchedule, "ja", "予約オーディオ")
saveContentTranslation(blankAudioSchedule, "en", " ")
saveSeriesTranslation(series, "en", "Translated Home Series")
saveSeriesTranslation(series, "ja", "翻訳ホームシリーズ")
saveSeriesTranslation(blankSeries, "en", " ")
flushAndClear()
val schedules = repository.findSchedules(
creator.id!!,
now,
canViewAdultContent = false,
viewerId = null,
isViewerCreator = false,
effectiveViewerGender = null,
locale = "en",
limit = 10
)
val seriesRecords = repository.findSeries(creator.id!!, null, now, false, ContentType.ALL, locale = "en", limit = 8)
val jaSeriesRecords = repository.findSeries(creator.id!!, null, now, false, ContentType.ALL, locale = "ja", limit = 8)
assertEquals(listOf(liveSchedule.id, audioSchedule.id, blankAudioSchedule.id), schedules.map { it.targetId })
assertEquals(liveSchedule.title, schedules.first().title)
assertEquals("Scheduled Audio", schedules[1].title)
assertEquals(blankAudioSchedule.title, schedules.last().title)
assertEquals("Translated Home Series", seriesRecords.first { it.seriesId == series.id }.title)
assertEquals(blankSeries.title, seriesRecords.first { it.seriesId == blankSeries.id }.title)
assertEquals("翻訳ホームシリーズ", jaSeriesRecords.first { it.seriesId == series.id }.title)
}
@Test @Test
@DisplayName("홈 오디오 목록은 최신 오디오를 제외하고 활성 고정 콘텐츠를 최근 고정순으로 먼저 조회한다") @DisplayName("홈 오디오 목록은 최신 오디오를 제외하고 활성 고정 콘텐츠를 최근 고정순으로 먼저 조회한다")
fun shouldSortHomeAudioContentsByActivePinBeforeReleaseDate() { fun shouldSortHomeAudioContentsByActivePinBeforeReleaseDate() {
@@ -1215,6 +1297,40 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
return seriesContent return seriesContent
} }
private fun saveSeriesTranslation(series: Series, locale: String, title: String): SeriesTranslation {
val translation = SeriesTranslation(
seriesId = series.id!!,
locale = locale,
renderedPayload = SeriesTranslationPayload(title = title, introduction = "", keywords = emptyList())
)
entityManager.persist(translation)
entityManager.flush()
val payload = "{\"title\":\"$title\",\"introduction\":\"\",\"keywords\":[]}"
entityManager.createNativeQuery(
"update series_translation set rendered_payload = '$payload' format json where id = :id"
)
.setParameter("id", translation.id)
.executeUpdate()
return translation
}
private fun saveContentTranslation(content: AudioContent, locale: String, title: String): ContentTranslation {
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()
return translation
}
private fun savePinContent( private fun savePinContent(
creator: Member, creator: Member,
content: AudioContent, content: AudioContent,