HomeService fetchData 리팩토링 및 DB JOIN 기반 번역 적용

fetchData 함수에서 별도로 수행하던 번역 데이터 조회를 DB JOIN 및
COALESCE를 사용하도록 개선하여 성능을 최적화함.

- AudioContentRepository, RankingRepository 등에 locale 파라미터 추가
- DB 레벨에서 번역된 제목을 조회하도록 쿼리 수정
- HomeService에서 불필요한 getTranslatedContentList 호출 제거
This commit is contained in:
2026-02-13 10:37:06 +09:00
parent 46b0989795
commit 341f24c643
7 changed files with 102 additions and 138 deletions

View File

@@ -25,6 +25,7 @@ import kr.co.vividnext.sodalive.content.pin.QPinContent.pinContent
import kr.co.vividnext.sodalive.content.playlist.AudioContentPlaylistContent
import kr.co.vividnext.sodalive.content.playlist.QAudioContentPlaylistContent
import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme
import kr.co.vividnext.sodalive.content.translation.QContentTranslation.contentTranslation
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.QEvent.event
@@ -188,7 +189,8 @@ interface AudioContentQueryRepository {
isAdult: Boolean,
orderByRandom: Boolean = false,
isPointAvailableOnly: Boolean = false,
excludeContentIds: List<Long> = emptyList()
excludeContentIds: List<Long> = emptyList(),
locale: String? = null
): List<AudioContentMainItem>
fun findContentByCurationId(
@@ -1331,7 +1333,8 @@ class AudioContentQueryRepositoryImpl(
isAdult: Boolean,
orderByRandom: Boolean,
isPointAvailableOnly: Boolean,
excludeContentIds: List<Long>
excludeContentIds: List<Long>,
locale: String?
): List<AudioContentMainItem> {
val blockMemberCondition = if (memberId != null) {
blockMember.member.id.eq(member.id)
@@ -1382,12 +1385,27 @@ class AudioContentQueryRepositoryImpl(
where = where.and(audioContent.id.notIn(excludeContentIds))
}
val titleExpression = if (locale != null) {
val translatedTitle = Expressions.stringTemplate(
"JSON_EXTRACT({0}, '$.title')",
contentTranslation.renderedPayload
)
val coalesceTitle = Expressions.stringTemplate(
"COALESCE(NULLIF({0}, ''), {1})",
translatedTitle,
audioContent.title
)
coalesceTitle
} else {
audioContent.title
}
var select = queryFactory
.select(
QAudioContentMainItem(
audioContent.id,
member.id,
audioContent.title,
titleExpression,
audioContent.coverImage.prepend("/").prepend(imageHost),
member.nickname,
audioContent.isPointAvailable
@@ -1397,6 +1415,11 @@ class AudioContentQueryRepositoryImpl(
.innerJoin(audioContent.member, member)
.innerJoin(audioContent.theme, audioContentTheme)
if (locale != null) {
select = select.leftJoin(contentTranslation)
.on(contentTranslation.contentId.eq(audioContent.id).and(contentTranslation.locale.eq(locale)))
}
if (memberId != null) {
where = where.and(blockMember.id.isNull)
select = select.leftJoin(blockMember).on(blockMemberCondition)

View File

@@ -1222,7 +1222,7 @@ class AudioContentService(
isPointAvailableOnly = isPointAvailableOnly
)
val contentList = repository.getLatestContentByTheme(
return repository.getLatestContentByTheme(
memberId = memberId,
theme = normalizedTheme,
contentType = contentType,
@@ -1233,26 +1233,9 @@ class AudioContentService(
isAdult = isAdult,
orderByRandom = orderByRandom,
isPointAvailableOnly = isPointAvailableOnly,
excludeContentIds = excludeContentIds
excludeContentIds = excludeContentIds,
locale = langContext.lang.code
)
val contentIds = contentList.map { it.contentId }
return if (contentIds.isNotEmpty()) {
val translations = contentTranslationRepository
.findByContentIdInAndLocale(contentIds = contentIds, locale = langContext.lang.code)
.associateBy { it.contentId }
contentList.map { item ->
val translatedTitle = translations[item.contentId]?.renderedPayload?.title
if (translatedTitle.isNullOrBlank()) {
item
} else {
item.copy(title = translatedTitle)
}
}
} else {
contentList
}
}
/**