From 4c0be733d01e5568d5bf6dfc4781dbf707a5f0cd Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 16 Dec 2025 02:51:36 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=9C=EB=A6=AC=EC=A6=88=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20-=20=EB=B2=88=EC=97=AD=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/series/ContentSeriesService.kt | 123 +++++++++++++++++- .../content/series/GetSeriesDetailResponse.kt | 6 +- .../series/translation/SeriesTranslation.kt | 6 + 3 files changed, 133 insertions(+), 2 deletions(-) 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 f6c8c26..83e9a0b 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 @@ -6,7 +6,9 @@ import kr.co.vividnext.sodalive.content.order.OrderRepository import kr.co.vividnext.sodalive.content.order.OrderType import kr.co.vividnext.sodalive.content.series.content.ContentSeriesContentRepository import kr.co.vividnext.sodalive.content.series.content.GetSeriesContentListResponse +import kr.co.vividnext.sodalive.content.series.translation.SeriesGenreTranslationRepository import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationRepository +import kr.co.vividnext.sodalive.content.series.translation.TranslatedSeries 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 @@ -14,10 +16,13 @@ import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesState import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.i18n.LangContext +import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService +import kr.co.vividnext.sodalive.i18n.translation.TranslateRequest import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -29,8 +34,12 @@ class ContentSeriesService( private val blockMemberRepository: BlockMemberRepository, private val explorerQueryRepository: ExplorerQueryRepository, private val seriesContentRepository: ContentSeriesContentRepository, + private val langContext: LangContext, + private val seriesTranslationRepository: SeriesTranslationRepository, + private val seriesGenreTranslationRepository: SeriesGenreTranslationRepository, + private val translationService: PapagoTranslationService, @Value("\${cloud.aws.cloud-front.host}") private val coverImageHost: String @@ -120,6 +129,7 @@ class ContentSeriesService( return GetSeriesListResponse(totalCount, items) } + @Transactional fun getSeriesDetail( seriesId: Long, isAdultContentVisible: Boolean, @@ -161,7 +171,115 @@ class ContentSeriesService( limit = 5 ) + /** + * series.languageCode != null && series.languageCode != languageCode + * + * 번역 시리즈를 조회한다. - series, locale + * 번역 콘텐츠가 있으면 + * TranslatedSeries로 가공한다 + * + * 번역 콘텐츠가 없으면 + * 파파고 API를 통해 번역한 후 저장한다. + * + * 번역 대상: title, introduction, keywordList + * + * 파파고로 번역한 데이터를 TranslatedSeries 가공한다 + */ + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd") + + // 요청된 언어(locale)에 대한 시리즈 번역을 조회하거나, 없으면 동기 번역 후 저장한다. + var translated: TranslatedSeries? = null + run { + val locale = langContext.lang.code + val languageCode = series.languageCode + // 원본 언어가 존재하고, 요청 언어와 다를 때만 번역 처리 + if (!languageCode.isNullOrBlank() && languageCode != locale) { + val existing = seriesTranslationRepository.findBySeriesIdAndLocale(seriesId = seriesId, locale = locale) + if (existing != null) { + val payload = existing.renderedPayload + val kws = payload.keywords.ifEmpty { keywordList } + translated = TranslatedSeries( + title = payload.title, + introduction = payload.introduction, + keywords = kws + ) + } else { + val texts = mutableListOf() + texts.add(series.title) + texts.add(series.introduction) + // 키워드는 개별 항목으로 번역 요청하여 N회 호출을 방지한다. + val keywordListForTranslate = keywordList + texts.addAll(keywordListForTranslate) + + val response = translationService.translate( + request = TranslateRequest( + texts = texts, + sourceLanguage = languageCode, + targetLanguage = locale + ) + ) + + val translatedTexts = response.translatedText + if (translatedTexts.size == texts.size) { + var index = 0 + val translatedTitle = translatedTexts[index++] + val translatedIntroduction = translatedTexts[index++] + val translatedKeywords = if (keywordListForTranslate.isNotEmpty()) { + translatedTexts.subList(index, translatedTexts.size) + } else { + // 번역할 키워드가 없으면 원본 키워드 반환 정책 적용 + keywordList + } + + val payload = kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationPayload( + title = translatedTitle, + introduction = translatedIntroduction, + keywords = translatedKeywords + ) + + seriesTranslationRepository.save( + kr.co.vividnext.sodalive.content.series.translation.SeriesTranslation( + seriesId = seriesId, + locale = locale, + renderedPayload = payload + ) + ) + + val kws = translatedKeywords.ifEmpty { keywordList } + translated = TranslatedSeries( + title = translatedTitle, + introduction = translatedIntroduction, + keywords = kws + ) + } + } + } + } + + // 장르 번역 조회 (있으면 반환) + val translatedGenre: String? = run { + val genreId = series.genre?.id + if (genreId != null) { + val locale = langContext.lang.code + val found = seriesGenreTranslationRepository.findBySeriesGenreIdAndLocale(genreId, locale) + val text = found?.genre + if (!text.isNullOrBlank()) { + text + } else { + null + } + } else { + null + } + } + + // publishedDateUtc는 ISO8601(Z 포함)로 반환 + val publishedDateUtc = series.createdAt!! + .atZone(ZoneId.of("UTC")) + .toInstant() + .toString() + return GetSeriesDetailResponse( seriesId = seriesId, title = series.title, @@ -176,6 +294,7 @@ class ContentSeriesService( .withZoneSameInstant(ZoneId.of("Asia/Seoul")) .toLocalDateTime() .format(dateTimeFormatter), + publishedDateUtc = publishedDateUtc, creator = GetSeriesDetailResponse.GetSeriesDetailCreator( creatorId = series.member!!.id!!, nickname = series.member!!.nickname, @@ -191,7 +310,9 @@ class ContentSeriesService( keywordList = keywordList, publishedDaysOfWeek = publishedDaysOfWeekText(series.publishedDaysOfWeek), contentList = seriesContentList.items, - contentCount = seriesContentList.totalCount + contentCount = seriesContentList.totalCount, + translated = translated, + translatedGenre = translatedGenre ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesDetailResponse.kt index 7b9daac..30536ad 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesDetailResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesDetailResponse.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.content.series import kr.co.vividnext.sodalive.content.series.content.GetSeriesContentListItem +import kr.co.vividnext.sodalive.content.series.translation.TranslatedSeries data class GetSeriesDetailResponse( val seriesId: Long, @@ -12,6 +13,7 @@ data class GetSeriesDetailResponse( val writer: String?, val studio: String?, val publishedDate: String, + val publishedDateUtc: String, val creator: GetSeriesDetailCreator, var rentalMinPrice: Int, var rentalMaxPrice: Int, @@ -21,7 +23,9 @@ data class GetSeriesDetailResponse( val keywordList: List, val publishedDaysOfWeek: String, val contentList: List, - val contentCount: Int + val contentCount: Int, + val translated: TranslatedSeries?, + val translatedGenre: String? ) { data class GetSeriesDetailCreator( val creatorId: Long, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/translation/SeriesTranslation.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/translation/SeriesTranslation.kt index c661d48..cc346b2 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/translation/SeriesTranslation.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/translation/SeriesTranslation.kt @@ -71,3 +71,9 @@ class SeriesTranslationPayloadConverter : AttributeConverter +)