Compare commits
5 Commits
45ee55028f
...
0eed29eadc
| Author | SHA1 | Date | |
|---|---|---|---|
| 0eed29eadc | |||
| db18d5c8b5 | |||
| f58687ef3a | |||
| 9b2b156d40 | |||
| e00a9ccff5 |
@@ -12,6 +12,7 @@ import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerService
|
|||||||
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationService
|
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationService
|
||||||
import kr.co.vividnext.sodalive.content.series.ContentSeriesService
|
import kr.co.vividnext.sodalive.content.series.ContentSeriesService
|
||||||
import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse
|
import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse
|
||||||
|
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationRepository
|
||||||
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeService
|
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeService
|
||||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
|
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
|
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
|
||||||
@@ -53,6 +54,7 @@ class HomeService(
|
|||||||
|
|
||||||
private val contentTranslationRepository: ContentTranslationRepository,
|
private val contentTranslationRepository: ContentTranslationRepository,
|
||||||
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
|
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
|
||||||
|
private val seriesTranslationRepository: SeriesTranslationRepository,
|
||||||
|
|
||||||
private val langContext: LangContext,
|
private val langContext: LangContext,
|
||||||
|
|
||||||
@@ -133,20 +135,25 @@ class HomeService(
|
|||||||
isAdult = isAdult
|
isAdult = isAdult
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 오직 보이스온에서만
|
||||||
val originalAudioDramaList = seriesService.getOriginalAudioDramaList(
|
val originalAudioDramaList = seriesService.getOriginalAudioDramaList(
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
orderByRandom = true
|
orderByRandom = true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val translatedOriginalAudioDramaList = getTranslatedSeriesList(seriesList = originalAudioDramaList)
|
||||||
|
|
||||||
val auditionList = auditionService.getInProgressAuditionList(isAdult = isAdult)
|
val auditionList = auditionService.getInProgressAuditionList(isAdult = isAdult)
|
||||||
|
|
||||||
|
// 요일별 시리즈
|
||||||
val dayOfWeekSeriesList = seriesService.getDayOfWeekSeriesList(
|
val dayOfWeekSeriesList = seriesService.getDayOfWeekSeriesList(
|
||||||
memberId = memberId,
|
memberId = memberId,
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
dayOfWeek = getDayOfWeekByTimezone(timezone)
|
dayOfWeek = getDayOfWeekByTimezone(timezone)
|
||||||
)
|
)
|
||||||
|
val translatedDayOfWeekSeriesList = getTranslatedSeriesList(seriesList = dayOfWeekSeriesList)
|
||||||
|
|
||||||
// 인기 캐릭터 조회
|
// 인기 캐릭터 조회
|
||||||
val translatedPopularCharacters = getTranslatedAiCharacterList(aiCharacterList = characterService.getPopularCharacters())
|
val translatedPopularCharacters = getTranslatedAiCharacterList(aiCharacterList = characterService.getPopularCharacters())
|
||||||
@@ -280,9 +287,9 @@ class HomeService(
|
|||||||
latestContentList = translatedLatestContentList,
|
latestContentList = translatedLatestContentList,
|
||||||
bannerList = bannerList,
|
bannerList = bannerList,
|
||||||
eventBannerList = eventBannerList,
|
eventBannerList = eventBannerList,
|
||||||
originalAudioDramaList = originalAudioDramaList,
|
originalAudioDramaList = translatedOriginalAudioDramaList,
|
||||||
auditionList = auditionList,
|
auditionList = auditionList,
|
||||||
dayOfWeekSeriesList = dayOfWeekSeriesList,
|
dayOfWeekSeriesList = translatedDayOfWeekSeriesList,
|
||||||
popularCharacters = translatedPopularCharacters,
|
popularCharacters = translatedPopularCharacters,
|
||||||
contentRanking = translatedContentRanking,
|
contentRanking = translatedContentRanking,
|
||||||
recommendChannelList = translatedRecommendChannelList,
|
recommendChannelList = translatedRecommendChannelList,
|
||||||
@@ -479,6 +486,44 @@ class HomeService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 시리즈 리스트의 제목을 현재 언어(locale)에 맞춰 일괄 번역한다.
|
||||||
|
*
|
||||||
|
* 처리 절차:
|
||||||
|
* - 입력된 시리즈들의 seriesId 집합을 만들고, 요청 언어 코드(langContext.lang.code)로
|
||||||
|
* seriesTranslationRepository에서 번역 데이터를 한 번에 조회한다.
|
||||||
|
* - 각 항목에 대해 번역된 제목이 존재하고 비어있지 않으면 title만 번역 값으로 교체한다.
|
||||||
|
* - 번역이 없거나 공백이면 원본 항목을 그대로 반환한다.
|
||||||
|
*
|
||||||
|
* 성능:
|
||||||
|
* - N건의 항목을 1회의 조회로 해결하기 위해 IN 쿼리를 사용한다.
|
||||||
|
*
|
||||||
|
* @param seriesList 번역 대상 SeriesListItem 목록
|
||||||
|
* @return 제목이 가능한 항목은 번역된 목록(불변 사본), 그 외는 원본 항목 유지
|
||||||
|
*/
|
||||||
|
private fun getTranslatedSeriesList(
|
||||||
|
seriesList: List<GetSeriesListResponse.SeriesListItem>
|
||||||
|
): List<GetSeriesListResponse.SeriesListItem> {
|
||||||
|
val seriesIds = seriesList.map { it.seriesId }
|
||||||
|
|
||||||
|
return if (seriesIds.isNotEmpty()) {
|
||||||
|
val translations = seriesTranslationRepository
|
||||||
|
.findBySeriesIdInAndLocale(seriesIds = seriesIds, locale = langContext.lang.code)
|
||||||
|
.associateBy { it.seriesId }
|
||||||
|
|
||||||
|
seriesList.map { item ->
|
||||||
|
val translatedTitle = translations[item.seriesId]?.renderedPayload?.title
|
||||||
|
if (translatedTitle.isNullOrBlank()) {
|
||||||
|
item
|
||||||
|
} else {
|
||||||
|
item.copy(title = translatedTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
seriesList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI 캐릭터 리스트의 이름/설명을 현재 언어(locale)에 맞춰 일괄 번역한다.
|
* AI 캐릭터 리스트의 이름/설명을 현재 언어(locale)에 맞춰 일괄 번역한다.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -372,14 +372,14 @@ class AudioContentService(
|
|||||||
query = papagoQuery
|
query = papagoQuery
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
} else {
|
||||||
|
applicationEventPublisher.publishEvent(
|
||||||
applicationEventPublisher.publishEvent(
|
LanguageTranslationEvent(
|
||||||
LanguageTranslationEvent(
|
id = audioContent.id!!,
|
||||||
id = audioContent.id!!,
|
targetType = LanguageTranslationTargetType.CONTENT
|
||||||
targetType = LanguageTranslationTargetType.CONTENT
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
return CreateAudioContentResponse(contentId = audioContent.id!!)
|
return CreateAudioContentResponse(contentId = audioContent.id!!)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.comment.CharacterCommentRepository
|
||||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
||||||
import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository
|
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.explorer.profile.CreatorCheersRepository
|
||||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationEvent
|
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationEvent
|
||||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
|
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.context.ApplicationEventPublisher
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.http.HttpEntity
|
import org.springframework.http.HttpEntity
|
||||||
import org.springframework.http.HttpHeaders
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
@@ -29,7 +31,8 @@ enum class LanguageDetectTargetType {
|
|||||||
COMMENT,
|
COMMENT,
|
||||||
CHARACTER,
|
CHARACTER,
|
||||||
CHARACTER_COMMENT,
|
CHARACTER_COMMENT,
|
||||||
CREATOR_CHEERS
|
CREATOR_CHEERS,
|
||||||
|
SERIES
|
||||||
}
|
}
|
||||||
|
|
||||||
class LanguageDetectEvent(
|
class LanguageDetectEvent(
|
||||||
@@ -49,6 +52,7 @@ class LanguageDetectListener(
|
|||||||
private val chatCharacterRepository: ChatCharacterRepository,
|
private val chatCharacterRepository: ChatCharacterRepository,
|
||||||
private val characterCommentRepository: CharacterCommentRepository,
|
private val characterCommentRepository: CharacterCommentRepository,
|
||||||
private val creatorCheersRepository: CreatorCheersRepository,
|
private val creatorCheersRepository: CreatorCheersRepository,
|
||||||
|
private val seriesRepository: ContentSeriesRepository,
|
||||||
|
|
||||||
private val applicationEventPublisher: ApplicationEventPublisher,
|
private val applicationEventPublisher: ApplicationEventPublisher,
|
||||||
|
|
||||||
@@ -80,6 +84,7 @@ class LanguageDetectListener(
|
|||||||
LanguageDetectTargetType.CHARACTER -> handleCharacterLanguageDetect(event)
|
LanguageDetectTargetType.CHARACTER -> handleCharacterLanguageDetect(event)
|
||||||
LanguageDetectTargetType.CHARACTER_COMMENT -> handleCharacterCommentLanguageDetect(event)
|
LanguageDetectTargetType.CHARACTER_COMMENT -> handleCharacterCommentLanguageDetect(event)
|
||||||
LanguageDetectTargetType.CREATOR_CHEERS -> handleCreatorCheersLanguageDetect(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? {
|
private fun requestPapagoLanguageCode(query: String, targetIdForLog: Long): String? {
|
||||||
return try {
|
return try {
|
||||||
val headers = HttpHeaders().apply {
|
val headers = HttpHeaders().apply {
|
||||||
|
|||||||
@@ -6,11 +6,14 @@ 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.SeriesTranslationRepository
|
||||||
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
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesState
|
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.LangContext
|
||||||
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
|
||||||
@@ -26,6 +29,8 @@ 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 seriesTranslationRepository: SeriesTranslationRepository,
|
||||||
|
|
||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
private val coverImageHost: String
|
private val coverImageHost: String
|
||||||
@@ -83,7 +88,7 @@ class ContentSeriesService(
|
|||||||
).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) }
|
).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) }
|
||||||
|
|
||||||
val items = seriesToSeriesListItem(seriesList = rawItems, isAdult = isAuth, contentType = contentType)
|
val items = seriesToSeriesListItem(seriesList = rawItems, isAdult = isAuth, contentType = contentType)
|
||||||
return GetSeriesListResponse(totalCount, items)
|
return GetSeriesListResponse(totalCount, getTranslatedSeriesList(items))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSeriesListByGenre(
|
fun getSeriesListByGenre(
|
||||||
@@ -338,27 +343,105 @@ class ContentSeriesService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun publishedDaysOfWeekText(publishedDaysOfWeek: Set<SeriesPublishedDaysOfWeek>): String {
|
private fun publishedDaysOfWeekText(publishedDaysOfWeek: Set<SeriesPublishedDaysOfWeek>): String {
|
||||||
|
/**
|
||||||
|
* i18n을 적용하여 언어별로 요일 표시를 변경한다.
|
||||||
|
*/
|
||||||
|
val lang = langContext.lang
|
||||||
|
|
||||||
|
val labelRandom = when (lang) {
|
||||||
|
Lang.EN -> "Random"
|
||||||
|
Lang.JA -> "ランダム"
|
||||||
|
else -> "랜덤"
|
||||||
|
}
|
||||||
|
val labels = when (lang) {
|
||||||
|
Lang.EN -> mapOf(
|
||||||
|
SeriesPublishedDaysOfWeek.SUN to "Sun",
|
||||||
|
SeriesPublishedDaysOfWeek.MON to "Mon",
|
||||||
|
SeriesPublishedDaysOfWeek.TUE to "Tue",
|
||||||
|
SeriesPublishedDaysOfWeek.WED to "Wed",
|
||||||
|
SeriesPublishedDaysOfWeek.THU to "Thu",
|
||||||
|
SeriesPublishedDaysOfWeek.FRI to "Fri",
|
||||||
|
SeriesPublishedDaysOfWeek.SAT to "Sat",
|
||||||
|
SeriesPublishedDaysOfWeek.RANDOM to labelRandom
|
||||||
|
)
|
||||||
|
|
||||||
|
Lang.JA -> mapOf(
|
||||||
|
SeriesPublishedDaysOfWeek.SUN to "日",
|
||||||
|
SeriesPublishedDaysOfWeek.MON to "月",
|
||||||
|
SeriesPublishedDaysOfWeek.TUE to "火",
|
||||||
|
SeriesPublishedDaysOfWeek.WED to "水",
|
||||||
|
SeriesPublishedDaysOfWeek.THU to "木",
|
||||||
|
SeriesPublishedDaysOfWeek.FRI to "金",
|
||||||
|
SeriesPublishedDaysOfWeek.SAT to "土",
|
||||||
|
SeriesPublishedDaysOfWeek.RANDOM to labelRandom
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> mapOf(
|
||||||
|
SeriesPublishedDaysOfWeek.SUN to "일",
|
||||||
|
SeriesPublishedDaysOfWeek.MON to "월",
|
||||||
|
SeriesPublishedDaysOfWeek.TUE to "화",
|
||||||
|
SeriesPublishedDaysOfWeek.WED to "수",
|
||||||
|
SeriesPublishedDaysOfWeek.THU to "목",
|
||||||
|
SeriesPublishedDaysOfWeek.FRI to "금",
|
||||||
|
SeriesPublishedDaysOfWeek.SAT to "토",
|
||||||
|
SeriesPublishedDaysOfWeek.RANDOM to labelRandom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val dayOfWeekText = publishedDaysOfWeek.toList().sortedBy { it.ordinal }
|
val dayOfWeekText = publishedDaysOfWeek.toList().sortedBy { it.ordinal }
|
||||||
.map {
|
.map { labels[it] ?: it.name }
|
||||||
when (it) {
|
|
||||||
SeriesPublishedDaysOfWeek.SUN -> "일"
|
|
||||||
SeriesPublishedDaysOfWeek.MON -> "월"
|
|
||||||
SeriesPublishedDaysOfWeek.TUE -> "화"
|
|
||||||
SeriesPublishedDaysOfWeek.WED -> "수"
|
|
||||||
SeriesPublishedDaysOfWeek.THU -> "목"
|
|
||||||
SeriesPublishedDaysOfWeek.FRI -> "금"
|
|
||||||
SeriesPublishedDaysOfWeek.SAT -> "토"
|
|
||||||
SeriesPublishedDaysOfWeek.RANDOM -> "랜덤"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.joinToString(", ") { it }
|
.joinToString(", ") { it }
|
||||||
|
|
||||||
return if (publishedDaysOfWeek.contains(SeriesPublishedDaysOfWeek.RANDOM)) {
|
val containsRandom = publishedDaysOfWeek.contains(SeriesPublishedDaysOfWeek.RANDOM)
|
||||||
|
return if (containsRandom) {
|
||||||
dayOfWeekText
|
dayOfWeekText
|
||||||
} else if (publishedDaysOfWeek.size < 7) {
|
} else if (publishedDaysOfWeek.size < 7) {
|
||||||
"매주 $dayOfWeekText"
|
when (lang) {
|
||||||
|
Lang.EN -> "Every $dayOfWeekText"
|
||||||
|
Lang.JA -> "毎週 $dayOfWeekText"
|
||||||
|
else -> "매주 $dayOfWeekText"
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
"매일"
|
when (lang) {
|
||||||
|
Lang.EN -> "Daily"
|
||||||
|
Lang.JA -> "毎日"
|
||||||
|
else -> "매일"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 시리즈 리스트의 제목을 현재 언어(locale)에 맞춰 일괄 번역한다.
|
||||||
|
*
|
||||||
|
* 처리 절차:
|
||||||
|
* - 입력된 시리즈들의 seriesId 집합을 만들고, 요청 언어 코드(langContext.lang.code)로
|
||||||
|
* seriesTranslationRepository에서 번역 데이터를 한 번에 조회한다.
|
||||||
|
* - 각 항목에 대해 번역된 제목이 존재하고 비어있지 않으면 title만 번역 값으로 교체한다.
|
||||||
|
* - 번역이 없거나 공백이면 원본 항목을 그대로 반환한다.
|
||||||
|
*
|
||||||
|
* 성능:
|
||||||
|
* - N건의 항목을 1회의 조회로 해결하기 위해 IN 쿼리를 사용한다.
|
||||||
|
*
|
||||||
|
* @param seriesList 번역 대상 SeriesListItem 목록
|
||||||
|
* @return 제목이 가능한 항목은 번역된 목록(불변 사본), 그 외는 원본 항목 유지
|
||||||
|
*/
|
||||||
|
private fun getTranslatedSeriesList(
|
||||||
|
seriesList: List<GetSeriesListResponse.SeriesListItem>
|
||||||
|
): List<GetSeriesListResponse.SeriesListItem> {
|
||||||
|
val seriesIds = seriesList.map { it.seriesId }
|
||||||
|
if (seriesIds.isEmpty()) return seriesList
|
||||||
|
|
||||||
|
val translations = seriesTranslationRepository
|
||||||
|
.findBySeriesIdInAndLocale(seriesIds = seriesIds, locale = langContext.lang.code)
|
||||||
|
.associateBy { it.seriesId }
|
||||||
|
|
||||||
|
return seriesList.map { item ->
|
||||||
|
val translatedTitle = translations[item.seriesId]?.renderedPayload?.title
|
||||||
|
if (translatedTitle.isNullOrBlank()) {
|
||||||
|
item
|
||||||
|
} else {
|
||||||
|
item.copy(title = translatedTitle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +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,
|
||||||
|
var genre: String
|
||||||
|
) : BaseEntity()
|
||||||
@@ -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?
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package kr.co.vividnext.sodalive.content.series.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
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
class SeriesTranslation(
|
||||||
|
val seriesId: Long,
|
||||||
|
val locale: String,
|
||||||
|
|
||||||
|
@Column(columnDefinition = "json")
|
||||||
|
@Convert(converter = SeriesTranslationPayloadConverter::class)
|
||||||
|
var renderedPayload: SeriesTranslationPayload
|
||||||
|
) : BaseEntity()
|
||||||
|
|
||||||
|
data class SeriesTranslationPayload(
|
||||||
|
val title: String,
|
||||||
|
val introduction: String,
|
||||||
|
val keywords: List<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Converter(autoApply = false)
|
||||||
|
class SeriesTranslationPayloadConverter : AttributeConverter<SeriesTranslationPayload, String> {
|
||||||
|
|
||||||
|
override fun convertToDatabaseColumn(attribute: SeriesTranslationPayload?): String {
|
||||||
|
if (attribute == null) return "{}"
|
||||||
|
return objectMapper.writeValueAsString(attribute)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun convertToEntityAttribute(dbData: String?): SeriesTranslationPayload {
|
||||||
|
if (dbData.isNullOrBlank()) {
|
||||||
|
return SeriesTranslationPayload(
|
||||||
|
title = "",
|
||||||
|
introduction = "",
|
||||||
|
keywords = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 호환 처리: 과거 스키마에서 keywords가 String 이었을 수 있으므로 유연하게 파싱한다.
|
||||||
|
return try {
|
||||||
|
val node = objectMapper.readTree(dbData)
|
||||||
|
val title = node.get("title")?.asText() ?: ""
|
||||||
|
val introduction = node.get("introduction")?.asText() ?: ""
|
||||||
|
val keywordsNode = node.get("keywords")
|
||||||
|
val keywords: List<String> = when {
|
||||||
|
keywordsNode == null || keywordsNode.isNull -> emptyList()
|
||||||
|
keywordsNode.isArray -> keywordsNode.mapNotNull { it.asText(null) }.filter { it.isNotBlank() }
|
||||||
|
keywordsNode.isTextual -> listOfNotNull(keywordsNode.asText()).filter { it.isNotBlank() }
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
SeriesTranslationPayload(
|
||||||
|
title = title,
|
||||||
|
introduction = introduction,
|
||||||
|
keywords = keywords
|
||||||
|
)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// 파싱 실패 시 안전한 기본값 반환
|
||||||
|
SeriesTranslationPayload(
|
||||||
|
title = "",
|
||||||
|
introduction = "",
|
||||||
|
keywords = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val objectMapper = jacksonObjectMapper()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
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?
|
||||||
|
fun findBySeriesIdInAndLocale(seriesIds: List<Long>, locale: String): List<SeriesTranslation>
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
|
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
import kr.co.vividnext.sodalive.content.AudioContentRepository
|
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.HashTag
|
||||||
import kr.co.vividnext.sodalive.content.hashtag.HashTagRepository
|
import kr.co.vividnext.sodalive.content.hashtag.HashTagRepository
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.content.AddingContentToTheSeriesRequest
|
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.content.SearchContentNotInSeriesResponse
|
||||||
import kr.co.vividnext.sodalive.creator.admin.content.series.genre.CreatorAdminContentSeriesGenreRepository
|
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.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.member.Member
|
||||||
import kr.co.vividnext.sodalive.utils.generateFileName
|
import kr.co.vividnext.sodalive.utils.generateFileName
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
@@ -30,6 +35,8 @@ class CreatorAdminContentSeriesService(
|
|||||||
private val s3Uploader: S3Uploader,
|
private val s3Uploader: S3Uploader,
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
|
|
||||||
|
private val applicationEventPublisher: ApplicationEventPublisher,
|
||||||
|
|
||||||
@Value("\${cloud.aws.s3.bucket}")
|
@Value("\${cloud.aws.s3.bucket}")
|
||||||
private val coverImageBucket: String,
|
private val coverImageBucket: String,
|
||||||
|
|
||||||
@@ -89,6 +96,31 @@ class CreatorAdminContentSeriesService(
|
|||||||
)
|
)
|
||||||
|
|
||||||
series.coverImage = coverImagePath
|
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
|
@Transactional
|
||||||
@@ -174,6 +206,15 @@ class CreatorAdminContentSeriesService(
|
|||||||
if (request.studio != null) {
|
if (request.studio != null) {
|
||||||
series.studio = request.studio
|
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 {
|
fun getSeriesList(offset: Long, limit: Long, creatorId: Long): GetCreatorAdminContentSeriesListResponse {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ data class Series(
|
|||||||
var title: String,
|
var title: String,
|
||||||
@Column(columnDefinition = "TEXT", nullable = false)
|
@Column(columnDefinition = "TEXT", nullable = false)
|
||||||
var introduction: String,
|
var introduction: String,
|
||||||
|
var languageCode: String? = null,
|
||||||
@Enumerated(value = EnumType.STRING)
|
@Enumerated(value = EnumType.STRING)
|
||||||
var state: SeriesState = SeriesState.PROCEEDING,
|
var state: SeriesState = SeriesState.PROCEEDING,
|
||||||
var writer: String? = null,
|
var writer: String? = null,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package kr.co.vividnext.sodalive.i18n.translation
|
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.repository.ChatCharacterRepository
|
||||||
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslation
|
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslation
|
||||||
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationRenderedPayload
|
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.TranslatedAiCharacterBackground
|
||||||
import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterPersonality
|
import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterPersonality
|
||||||
import kr.co.vividnext.sodalive.content.AudioContentRepository
|
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.AudioContentThemeQueryRepository
|
||||||
import kr.co.vividnext.sodalive.content.theme.translation.ContentThemeTranslation
|
import kr.co.vividnext.sodalive.content.theme.translation.ContentThemeTranslation
|
||||||
import kr.co.vividnext.sodalive.content.theme.translation.ContentThemeTranslationRepository
|
import kr.co.vividnext.sodalive.content.theme.translation.ContentThemeTranslationRepository
|
||||||
@@ -25,7 +32,10 @@ import org.springframework.transaction.event.TransactionalEventListener
|
|||||||
enum class LanguageTranslationTargetType {
|
enum class LanguageTranslationTargetType {
|
||||||
CONTENT,
|
CONTENT,
|
||||||
CHARACTER,
|
CHARACTER,
|
||||||
CONTENT_THEME
|
CONTENT_THEME,
|
||||||
|
|
||||||
|
SERIES,
|
||||||
|
SERIES_GENRE
|
||||||
}
|
}
|
||||||
|
|
||||||
class LanguageTranslationEvent(
|
class LanguageTranslationEvent(
|
||||||
@@ -38,10 +48,14 @@ class LanguageTranslationListener(
|
|||||||
private val audioContentRepository: AudioContentRepository,
|
private val audioContentRepository: AudioContentRepository,
|
||||||
private val chatCharacterRepository: ChatCharacterRepository,
|
private val chatCharacterRepository: ChatCharacterRepository,
|
||||||
private val audioContentThemeRepository: AudioContentThemeQueryRepository,
|
private val audioContentThemeRepository: AudioContentThemeQueryRepository,
|
||||||
|
private val seriesRepository: AdminContentSeriesRepository,
|
||||||
|
private val seriesGenreRepository: AdminContentSeriesGenreRepository,
|
||||||
|
|
||||||
private val contentTranslationRepository: ContentTranslationRepository,
|
private val contentTranslationRepository: ContentTranslationRepository,
|
||||||
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
|
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
|
||||||
private val contentThemeTranslationRepository: ContentThemeTranslationRepository,
|
private val contentThemeTranslationRepository: ContentThemeTranslationRepository,
|
||||||
|
private val seriesTranslationRepository: SeriesTranslationRepository,
|
||||||
|
private val seriesGenreTranslationRepository: SeriesGenreTranslationRepository,
|
||||||
|
|
||||||
private val translationService: PapagoTranslationService
|
private val translationService: PapagoTranslationService
|
||||||
) {
|
) {
|
||||||
@@ -53,6 +67,8 @@ class LanguageTranslationListener(
|
|||||||
LanguageTranslationTargetType.CONTENT -> handleContentLanguageTranslation(event)
|
LanguageTranslationTargetType.CONTENT -> handleContentLanguageTranslation(event)
|
||||||
LanguageTranslationTargetType.CHARACTER -> handleCharacterLanguageTranslation(event)
|
LanguageTranslationTargetType.CHARACTER -> handleCharacterLanguageTranslation(event)
|
||||||
LanguageTranslationTargetType.CONTENT_THEME -> handleContentThemeLanguageTranslation(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user