원작 등록/수정시 번역 API 호출
This commit is contained in:
@@ -10,6 +10,11 @@ import kr.co.vividnext.sodalive.chat.original.OriginalWorkTag
|
||||
import kr.co.vividnext.sodalive.chat.original.OriginalWorkTagMapping
|
||||
import kr.co.vividnext.sodalive.chat.original.repository.OriginalWorkTagRepository
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.content.LanguageDetectEvent
|
||||
import kr.co.vividnext.sodalive.content.LanguageDetectTargetType
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationEvent
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.data.domain.Sort
|
||||
@@ -24,7 +29,9 @@ import org.springframework.transaction.annotation.Transactional
|
||||
class AdminOriginalWorkService(
|
||||
private val originalWorkRepository: OriginalWorkRepository,
|
||||
private val chatCharacterRepository: ChatCharacterRepository,
|
||||
private val originalWorkTagRepository: OriginalWorkTagRepository
|
||||
private val originalWorkTagRepository: OriginalWorkTagRepository,
|
||||
|
||||
private val applicationEventPublisher: ApplicationEventPublisher
|
||||
) {
|
||||
|
||||
/** 원작 등록 (중복 제목 방지 포함) */
|
||||
@@ -56,7 +63,44 @@ class AdminOriginalWorkService(
|
||||
entity.tagMappings.add(OriginalWorkTagMapping(originalWork = entity, tag = tagEntity))
|
||||
}
|
||||
}
|
||||
return originalWorkRepository.save(entity)
|
||||
|
||||
val originalWork = originalWorkRepository.save(entity)
|
||||
|
||||
/**
|
||||
* 저장이 완료된 후
|
||||
* originalWork의
|
||||
*
|
||||
* languageCode == null이면 언어 감지 이벤트 호출
|
||||
* languageCode != null이면 번역 이벤트 호출
|
||||
*
|
||||
*/
|
||||
if (originalWork.languageCode == null) {
|
||||
val papagoQuery = listOf(
|
||||
originalWork.title,
|
||||
originalWork.contentType,
|
||||
originalWork.category,
|
||||
originalWork.description
|
||||
)
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString(" ")
|
||||
|
||||
applicationEventPublisher.publishEvent(
|
||||
LanguageDetectEvent(
|
||||
id = originalWork.id!!,
|
||||
query = papagoQuery,
|
||||
targetType = LanguageDetectTargetType.ORIGINAL_WORK
|
||||
)
|
||||
)
|
||||
} else {
|
||||
applicationEventPublisher.publishEvent(
|
||||
LanguageTranslationEvent(
|
||||
id = originalWork.id!!,
|
||||
targetType = LanguageTranslationTargetType.ORIGINAL_WORK
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return originalWork
|
||||
}
|
||||
|
||||
/** 원작 수정 (이미지 경로 포함 선택적 변경) */
|
||||
@@ -107,6 +151,25 @@ class AdminOriginalWorkService(
|
||||
if (imagePath != null) {
|
||||
ow.imagePath = imagePath
|
||||
}
|
||||
|
||||
/**
|
||||
* 번역 이벤트 호출
|
||||
*/
|
||||
if (
|
||||
request.title != null ||
|
||||
request.contentType != null ||
|
||||
request.category != null ||
|
||||
request.description != null ||
|
||||
request.tags != null
|
||||
) {
|
||||
applicationEventPublisher.publishEvent(
|
||||
LanguageTranslationEvent(
|
||||
id = ow.id!!,
|
||||
targetType = LanguageTranslationTargetType.ORIGINAL_WORK
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return originalWorkRepository.save(ow)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ class OriginalWork(
|
||||
@Column(columnDefinition = "TEXT")
|
||||
var description: String = "",
|
||||
|
||||
/** 언어 코드 */
|
||||
@Column(nullable = true)
|
||||
var languageCode: String? = null,
|
||||
|
||||
/** 원천 원작 */
|
||||
@Column(nullable = true)
|
||||
var originalWork: String? = null,
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package kr.co.vividnext.sodalive.chat.original.translation
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||
import javax.persistence.AttributeConverter
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Convert
|
||||
import javax.persistence.Converter
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Table
|
||||
import javax.persistence.UniqueConstraint
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
uniqueConstraints = [
|
||||
UniqueConstraint(columnNames = ["original_work_id", "locale"])
|
||||
]
|
||||
)
|
||||
class OriginalWorkTranslation(
|
||||
@Column(name = "original_work_id")
|
||||
val originalWorkId: Long,
|
||||
@Column(name = "locale")
|
||||
val locale: String,
|
||||
|
||||
@Column(columnDefinition = "json")
|
||||
@Convert(converter = OriginalWorkTranslationPayloadConverter::class)
|
||||
var renderedPayload: OriginalWorkTranslationPayload
|
||||
) : BaseEntity()
|
||||
|
||||
data class OriginalWorkTranslationPayload(
|
||||
val title: String,
|
||||
val contentType: String,
|
||||
val category: String,
|
||||
val description: String,
|
||||
val tags: List<String>
|
||||
)
|
||||
|
||||
@Converter(autoApply = false)
|
||||
class OriginalWorkTranslationPayloadConverter : AttributeConverter<OriginalWorkTranslationPayload, String> {
|
||||
|
||||
override fun convertToDatabaseColumn(attribute: OriginalWorkTranslationPayload?): String {
|
||||
if (attribute == null) return "{}"
|
||||
return objectMapper.writeValueAsString(attribute)
|
||||
}
|
||||
|
||||
override fun convertToEntityAttribute(dbData: String?): OriginalWorkTranslationPayload {
|
||||
if (dbData.isNullOrBlank()) {
|
||||
return OriginalWorkTranslationPayload(
|
||||
title = "",
|
||||
contentType = "",
|
||||
category = "",
|
||||
description = "",
|
||||
tags = emptyList()
|
||||
)
|
||||
}
|
||||
return try {
|
||||
val node = objectMapper.readTree(dbData)
|
||||
val title = node.get("title")?.asText() ?: ""
|
||||
val contentType = node.get("contentType")?.asText() ?: ""
|
||||
val category = node.get("category")?.asText() ?: ""
|
||||
val description = node.get("description")?.asText() ?: ""
|
||||
val tagsNode = node.get("tags")
|
||||
val tags: List<String> = when {
|
||||
tagsNode == null || tagsNode.isNull -> emptyList()
|
||||
tagsNode.isArray -> tagsNode.mapNotNull { it.asText(null) }.filter { it.isNotBlank() }
|
||||
tagsNode.isTextual -> tagsNode.asText()
|
||||
.split(',')
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
|
||||
else -> emptyList()
|
||||
}
|
||||
OriginalWorkTranslationPayload(
|
||||
title = title,
|
||||
contentType = contentType,
|
||||
category = category,
|
||||
description = description,
|
||||
tags = tags
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
OriginalWorkTranslationPayload(
|
||||
title = "",
|
||||
contentType = "",
|
||||
category = "",
|
||||
description = "",
|
||||
tags = emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val objectMapper = jacksonObjectMapper()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.chat.original.translation
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
interface OriginalWorkTranslationRepository : JpaRepository<OriginalWorkTranslation, Long> {
|
||||
fun findByOriginalWorkIdAndLocale(originalWorkId: Long, locale: String): OriginalWorkTranslation?
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.content
|
||||
|
||||
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentRepository
|
||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
||||
import kr.co.vividnext.sodalive.chat.original.OriginalWorkRepository
|
||||
import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository
|
||||
import kr.co.vividnext.sodalive.content.series.ContentSeriesRepository
|
||||
import kr.co.vividnext.sodalive.explorer.profile.CreatorCheersRepository
|
||||
@@ -32,7 +33,8 @@ enum class LanguageDetectTargetType {
|
||||
CHARACTER,
|
||||
CHARACTER_COMMENT,
|
||||
CREATOR_CHEERS,
|
||||
SERIES
|
||||
SERIES,
|
||||
ORIGINAL_WORK
|
||||
}
|
||||
|
||||
class LanguageDetectEvent(
|
||||
@@ -53,6 +55,7 @@ class LanguageDetectListener(
|
||||
private val characterCommentRepository: CharacterCommentRepository,
|
||||
private val creatorCheersRepository: CreatorCheersRepository,
|
||||
private val seriesRepository: ContentSeriesRepository,
|
||||
private val originalWorkRepository: OriginalWorkRepository,
|
||||
|
||||
private val applicationEventPublisher: ApplicationEventPublisher,
|
||||
|
||||
@@ -85,6 +88,7 @@ class LanguageDetectListener(
|
||||
LanguageDetectTargetType.CHARACTER_COMMENT -> handleCharacterCommentLanguageDetect(event)
|
||||
LanguageDetectTargetType.CREATOR_CHEERS -> handleCreatorCheersLanguageDetect(event)
|
||||
LanguageDetectTargetType.SERIES -> handleSeriesLanguageDetect(event)
|
||||
LanguageDetectTargetType.ORIGINAL_WORK -> handleOriginalWorkLanguageDetect(event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,6 +302,45 @@ class LanguageDetectListener(
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleOriginalWorkLanguageDetect(event: LanguageDetectEvent) {
|
||||
val originalWorkId = event.id
|
||||
|
||||
val originalWork = originalWorkRepository.findByIdOrNull(originalWorkId)
|
||||
if (originalWork == null) {
|
||||
log.warn("[PapagoLanguageDetect] OriginalWork not found. originalWorkId={}", originalWorkId)
|
||||
return
|
||||
}
|
||||
|
||||
// 이미 언어 코드가 설정된 경우 호출하지 않음
|
||||
if (!originalWork.languageCode.isNullOrBlank()) {
|
||||
log.debug(
|
||||
"[PapagoLanguageDetect] languageCode already set. Skip language detection. originalWorkId={}, languageCode={}",
|
||||
originalWorkId,
|
||||
originalWork.languageCode
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val langCode = requestPapagoLanguageCode(event.query, originalWorkId) ?: return
|
||||
|
||||
originalWork.languageCode = langCode
|
||||
originalWorkRepository.save(originalWork)
|
||||
|
||||
// 언어 감지가 완료된 후 언어 번역 이벤트 호출
|
||||
applicationEventPublisher.publishEvent(
|
||||
LanguageTranslationEvent(
|
||||
id = originalWorkId,
|
||||
targetType = LanguageTranslationTargetType.ORIGINAL_WORK
|
||||
)
|
||||
)
|
||||
|
||||
log.info(
|
||||
"[PapagoLanguageDetect] languageCode updated from Papago. originalWorkId={}, langCode={}",
|
||||
originalWorkId,
|
||||
langCode
|
||||
)
|
||||
}
|
||||
|
||||
private fun requestPapagoLanguageCode(query: String, targetIdForLog: Long): String? {
|
||||
return try {
|
||||
val headers = HttpHeaders().apply {
|
||||
|
||||
@@ -8,6 +8,10 @@ import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationR
|
||||
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationRepository
|
||||
import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterBackground
|
||||
import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterPersonality
|
||||
import kr.co.vividnext.sodalive.chat.original.OriginalWorkRepository
|
||||
import kr.co.vividnext.sodalive.chat.original.translation.OriginalWorkTranslation
|
||||
import kr.co.vividnext.sodalive.chat.original.translation.OriginalWorkTranslationPayload
|
||||
import kr.co.vividnext.sodalive.chat.original.translation.OriginalWorkTranslationRepository
|
||||
import kr.co.vividnext.sodalive.content.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.content.series.translation.SeriesGenreTranslation
|
||||
import kr.co.vividnext.sodalive.content.series.translation.SeriesGenreTranslationRepository
|
||||
@@ -35,7 +39,9 @@ enum class LanguageTranslationTargetType {
|
||||
CONTENT_THEME,
|
||||
|
||||
SERIES,
|
||||
SERIES_GENRE
|
||||
SERIES_GENRE,
|
||||
|
||||
ORIGINAL_WORK
|
||||
}
|
||||
|
||||
class LanguageTranslationEvent(
|
||||
@@ -50,12 +56,14 @@ class LanguageTranslationListener(
|
||||
private val audioContentThemeRepository: AudioContentThemeQueryRepository,
|
||||
private val seriesRepository: AdminContentSeriesRepository,
|
||||
private val seriesGenreRepository: AdminContentSeriesGenreRepository,
|
||||
private val originalWorkRepository: OriginalWorkRepository,
|
||||
|
||||
private val contentTranslationRepository: ContentTranslationRepository,
|
||||
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
|
||||
private val contentThemeTranslationRepository: ContentThemeTranslationRepository,
|
||||
private val seriesTranslationRepository: SeriesTranslationRepository,
|
||||
private val seriesGenreTranslationRepository: SeriesGenreTranslationRepository,
|
||||
private val originalWorkTranslationRepository: OriginalWorkTranslationRepository,
|
||||
|
||||
private val translationService: PapagoTranslationService
|
||||
) {
|
||||
@@ -69,6 +77,7 @@ class LanguageTranslationListener(
|
||||
LanguageTranslationTargetType.CONTENT_THEME -> handleContentThemeLanguageTranslation(event)
|
||||
LanguageTranslationTargetType.SERIES -> handleSeriesLanguageTranslation(event)
|
||||
LanguageTranslationTargetType.SERIES_GENRE -> handleSeriesGenreLanguageTranslation(event)
|
||||
LanguageTranslationTargetType.ORIGINAL_WORK -> handleOriginalWorkLanguageTranslation(event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,4 +373,84 @@ class LanguageTranslationListener(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOriginalWorkLanguageTranslation(event: LanguageTranslationEvent) {
|
||||
val originalWork = originalWorkRepository.findByIdOrNull(event.id) ?: return
|
||||
val languageCode = originalWork.languageCode
|
||||
if (languageCode != null) return
|
||||
|
||||
/**
|
||||
* handleSeriesLanguageTranslation 참조하여 원작 번역 구현
|
||||
*
|
||||
* originalWorkTranslationRepository
|
||||
*
|
||||
* 번역대상
|
||||
* - title
|
||||
* - contentType
|
||||
* - category
|
||||
* - description
|
||||
* - tags
|
||||
*/
|
||||
getTranslatableLanguageCodes(languageCode).forEach { locale ->
|
||||
val tagsJoined = originalWork.tagMappings
|
||||
.mapNotNull { it.tag.tag }
|
||||
.joinToString(", ")
|
||||
|
||||
val texts = mutableListOf<String>()
|
||||
texts.add(originalWork.title)
|
||||
texts.add(originalWork.contentType)
|
||||
texts.add(originalWork.category)
|
||||
texts.add(originalWork.description)
|
||||
texts.add(tagsJoined)
|
||||
|
||||
val sourceLanguage = originalWork.languageCode ?: "ko"
|
||||
|
||||
val response = translationService.translate(
|
||||
request = TranslateRequest(
|
||||
texts = texts,
|
||||
sourceLanguage = sourceLanguage,
|
||||
targetLanguage = locale
|
||||
)
|
||||
)
|
||||
|
||||
val translatedTexts = response.translatedText
|
||||
if (translatedTexts.size == texts.size) {
|
||||
var index = 0
|
||||
val translatedTitle = translatedTexts[index++]
|
||||
val translatedContentType = translatedTexts[index++]
|
||||
val translatedCategory = translatedTexts[index++]
|
||||
val translatedDescription = translatedTexts[index++]
|
||||
val translatedTagsJoined = translatedTexts[index]
|
||||
|
||||
val translatedTags = translatedTagsJoined
|
||||
.split(",")
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
|
||||
val payload = OriginalWorkTranslationPayload(
|
||||
title = translatedTitle,
|
||||
contentType = translatedContentType,
|
||||
category = translatedCategory,
|
||||
description = translatedDescription,
|
||||
tags = translatedTags
|
||||
)
|
||||
|
||||
val existing = originalWorkTranslationRepository
|
||||
.findByOriginalWorkIdAndLocale(originalWork.id!!, locale)
|
||||
|
||||
if (existing == null) {
|
||||
originalWorkTranslationRepository.save(
|
||||
OriginalWorkTranslation(
|
||||
originalWorkId = originalWork.id!!,
|
||||
locale = locale,
|
||||
renderedPayload = payload
|
||||
)
|
||||
)
|
||||
} else {
|
||||
existing.renderedPayload = payload
|
||||
originalWorkTranslationRepository.save(existing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user