시리즈 상세 - 번역 데이터 조회 기능 추가

This commit is contained in:
2025-12-16 02:51:36 +09:00
parent 0eed29eadc
commit 4c0be733d0
3 changed files with 133 additions and 2 deletions

View File

@@ -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<String>()
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
)
}

View File

@@ -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<String>,
val publishedDaysOfWeek: String,
val contentList: List<GetSeriesContentListItem>,
val contentCount: Int
val contentCount: Int,
val translated: TranslatedSeries?,
val translatedGenre: String?
) {
data class GetSeriesDetailCreator(
val creatorId: Long,

View File

@@ -71,3 +71,9 @@ class SeriesTranslationPayloadConverter : AttributeConverter<SeriesTranslationPa
private val objectMapper = jacksonObjectMapper()
}
}
data class TranslatedSeries(
val title: String,
val introduction: String,
val keywords: List<String>
)