시리즈 상세 - 번역 데이터 조회 기능 추가
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.order.OrderType
|
||||||
import kr.co.vividnext.sodalive.content.series.content.ContentSeriesContentRepository
|
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.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.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.Series
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
|
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesSortType
|
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.explorer.ExplorerQueryRepository
|
||||||
import kr.co.vividnext.sodalive.i18n.Lang
|
import kr.co.vividnext.sodalive.i18n.Lang
|
||||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
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.Member
|
||||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
@@ -29,8 +34,12 @@ class ContentSeriesService(
|
|||||||
private val blockMemberRepository: BlockMemberRepository,
|
private val blockMemberRepository: BlockMemberRepository,
|
||||||
private val explorerQueryRepository: ExplorerQueryRepository,
|
private val explorerQueryRepository: ExplorerQueryRepository,
|
||||||
private val seriesContentRepository: ContentSeriesContentRepository,
|
private val seriesContentRepository: ContentSeriesContentRepository,
|
||||||
|
|
||||||
private val langContext: LangContext,
|
private val langContext: LangContext,
|
||||||
|
|
||||||
private val seriesTranslationRepository: SeriesTranslationRepository,
|
private val seriesTranslationRepository: SeriesTranslationRepository,
|
||||||
|
private val seriesGenreTranslationRepository: SeriesGenreTranslationRepository,
|
||||||
|
private val translationService: PapagoTranslationService,
|
||||||
|
|
||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
private val coverImageHost: String
|
private val coverImageHost: String
|
||||||
@@ -120,6 +129,7 @@ class ContentSeriesService(
|
|||||||
return GetSeriesListResponse(totalCount, items)
|
return GetSeriesListResponse(totalCount, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
fun getSeriesDetail(
|
fun getSeriesDetail(
|
||||||
seriesId: Long,
|
seriesId: Long,
|
||||||
isAdultContentVisible: Boolean,
|
isAdultContentVisible: Boolean,
|
||||||
@@ -161,7 +171,115 @@ class ContentSeriesService(
|
|||||||
limit = 5
|
limit = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* series.languageCode != null && series.languageCode != languageCode
|
||||||
|
*
|
||||||
|
* 번역 시리즈를 조회한다. - series, locale
|
||||||
|
* 번역 콘텐츠가 있으면
|
||||||
|
* TranslatedSeries로 가공한다
|
||||||
|
*
|
||||||
|
* 번역 콘텐츠가 없으면
|
||||||
|
* 파파고 API를 통해 번역한 후 저장한다.
|
||||||
|
*
|
||||||
|
* 번역 대상: title, introduction, keywordList
|
||||||
|
*
|
||||||
|
* 파파고로 번역한 데이터를 TranslatedSeries 가공한다
|
||||||
|
*/
|
||||||
|
|
||||||
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd")
|
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(
|
return GetSeriesDetailResponse(
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
title = series.title,
|
title = series.title,
|
||||||
@@ -176,6 +294,7 @@ class ContentSeriesService(
|
|||||||
.withZoneSameInstant(ZoneId.of("Asia/Seoul"))
|
.withZoneSameInstant(ZoneId.of("Asia/Seoul"))
|
||||||
.toLocalDateTime()
|
.toLocalDateTime()
|
||||||
.format(dateTimeFormatter),
|
.format(dateTimeFormatter),
|
||||||
|
publishedDateUtc = publishedDateUtc,
|
||||||
creator = GetSeriesDetailResponse.GetSeriesDetailCreator(
|
creator = GetSeriesDetailResponse.GetSeriesDetailCreator(
|
||||||
creatorId = series.member!!.id!!,
|
creatorId = series.member!!.id!!,
|
||||||
nickname = series.member!!.nickname,
|
nickname = series.member!!.nickname,
|
||||||
@@ -191,7 +310,9 @@ class ContentSeriesService(
|
|||||||
keywordList = keywordList,
|
keywordList = keywordList,
|
||||||
publishedDaysOfWeek = publishedDaysOfWeekText(series.publishedDaysOfWeek),
|
publishedDaysOfWeek = publishedDaysOfWeekText(series.publishedDaysOfWeek),
|
||||||
contentList = seriesContentList.items,
|
contentList = seriesContentList.items,
|
||||||
contentCount = seriesContentList.totalCount
|
contentCount = seriesContentList.totalCount,
|
||||||
|
translated = translated,
|
||||||
|
translatedGenre = translatedGenre
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package kr.co.vividnext.sodalive.content.series
|
package kr.co.vividnext.sodalive.content.series
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.content.series.content.GetSeriesContentListItem
|
import kr.co.vividnext.sodalive.content.series.content.GetSeriesContentListItem
|
||||||
|
import kr.co.vividnext.sodalive.content.series.translation.TranslatedSeries
|
||||||
|
|
||||||
data class GetSeriesDetailResponse(
|
data class GetSeriesDetailResponse(
|
||||||
val seriesId: Long,
|
val seriesId: Long,
|
||||||
@@ -12,6 +13,7 @@ data class GetSeriesDetailResponse(
|
|||||||
val writer: String?,
|
val writer: String?,
|
||||||
val studio: String?,
|
val studio: String?,
|
||||||
val publishedDate: String,
|
val publishedDate: String,
|
||||||
|
val publishedDateUtc: String,
|
||||||
val creator: GetSeriesDetailCreator,
|
val creator: GetSeriesDetailCreator,
|
||||||
var rentalMinPrice: Int,
|
var rentalMinPrice: Int,
|
||||||
var rentalMaxPrice: Int,
|
var rentalMaxPrice: Int,
|
||||||
@@ -21,7 +23,9 @@ data class GetSeriesDetailResponse(
|
|||||||
val keywordList: List<String>,
|
val keywordList: List<String>,
|
||||||
val publishedDaysOfWeek: String,
|
val publishedDaysOfWeek: String,
|
||||||
val contentList: List<GetSeriesContentListItem>,
|
val contentList: List<GetSeriesContentListItem>,
|
||||||
val contentCount: Int
|
val contentCount: Int,
|
||||||
|
val translated: TranslatedSeries?,
|
||||||
|
val translatedGenre: String?
|
||||||
) {
|
) {
|
||||||
data class GetSeriesDetailCreator(
|
data class GetSeriesDetailCreator(
|
||||||
val creatorId: Long,
|
val creatorId: Long,
|
||||||
|
|||||||
@@ -71,3 +71,9 @@ class SeriesTranslationPayloadConverter : AttributeConverter<SeriesTranslationPa
|
|||||||
private val objectMapper = jacksonObjectMapper()
|
private val objectMapper = jacksonObjectMapper()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class TranslatedSeries(
|
||||||
|
val title: String,
|
||||||
|
val introduction: String,
|
||||||
|
val keywords: List<String>
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user