시리즈 상세 - 번역 데이터 조회 기능 추가
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user