diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesService.kt index 8794eba..1f8dba5 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesService.kt @@ -11,6 +11,7 @@ import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslation import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationPayload import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationRepository import kr.co.vividnext.sodalive.content.series.translation.TranslatedSeries +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.SeriesPublishedDaysOfWeek import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesSortType @@ -41,6 +42,7 @@ class ContentSeriesService( private val seriesTranslationRepository: SeriesTranslationRepository, private val seriesGenreTranslationRepository: SeriesGenreTranslationRepository, + private val contentTranslationRepository: ContentTranslationRepository, private val translationService: PapagoTranslationService, @Value("\${cloud.aws.cloud-front.host}") @@ -58,11 +60,77 @@ class ContentSeriesService( limit: Long = 20 ): List { val originalAudioDramaList = repository.getOriginalAudioDramaList(isAdult, contentType, orderByRandom, offset, limit) - return seriesToSeriesListItem(originalAudioDramaList, isAdult, contentType) + return getTranslatedSeriesList(seriesToSeriesListItem(originalAudioDramaList, isAdult, contentType)) } fun getGenreList(memberId: Long, isAdult: Boolean, contentType: ContentType): List { - return repository.getGenreList(memberId = memberId, isAdult = isAdult, contentType = contentType) + /** + * langContext.lang == Lang.EN || Lang.JA 일 때 번역된 콘텐츠 테마 반환 + * 번역이 없으면 번역 API 호출 후 저장하고 반환 + */ + val genres = repository.getGenreList(memberId = memberId, isAdult = isAdult, contentType = contentType) + + val currentLang = langContext.lang + if (currentLang == Lang.EN || currentLang == Lang.JA) { + val targetLocale = currentLang.code + val ids = genres.map { it.id } + + // 기존 번역 일괄 조회 + val existing = if (ids.isNotEmpty()) { + // 메서드가 없을 경우 개별 조회로 대체되지만, 성능을 위해 우선 시도 + try { + seriesGenreTranslationRepository + .findBySeriesGenreIdInAndLocale(ids, targetLocale) + } catch (_: Exception) { + // Spring Data 메서드 미존재 시 안전하게 개별 조회로 폴백 + ids.mapNotNull { id -> + seriesGenreTranslationRepository.findBySeriesGenreIdAndLocale(id, targetLocale) + } + } + } else { + emptyList() + } + + val existingMap = existing.associateBy { it.seriesGenreId }.toMutableMap() + + // 미번역 항목 수집 + val untranslated = genres.filter { existingMap[it.id] == null } + if (untranslated.isNotEmpty()) { + val texts = untranslated.map { it.genre } + val response = translationService.translate( + request = TranslateRequest( + texts = texts, + sourceLanguage = "ko", + targetLanguage = targetLocale + ) + ) + + val translatedTexts = response.translatedText + val toSave = mutableListOf() + untranslated.forEachIndexed { index, item -> + val translated = translatedTexts.getOrNull(index) ?: item.genre + toSave.add( + kr.co.vividnext.sodalive.content.series.translation.SeriesGenreTranslation( + seriesGenreId = item.id, + locale = targetLocale, + genre = translated + ) + ) + } + if (toSave.isNotEmpty()) { + seriesGenreTranslationRepository.saveAll(toSave) + toSave.forEach { saved -> existingMap[saved.seriesGenreId] = saved } + } + } + + // 원래 순서 보존하여 결과 조립 + return genres.map { g -> + val translated = existingMap[g.id]?.genre ?: g.genre + GetSeriesGenreListResponse(id = g.id, genre = translated) + } + } + + return genres } fun getSeriesList( @@ -128,7 +196,7 @@ class ContentSeriesService( ).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) } val items = seriesToSeriesListItem(seriesList = rawItems, isAdult = isAuth, contentType = contentType) - return GetSeriesListResponse(totalCount, items) + return GetSeriesListResponse(totalCount, getTranslatedSeriesList(items)) } @Transactional @@ -356,7 +424,33 @@ class ContentSeriesService( it } - return GetSeriesContentListResponse(totalCount, contentList) + /** + * 콘텐츠 리스트의 제목을 현재 언어(locale)에 맞춰 일괄 번역한다. + * + * 처리 절차: + * - 입력된 콘텐츠들의 contentId 집합을 만들고, 요청 언어 코드(langContext.lang.code)로 + * contentTranslationRepository에서 번역 데이터를 한 번에 조회한다. + * - 각 항목에 대해 번역된 제목이 존재하고 비어있지 않으면 title만 번역 값으로 교체한다. + * - 번역이 없거나 공백이면 원본 항목을 그대로 반환한다. + * + * 성능: + * - N건의 항목을 1회의 조회로 해결하기 위해 IN 쿼리를 사용한다. + */ + val contentIds = contentList.map { it.contentId } + val translatedItems = 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 + } + + return GetSeriesContentListResponse(totalCount, translatedItems) } fun getRecommendSeriesList( @@ -371,7 +465,13 @@ class ContentSeriesService( limit = 20 ).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) } - return seriesToSeriesListItem(seriesList = seriesList, isAdult = isAuth, contentType = contentType) + return getTranslatedSeriesList( + seriesToSeriesListItem( + seriesList = seriesList, + isAdult = isAuth, + contentType = contentType + ) + ) } fun fetchSeriesByCurationId( @@ -386,7 +486,7 @@ class ContentSeriesService( isAdult = isAdult, contentType = contentType ) - return seriesToSeriesListItem(seriesList, isAdult, contentType) + return getTranslatedSeriesList(seriesToSeriesListItem(seriesList, isAdult, contentType)) } fun getDayOfWeekSeriesList( @@ -416,7 +516,7 @@ class ContentSeriesService( seriesList } - return seriesToSeriesListItem(seriesList, isAdult, contentType) + return getTranslatedSeriesList(seriesToSeriesListItem(seriesList, isAdult, contentType)) } private fun seriesToSeriesListItem( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/translation/SeriesGenreTranslationRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/translation/SeriesGenreTranslationRepository.kt index e88589b..3e48736 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/translation/SeriesGenreTranslationRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/translation/SeriesGenreTranslationRepository.kt @@ -4,4 +4,6 @@ import org.springframework.data.jpa.repository.JpaRepository interface SeriesGenreTranslationRepository : JpaRepository { fun findBySeriesGenreIdAndLocale(seriesGenreId: Long, locale: String): SeriesGenreTranslation? + + fun findBySeriesGenreIdInAndLocale(seriesGenreIds: List, locale: String): List }