feat(i18n): 번역 작업 큐와 언어 감지 캐시를 도입한다
조회 중 외부 번역 호출을 줄이고 누락 번역을 비동기 job으로 처리한다.
This commit is contained in:
@@ -22,8 +22,6 @@ import kr.co.vividnext.sodalive.content.pin.PinContent
|
||||
import kr.co.vividnext.sodalive.content.pin.PinContentRepository
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository
|
||||
import kr.co.vividnext.sodalive.content.theme.translation.ContentThemeTranslationRepository
|
||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslation
|
||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslationPayload
|
||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
|
||||
import kr.co.vividnext.sodalive.content.translation.TranslatedContent
|
||||
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
|
||||
@@ -36,8 +34,7 @@ import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationEvent
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
|
||||
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
|
||||
import kr.co.vividnext.sodalive.i18n.translation.TranslateRequest
|
||||
import kr.co.vividnext.sodalive.i18n.translation.ResourceTranslationJobScheduler
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.isAdultVisibleByPolicy
|
||||
@@ -70,7 +67,7 @@ class AudioContentService(
|
||||
private val audioContentLikeRepository: AudioContentLikeRepository,
|
||||
private val pinContentRepository: PinContentRepository,
|
||||
|
||||
private val translationService: PapagoTranslationService,
|
||||
private val resourceTranslationJobScheduler: ResourceTranslationJobScheduler,
|
||||
private val contentTranslationRepository: ContentTranslationRepository,
|
||||
|
||||
private val s3Uploader: S3Uploader,
|
||||
@@ -770,7 +767,7 @@ class AudioContentService(
|
||||
* TranslatedContent로 가공한다
|
||||
*
|
||||
* 번역 콘텐츠가 없으면
|
||||
* 파파고 API를 통해 번역한 후 저장한다.
|
||||
* 누락 번역 job을 예약하고 원문 응답을 유지한다.
|
||||
*
|
||||
* 번역 대상: title, detail, tags
|
||||
*
|
||||
@@ -792,49 +789,11 @@ class AudioContentService(
|
||||
tags = payload.tags
|
||||
)
|
||||
} else {
|
||||
val texts = mutableListOf<String>()
|
||||
texts.add(audioContent.title)
|
||||
texts.add(audioContent.detail)
|
||||
texts.add(tag)
|
||||
|
||||
val sourceLanguage = audioContent.languageCode ?: "ko"
|
||||
|
||||
val response = translationService.translate(
|
||||
request = TranslateRequest(
|
||||
texts = texts,
|
||||
sourceLanguage = sourceLanguage,
|
||||
targetLanguage = langContext.lang.code
|
||||
)
|
||||
resourceTranslationJobScheduler.scheduleResourceTranslation(
|
||||
resourceType = LanguageTranslationTargetType.CONTENT,
|
||||
resourceId = audioContent.id!!,
|
||||
targetLanguage = langContext.lang.code
|
||||
)
|
||||
|
||||
val translatedTexts = response.translatedText
|
||||
if (translatedTexts.size == texts.size) {
|
||||
var index = 0
|
||||
|
||||
val translatedTitle = translatedTexts[index++]
|
||||
val translatedDetail = translatedTexts[index++]
|
||||
val translatedTags = translatedTexts[index]
|
||||
|
||||
val payload = ContentTranslationPayload(
|
||||
title = translatedTitle,
|
||||
detail = translatedDetail,
|
||||
tags = translatedTags
|
||||
)
|
||||
|
||||
contentTranslationRepository.save(
|
||||
ContentTranslation(
|
||||
contentId = audioContent.id!!,
|
||||
locale = langContext.lang.code,
|
||||
renderedPayload = payload
|
||||
)
|
||||
)
|
||||
|
||||
translated = TranslatedContent(
|
||||
title = translatedTitle,
|
||||
detail = translatedDetail,
|
||||
tags = translatedTags
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ class LanguageDetectListener(
|
||||
private val seriesRepository: ContentSeriesRepository,
|
||||
private val originalWorkRepository: OriginalWorkRepository,
|
||||
private val categoryRepository: CategoryRepository,
|
||||
private val languageDetectionCacheService: LanguageDetectionCacheService,
|
||||
|
||||
private val applicationEventPublisher: ApplicationEventPublisher,
|
||||
|
||||
@@ -116,7 +117,7 @@ class LanguageDetectListener(
|
||||
return
|
||||
}
|
||||
|
||||
val langCode = requestPapagoLanguageCode(event.query, characterId) ?: return
|
||||
val langCode = detectLanguageCode(event, characterId) ?: return
|
||||
|
||||
character.languageCode = langCode
|
||||
chatCharacterRepository.save(character)
|
||||
@@ -154,7 +155,7 @@ class LanguageDetectListener(
|
||||
return
|
||||
}
|
||||
|
||||
val langCode = requestPapagoLanguageCode(event.query, contentId) ?: return
|
||||
val langCode = detectLanguageCode(event, contentId) ?: return
|
||||
|
||||
audioContent.languageCode = langCode
|
||||
|
||||
@@ -194,7 +195,7 @@ class LanguageDetectListener(
|
||||
return
|
||||
}
|
||||
|
||||
val langCode = requestPapagoLanguageCode(event.query, commentId) ?: return
|
||||
val langCode = detectLanguageCode(event, commentId) ?: return
|
||||
|
||||
comment.languageCode = langCode
|
||||
audioContentCommentRepository.save(comment)
|
||||
@@ -226,7 +227,7 @@ class LanguageDetectListener(
|
||||
return
|
||||
}
|
||||
|
||||
val langCode = requestPapagoLanguageCode(event.query, commentId) ?: return
|
||||
val langCode = detectLanguageCode(event, commentId) ?: return
|
||||
|
||||
comment.languageCode = langCode
|
||||
characterCommentRepository.save(comment)
|
||||
@@ -257,7 +258,7 @@ class LanguageDetectListener(
|
||||
return
|
||||
}
|
||||
|
||||
val langCode = requestPapagoLanguageCode(event.query, cheersId) ?: return
|
||||
val langCode = detectLanguageCode(event, cheersId) ?: return
|
||||
|
||||
cheers.languageCode = langCode
|
||||
creatorCheersRepository.save(cheers)
|
||||
@@ -288,7 +289,7 @@ class LanguageDetectListener(
|
||||
return
|
||||
}
|
||||
|
||||
val langCode = requestPapagoLanguageCode(event.query, seriesId) ?: return
|
||||
val langCode = detectLanguageCode(event, seriesId) ?: return
|
||||
|
||||
series.languageCode = langCode
|
||||
seriesRepository.save(series)
|
||||
@@ -326,7 +327,7 @@ class LanguageDetectListener(
|
||||
return
|
||||
}
|
||||
|
||||
val langCode = requestPapagoLanguageCode(event.query, originalWorkId) ?: return
|
||||
val langCode = detectLanguageCode(event, originalWorkId) ?: return
|
||||
|
||||
originalWork.languageCode = langCode
|
||||
originalWorkRepository.save(originalWork)
|
||||
@@ -352,7 +353,7 @@ class LanguageDetectListener(
|
||||
val category = categoryRepository.findByIdOrNull(categoryId) ?: return
|
||||
if (!category.languageCode.isNullOrBlank()) return
|
||||
|
||||
val langCode = requestPapagoLanguageCode(event.query, categoryId) ?: return
|
||||
val langCode = detectLanguageCode(event, categoryId) ?: return
|
||||
|
||||
category.languageCode = langCode
|
||||
categoryRepository.save(category)
|
||||
@@ -365,6 +366,12 @@ class LanguageDetectListener(
|
||||
)
|
||||
}
|
||||
|
||||
private fun detectLanguageCode(event: LanguageDetectEvent, targetIdForLog: Long): String? {
|
||||
return languageDetectionCacheService.detectWithCache(event.query) {
|
||||
requestPapagoLanguageCode(event.query, targetIdForLog)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestPapagoLanguageCode(query: String, targetIdForLog: Long): String? {
|
||||
return try {
|
||||
val headers = HttpHeaders().apply {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
import kr.co.vividnext.sodalive.i18n.translation.SourceTextNormalizer
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Service
|
||||
class LanguageDetectionCacheService(
|
||||
private val languageDetectionResultRepository: LanguageDetectionResultRepository
|
||||
) {
|
||||
@Transactional
|
||||
fun detectWithCache(
|
||||
query: String,
|
||||
provider: String = DEFAULT_PROVIDER,
|
||||
detector: () -> String?
|
||||
): String? {
|
||||
val normalizedQuery = SourceTextNormalizer.normalize(query)
|
||||
if (normalizedQuery.isBlank()) return null
|
||||
|
||||
val sourceHash = SourceTextNormalizer.hash(normalizedQuery)
|
||||
val cached = languageDetectionResultRepository.findBySourceHashAndProviderAndNormalizationVersion(
|
||||
sourceHash = sourceHash,
|
||||
provider = provider,
|
||||
normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION
|
||||
)
|
||||
if (cached != null) return cached.detectedLanguage
|
||||
|
||||
val detectedLanguage = detector()?.takeIf { it.isNotBlank() } ?: return null
|
||||
languageDetectionResultRepository.save(
|
||||
LanguageDetectionResult(
|
||||
sourceHash = sourceHash,
|
||||
sourceTextSample = normalizedQuery.take(MAX_SAMPLE_LENGTH),
|
||||
detectedLanguage = detectedLanguage.lowercase(),
|
||||
provider = provider,
|
||||
confidence = null,
|
||||
normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION
|
||||
)
|
||||
)
|
||||
return detectedLanguage.lowercase()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_PROVIDER = "papago"
|
||||
private const val MAX_SAMPLE_LENGTH = 500
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||
import kr.co.vividnext.sodalive.i18n.translation.SourceTextNormalizer
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Table
|
||||
import javax.persistence.UniqueConstraint
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "language_detection_result",
|
||||
uniqueConstraints = [
|
||||
UniqueConstraint(
|
||||
name = "uk_language_detection_result_hash_provider_version",
|
||||
columnNames = ["source_hash", "provider", "normalization_version"]
|
||||
)
|
||||
]
|
||||
)
|
||||
class LanguageDetectionResult(
|
||||
@Column(name = "source_hash", nullable = false, length = 64)
|
||||
val sourceHash: String,
|
||||
|
||||
@Column(name = "source_text_sample", nullable = false, length = 500)
|
||||
val sourceTextSample: String,
|
||||
|
||||
@Column(name = "detected_language", nullable = false, length = 10)
|
||||
val detectedLanguage: String,
|
||||
|
||||
@Column(name = "provider", nullable = false, length = 50)
|
||||
val provider: String,
|
||||
|
||||
@Column(name = "confidence")
|
||||
val confidence: Double? = null,
|
||||
|
||||
@Column(name = "normalization_version", nullable = false, length = 20)
|
||||
val normalizationVersion: String = SourceTextNormalizer.NORMALIZATION_VERSION
|
||||
) : BaseEntity()
|
||||
@@ -0,0 +1,11 @@
|
||||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
interface LanguageDetectionResultRepository : JpaRepository<LanguageDetectionResult, Long> {
|
||||
fun findBySourceHashAndProviderAndNormalizationVersion(
|
||||
sourceHash: String,
|
||||
provider: String,
|
||||
normalizationVersion: String
|
||||
): LanguageDetectionResult?
|
||||
}
|
||||
@@ -7,8 +7,7 @@ import kr.co.vividnext.sodalive.content.LanguageDetectTargetType
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationEvent
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
|
||||
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
|
||||
import kr.co.vividnext.sodalive.i18n.translation.TranslateRequest
|
||||
import kr.co.vividnext.sodalive.i18n.translation.ResourceTranslationJobScheduler
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
@@ -25,7 +24,7 @@ class CategoryService(
|
||||
|
||||
private val langContext: LangContext,
|
||||
private val applicationEventPublisher: ApplicationEventPublisher,
|
||||
private val translationService: PapagoTranslationService
|
||||
private val resourceTranslationJobScheduler: ResourceTranslationJobScheduler
|
||||
) {
|
||||
@Transactional
|
||||
fun createCategory(request: CreateCategoryRequest, member: Member) {
|
||||
@@ -148,7 +147,7 @@ class CategoryService(
|
||||
.findByCategoryIdInAndLocale(categoryIds, locale)
|
||||
.associateBy { it.categoryId }
|
||||
|
||||
// 각 항목에 대해 번역 적용. 없으면 Papago로 번역 저장 후 적용
|
||||
// 각 항목에 대해 번역 적용. 없으면 누락 번역 job만 예약하고 원문을 반환한다.
|
||||
val result = mutableListOf<GetCategoryListResponse>()
|
||||
for (item in baseList) {
|
||||
val entity = entityMap[item.categoryId]
|
||||
@@ -165,38 +164,11 @@ class CategoryService(
|
||||
continue
|
||||
}
|
||||
|
||||
// 번역본이 없으면 Papago 번역 후 저장
|
||||
val texts = listOf(entity.title)
|
||||
val response = translationService.translate(
|
||||
request = TranslateRequest(
|
||||
texts = texts,
|
||||
sourceLanguage = sourceLang,
|
||||
targetLanguage = locale
|
||||
)
|
||||
resourceTranslationJobScheduler.scheduleResourceTranslation(
|
||||
resourceType = LanguageTranslationTargetType.CREATOR_CONTENT_CATEGORY,
|
||||
resourceId = entity.id!!,
|
||||
targetLanguage = locale
|
||||
)
|
||||
|
||||
val translatedTexts = response.translatedText
|
||||
if (translatedTexts.size == texts.size) {
|
||||
val translatedCategory = translatedTexts[0]
|
||||
|
||||
val existingOne = categoryTranslationRepository
|
||||
.findByCategoryIdAndLocale(entity.id!!, locale)
|
||||
if (existingOne == null) {
|
||||
categoryTranslationRepository.save(
|
||||
CategoryTranslation(
|
||||
categoryId = entity.id!!,
|
||||
locale = locale,
|
||||
category = translatedCategory
|
||||
)
|
||||
)
|
||||
} else {
|
||||
existingOne.category = translatedCategory
|
||||
categoryTranslationRepository.save(existingOne)
|
||||
}
|
||||
|
||||
result.add(GetCategoryListResponse(categoryId = item.categoryId, category = translatedCategory))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 번역이 필요 없거나 실패한 경우 원본 사용
|
||||
|
||||
@@ -7,8 +7,6 @@ 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.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
|
||||
@@ -17,8 +15,8 @@ import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesSortType
|
||||
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.i18n.translation.LanguageTranslationTargetType
|
||||
import kr.co.vividnext.sodalive.i18n.translation.ResourceTranslationJobScheduler
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.isAdultVisibleByPolicy
|
||||
@@ -41,7 +39,7 @@ class ContentSeriesService(
|
||||
private val seriesTranslationRepository: SeriesTranslationRepository,
|
||||
private val seriesGenreTranslationRepository: SeriesGenreTranslationRepository,
|
||||
private val contentTranslationRepository: ContentTranslationRepository,
|
||||
private val translationService: PapagoTranslationService,
|
||||
private val resourceTranslationJobScheduler: ResourceTranslationJobScheduler,
|
||||
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val coverImageHost: String
|
||||
@@ -91,7 +89,7 @@ class ContentSeriesService(
|
||||
fun getGenreList(memberId: Long, isAdult: Boolean, contentType: ContentType): List<GetSeriesGenreListResponse> {
|
||||
/**
|
||||
* langContext.lang == Lang.EN || Lang.JA 일 때 번역된 콘텐츠 테마 반환
|
||||
* 번역이 없으면 번역 API 호출 후 저장하고 반환
|
||||
* 번역이 없으면 누락 번역 job만 예약하고 원문을 반환
|
||||
*/
|
||||
val genres = repository.getGenreList(memberId = memberId, isAdult = isAdult, contentType = contentType)
|
||||
|
||||
@@ -120,32 +118,12 @@ class ContentSeriesService(
|
||||
|
||||
// 미번역 항목 수집
|
||||
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
|
||||
)
|
||||
untranslated.forEach { item ->
|
||||
resourceTranslationJobScheduler.scheduleResourceTranslation(
|
||||
resourceType = LanguageTranslationTargetType.SERIES_GENRE,
|
||||
resourceId = item.id,
|
||||
targetLanguage = targetLocale
|
||||
)
|
||||
|
||||
val translatedTexts = response.translatedText
|
||||
val toSave = mutableListOf<kr.co.vividnext.sodalive.content.series.translation.SeriesGenreTranslation>()
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
// 원래 순서 보존하여 결과 조립
|
||||
@@ -283,7 +261,7 @@ class ContentSeriesService(
|
||||
* TranslatedSeries로 가공한다
|
||||
*
|
||||
* 번역 콘텐츠가 없으면
|
||||
* 파파고 API를 통해 번역한 후 저장한다.
|
||||
* 누락 번역 job을 예약하고 원문 응답을 유지한다.
|
||||
*
|
||||
* 번역 대상: title, introduction, keywordList
|
||||
*
|
||||
@@ -309,54 +287,11 @@ class ContentSeriesService(
|
||||
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
|
||||
)
|
||||
resourceTranslationJobScheduler.scheduleResourceTranslation(
|
||||
resourceType = LanguageTranslationTargetType.SERIES,
|
||||
resourceId = seriesId,
|
||||
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 = SeriesTranslationPayload(
|
||||
title = translatedTitle,
|
||||
introduction = translatedIntroduction,
|
||||
keywords = translatedKeywords
|
||||
)
|
||||
|
||||
seriesTranslationRepository.save(
|
||||
SeriesTranslation(
|
||||
seriesId = seriesId,
|
||||
locale = locale,
|
||||
renderedPayload = payload
|
||||
)
|
||||
)
|
||||
|
||||
val kws = translatedKeywords.ifEmpty { keywordList }
|
||||
translated = TranslatedSeries(
|
||||
title = translatedTitle,
|
||||
introduction = translatedIntroduction,
|
||||
keywords = kws
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,11 @@ import kr.co.vividnext.sodalive.content.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.content.ContentType
|
||||
import kr.co.vividnext.sodalive.content.SortType
|
||||
import kr.co.vividnext.sodalive.content.theme.content.GetContentByThemeResponse
|
||||
import kr.co.vividnext.sodalive.content.theme.translation.ContentThemeTranslation
|
||||
import kr.co.vividnext.sodalive.content.theme.translation.ContentThemeTranslationRepository
|
||||
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.i18n.translation.LanguageTranslationTargetType
|
||||
import kr.co.vividnext.sodalive.i18n.translation.ResourceTranslationJobScheduler
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.isAdultVisibleByPolicy
|
||||
import org.springframework.stereotype.Service
|
||||
@@ -22,7 +21,7 @@ class AudioContentThemeService(
|
||||
private val contentRepository: AudioContentRepository,
|
||||
private val contentThemeTranslationRepository: ContentThemeTranslationRepository,
|
||||
|
||||
private val papagoTranslationService: PapagoTranslationService,
|
||||
private val resourceTranslationJobScheduler: ResourceTranslationJobScheduler,
|
||||
private val langContext: LangContext
|
||||
) {
|
||||
@Transactional(readOnly = true)
|
||||
@@ -51,7 +50,7 @@ class AudioContentThemeService(
|
||||
|
||||
/**
|
||||
* langContext.lang == Lang.EN || Lang.JA 일 때 번역된 콘텐츠 테마 반환
|
||||
* 번역이 없으면 번역 API 호출 후 저장하고 반환
|
||||
* 번역이 없으면 누락 번역 job만 예약하고 원문을 반환
|
||||
*/
|
||||
val currentLang = langContext.lang
|
||||
if (currentLang == Lang.EN || currentLang == Lang.JA) {
|
||||
@@ -66,43 +65,14 @@ class AudioContentThemeService(
|
||||
|
||||
val existingMap = existingTranslations.associateBy { it.contentThemeId }
|
||||
|
||||
// 2) 미번역 항목만 수집하여 한 번에 번역 요청
|
||||
// 2) 미번역 항목은 조회 스레드에서 번역하지 않고 job만 예약
|
||||
val untranslatedPairs = themesWithIds.filter { existingMap[it.id] == null }
|
||||
|
||||
if (untranslatedPairs.isNotEmpty()) {
|
||||
val texts = untranslatedPairs.map { it.theme }
|
||||
|
||||
val response = papagoTranslationService.translate(
|
||||
request = TranslateRequest(
|
||||
texts = texts,
|
||||
sourceLanguage = "ko",
|
||||
targetLanguage = targetLocale
|
||||
)
|
||||
untranslatedPairs.forEach { pair ->
|
||||
resourceTranslationJobScheduler.scheduleResourceTranslation(
|
||||
resourceType = LanguageTranslationTargetType.CONTENT_THEME,
|
||||
resourceId = pair.id,
|
||||
targetLanguage = targetLocale
|
||||
)
|
||||
|
||||
val translatedTexts = response.translatedText
|
||||
val entitiesToSave = mutableListOf<ContentThemeTranslation>()
|
||||
|
||||
// translatedTexts 크기가 다르면 안전하게 원문으로 대체
|
||||
untranslatedPairs.forEachIndexed { index, pair ->
|
||||
val translated = translatedTexts.getOrNull(index) ?: pair.theme
|
||||
entitiesToSave.add(
|
||||
ContentThemeTranslation(
|
||||
contentThemeId = pair.id,
|
||||
locale = targetLocale,
|
||||
theme = translated
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (entitiesToSave.isNotEmpty()) {
|
||||
contentThemeTranslationRepository.saveAll(entitiesToSave)
|
||||
}
|
||||
|
||||
// 저장 후 맵을 갱신
|
||||
entitiesToSave.forEach { entity ->
|
||||
(existingMap as MutableMap)[entity.contentThemeId] = entity
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 원래 순서대로 결과 조립 (번역 없으면 원문 fallback)
|
||||
|
||||
Reference in New Issue
Block a user