크리에이터 관리자에서 시리즈 등록/수정시 번역데이터 생성 기능 추가

This commit is contained in:
2025-12-16 00:25:24 +09:00
parent 9b2b156d40
commit f58687ef3a
8 changed files with 235 additions and 10 deletions

View File

@@ -372,14 +372,14 @@ class AudioContentService(
query = papagoQuery
)
)
}
} else {
applicationEventPublisher.publishEvent(
LanguageTranslationEvent(
id = audioContent.id!!,
targetType = LanguageTranslationTargetType.CONTENT
)
)
}
return CreateAudioContentResponse(contentId = audioContent.id!!)
}

View File

@@ -3,12 +3,14 @@ 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.content.comment.AudioContentCommentRepository
import kr.co.vividnext.sodalive.content.series.ContentSeriesRepository
import kr.co.vividnext.sodalive.explorer.profile.CreatorCheersRepository
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationEvent
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
@@ -29,7 +31,8 @@ enum class LanguageDetectTargetType {
COMMENT,
CHARACTER,
CHARACTER_COMMENT,
CREATOR_CHEERS
CREATOR_CHEERS,
SERIES
}
class LanguageDetectEvent(
@@ -49,6 +52,7 @@ class LanguageDetectListener(
private val chatCharacterRepository: ChatCharacterRepository,
private val characterCommentRepository: CharacterCommentRepository,
private val creatorCheersRepository: CreatorCheersRepository,
private val seriesRepository: ContentSeriesRepository,
private val applicationEventPublisher: ApplicationEventPublisher,
@@ -80,6 +84,7 @@ class LanguageDetectListener(
LanguageDetectTargetType.CHARACTER -> handleCharacterLanguageDetect(event)
LanguageDetectTargetType.CHARACTER_COMMENT -> handleCharacterCommentLanguageDetect(event)
LanguageDetectTargetType.CREATOR_CHEERS -> handleCreatorCheersLanguageDetect(event)
LanguageDetectTargetType.SERIES -> handleSeriesLanguageDetect(event)
}
}
@@ -255,6 +260,44 @@ class LanguageDetectListener(
)
}
private fun handleSeriesLanguageDetect(event: LanguageDetectEvent) {
val seriesId = event.id
val series = seriesRepository.findByIdOrNull(seriesId)
if (series == null) {
log.warn("[PapagoLanguageDetect] Series not found. seriesId={}", seriesId)
return
}
// 이미 언어 코드가 설정된 경우 호출하지 않음
if (!series.languageCode.isNullOrBlank()) {
log.debug(
"[PapagoLanguageDetect] languageCode already set. Skip language detection. seriesId={}, languageCode={}",
seriesId,
series.languageCode
)
return
}
val langCode = requestPapagoLanguageCode(event.query, seriesId) ?: return
series.languageCode = langCode
seriesRepository.save(series)
applicationEventPublisher.publishEvent(
LanguageTranslationEvent(
id = seriesId,
targetType = LanguageTranslationTargetType.SERIES
)
)
log.info(
"[PapagoLanguageDetect] languageCode updated from Papago. seriesId={}, langCode={}",
seriesId,
langCode
)
}
private fun requestPapagoLanguageCode(query: String, targetIdForLog: Long): String? {
return try {
val headers = HttpHeaders().apply {

View File

@@ -1,11 +1,21 @@
package kr.co.vividnext.sodalive.content.series.translation
import kr.co.vividnext.sodalive.common.BaseEntity
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Table
import javax.persistence.UniqueConstraint
@Entity
@Table(
uniqueConstraints = [
UniqueConstraint(columnNames = ["series_genre_id", "locale"])
]
)
class SeriesGenreTranslation(
@Column(name = "series_genre_id")
val seriesGenreId: Long,
@Column(name = "locale")
val locale: String,
val genre: String
var genre: String
) : BaseEntity()

View File

@@ -0,0 +1,7 @@
package kr.co.vividnext.sodalive.content.series.translation
import org.springframework.data.jpa.repository.JpaRepository
interface SeriesGenreTranslationRepository : JpaRepository<SeriesGenreTranslation, Long> {
fun findBySeriesGenreIdAndLocale(seriesGenreId: Long, locale: String): SeriesGenreTranslation?
}

View File

@@ -0,0 +1,7 @@
package kr.co.vividnext.sodalive.content.series.translation
import org.springframework.data.jpa.repository.JpaRepository
interface SeriesTranslationRepository : JpaRepository<SeriesTranslation, Long> {
fun findBySeriesIdAndLocale(seriesId: Long, locale: String): SeriesTranslation?
}

View File

@@ -5,6 +5,8 @@ import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.content.AudioContentRepository
import kr.co.vividnext.sodalive.content.LanguageDetectEvent
import kr.co.vividnext.sodalive.content.LanguageDetectTargetType
import kr.co.vividnext.sodalive.content.hashtag.HashTag
import kr.co.vividnext.sodalive.content.hashtag.HashTagRepository
import kr.co.vividnext.sodalive.creator.admin.content.series.content.AddingContentToTheSeriesRequest
@@ -12,9 +14,12 @@ import kr.co.vividnext.sodalive.creator.admin.content.series.content.RemoveConte
import kr.co.vividnext.sodalive.creator.admin.content.series.content.SearchContentNotInSeriesResponse
import kr.co.vividnext.sodalive.creator.admin.content.series.genre.CreatorAdminContentSeriesGenreRepository
import kr.co.vividnext.sodalive.creator.admin.content.series.keyword.SeriesKeyword
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationEvent
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@@ -30,6 +35,8 @@ class CreatorAdminContentSeriesService(
private val s3Uploader: S3Uploader,
private val objectMapper: ObjectMapper,
private val applicationEventPublisher: ApplicationEventPublisher,
@Value("\${cloud.aws.s3.bucket}")
private val coverImageBucket: String,
@@ -89,6 +96,31 @@ class CreatorAdminContentSeriesService(
)
series.coverImage = coverImagePath
if (series.languageCode.isNullOrBlank()) {
val papagoQuery = listOf(
request.title.trim(),
request.introduction.trim(),
request.keyword.trim()
)
.filter { it.isNotBlank() }
.joinToString(" ")
applicationEventPublisher.publishEvent(
LanguageDetectEvent(
id = series.id!!,
query = papagoQuery,
targetType = LanguageDetectTargetType.SERIES
)
)
} else {
applicationEventPublisher.publishEvent(
LanguageTranslationEvent(
id = series.id!!,
targetType = LanguageTranslationTargetType.SERIES
)
)
}
}
@Transactional
@@ -174,6 +206,15 @@ class CreatorAdminContentSeriesService(
if (request.studio != null) {
series.studio = request.studio
}
if (request.title != null || request.introduction != null) {
applicationEventPublisher.publishEvent(
LanguageTranslationEvent(
id = series.id!!,
targetType = LanguageTranslationTargetType.SERIES
)
)
}
}
fun getSeriesList(offset: Long, limit: Long, creatorId: Long): GetCreatorAdminContentSeriesListResponse {

View File

@@ -34,6 +34,7 @@ data class Series(
var title: String,
@Column(columnDefinition = "TEXT", nullable = false)
var introduction: String,
var languageCode: String? = null,
@Enumerated(value = EnumType.STRING)
var state: SeriesState = SeriesState.PROCEEDING,
var writer: String? = null,

View File

@@ -1,5 +1,7 @@
package kr.co.vividnext.sodalive.i18n.translation
import kr.co.vividnext.sodalive.admin.content.series.AdminContentSeriesRepository
import kr.co.vividnext.sodalive.admin.content.series.genre.AdminContentSeriesGenreRepository
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslation
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationRenderedPayload
@@ -7,6 +9,11 @@ import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationR
import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterBackground
import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterPersonality
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
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.theme.AudioContentThemeQueryRepository
import kr.co.vividnext.sodalive.content.theme.translation.ContentThemeTranslation
import kr.co.vividnext.sodalive.content.theme.translation.ContentThemeTranslationRepository
@@ -25,7 +32,10 @@ import org.springframework.transaction.event.TransactionalEventListener
enum class LanguageTranslationTargetType {
CONTENT,
CHARACTER,
CONTENT_THEME
CONTENT_THEME,
SERIES,
SERIES_GENRE
}
class LanguageTranslationEvent(
@@ -38,10 +48,14 @@ class LanguageTranslationListener(
private val audioContentRepository: AudioContentRepository,
private val chatCharacterRepository: ChatCharacterRepository,
private val audioContentThemeRepository: AudioContentThemeQueryRepository,
private val seriesRepository: AdminContentSeriesRepository,
private val seriesGenreRepository: AdminContentSeriesGenreRepository,
private val contentTranslationRepository: ContentTranslationRepository,
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
private val contentThemeTranslationRepository: ContentThemeTranslationRepository,
private val seriesTranslationRepository: SeriesTranslationRepository,
private val seriesGenreTranslationRepository: SeriesGenreTranslationRepository,
private val translationService: PapagoTranslationService
) {
@@ -53,6 +67,8 @@ class LanguageTranslationListener(
LanguageTranslationTargetType.CONTENT -> handleContentLanguageTranslation(event)
LanguageTranslationTargetType.CHARACTER -> handleCharacterLanguageTranslation(event)
LanguageTranslationTargetType.CONTENT_THEME -> handleContentThemeLanguageTranslation(event)
LanguageTranslationTargetType.SERIES -> handleSeriesLanguageTranslation(event)
LanguageTranslationTargetType.SERIES_GENRE -> handleSeriesGenreLanguageTranslation(event)
}
}
@@ -248,4 +264,104 @@ class LanguageTranslationListener(
}
}
}
private fun handleSeriesLanguageTranslation(event: LanguageTranslationEvent) {
val series = seriesRepository.findByIdOrNull(event.id) ?: return
val languageCode = series.languageCode
if (languageCode != null) return
getTranslatableLanguageCodes(languageCode).forEach { locale ->
val keywords = series.keywordList
.mapNotNull { it.keyword?.tag }
.joinToString(", ")
val texts = mutableListOf<String>()
texts.add(series.title)
texts.add(series.introduction)
texts.add(keywords)
val sourceLanguage = series.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 translatedIntroduction = translatedTexts[index++]
val translatedKeywordsJoined = translatedTexts[index]
val translatedKeywords = translatedKeywordsJoined
.split(",")
.map { it.trim() }
.filter { it.isNotBlank() }
val payload = SeriesTranslationPayload(
title = translatedTitle,
introduction = translatedIntroduction,
keywords = translatedKeywords
)
val existing = seriesTranslationRepository
.findBySeriesIdAndLocale(series.id!!, locale)
if (existing == null) {
seriesTranslationRepository.save(
SeriesTranslation(
seriesId = series.id!!,
locale = locale,
renderedPayload = payload
)
)
} else {
existing.renderedPayload = payload
seriesTranslationRepository.save(existing)
}
}
}
}
private fun handleSeriesGenreLanguageTranslation(event: LanguageTranslationEvent) {
val seriesGenre = seriesGenreRepository.findActiveSeriesGenreById(event.id) ?: return
val sourceLanguage = "ko"
getTranslatableLanguageCodes(sourceLanguage).forEach { locale ->
val texts = mutableListOf<String>()
texts.add(seriesGenre.genre)
val response = translationService.translate(
request = TranslateRequest(
texts = texts,
sourceLanguage = sourceLanguage,
targetLanguage = locale
)
)
val translatedTexts = response.translatedText
if (translatedTexts.size == texts.size) {
val translatedGenre = translatedTexts[0]
val existing = seriesGenreTranslationRepository
.findBySeriesGenreIdAndLocale(seriesGenre.id!!, locale)
if (existing == null) {
seriesGenreTranslationRepository.save(
SeriesGenreTranslation(
seriesGenreId = seriesGenre.id!!,
locale = locale,
genre = translatedGenre
)
)
} else {
existing.genre = translatedGenre
seriesGenreTranslationRepository.save(existing)
}
}
}
}
}