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
@@ -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,