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.series.ContentSeriesService
|
||||
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.translation.ContentTranslationRepository
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
|
||||
@@ -53,6 +54,7 @@ class HomeService(
|
||||
|
||||
private val contentTranslationRepository: ContentTranslationRepository,
|
||||
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
|
||||
private val seriesTranslationRepository: SeriesTranslationRepository,
|
||||
|
||||
private val langContext: LangContext,
|
||||
|
||||
@@ -133,20 +135,25 @@ class HomeService(
|
||||
isAdult = isAdult
|
||||
)
|
||||
|
||||
// 오직 보이스온에서만
|
||||
val originalAudioDramaList = seriesService.getOriginalAudioDramaList(
|
||||
isAdult = isAdult,
|
||||
contentType = contentType,
|
||||
orderByRandom = true
|
||||
)
|
||||
|
||||
val translatedOriginalAudioDramaList = getTranslatedSeriesList(seriesList = originalAudioDramaList)
|
||||
|
||||
val auditionList = auditionService.getInProgressAuditionList(isAdult = isAdult)
|
||||
|
||||
// 요일별 시리즈
|
||||
val dayOfWeekSeriesList = seriesService.getDayOfWeekSeriesList(
|
||||
memberId = memberId,
|
||||
isAdult = isAdult,
|
||||
contentType = contentType,
|
||||
dayOfWeek = getDayOfWeekByTimezone(timezone)
|
||||
)
|
||||
val translatedDayOfWeekSeriesList = getTranslatedSeriesList(seriesList = dayOfWeekSeriesList)
|
||||
|
||||
// 인기 캐릭터 조회
|
||||
val translatedPopularCharacters = getTranslatedAiCharacterList(aiCharacterList = characterService.getPopularCharacters())
|
||||
@@ -280,9 +287,9 @@ class HomeService(
|
||||
latestContentList = translatedLatestContentList,
|
||||
bannerList = bannerList,
|
||||
eventBannerList = eventBannerList,
|
||||
originalAudioDramaList = originalAudioDramaList,
|
||||
originalAudioDramaList = translatedOriginalAudioDramaList,
|
||||
auditionList = auditionList,
|
||||
dayOfWeekSeriesList = dayOfWeekSeriesList,
|
||||
dayOfWeekSeriesList = translatedDayOfWeekSeriesList,
|
||||
popularCharacters = translatedPopularCharacters,
|
||||
contentRanking = translatedContentRanking,
|
||||
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)에 맞춰 일괄 번역한다.
|
||||
*
|
||||
|
||||
@@ -372,14 +372,14 @@ class AudioContentService(
|
||||
query = papagoQuery
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
applicationEventPublisher.publishEvent(
|
||||
LanguageTranslationEvent(
|
||||
id = audioContent.id!!,
|
||||
targetType = LanguageTranslationTargetType.CONTENT
|
||||
} else {
|
||||
applicationEventPublisher.publishEvent(
|
||||
LanguageTranslationEvent(
|
||||
id = audioContent.id!!,
|
||||
targetType = LanguageTranslationTargetType.CONTENT
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
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.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 {
|
||||
|
||||
@@ -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.series.content.ContentSeriesContentRepository
|
||||
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.SeriesPublishedDaysOfWeek
|
||||
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.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.block.BlockMemberRepository
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
@@ -26,6 +29,8 @@ class ContentSeriesService(
|
||||
private val blockMemberRepository: BlockMemberRepository,
|
||||
private val explorerQueryRepository: ExplorerQueryRepository,
|
||||
private val seriesContentRepository: ContentSeriesContentRepository,
|
||||
private val langContext: LangContext,
|
||||
private val seriesTranslationRepository: SeriesTranslationRepository,
|
||||
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val coverImageHost: String
|
||||
@@ -83,7 +88,7 @@ class ContentSeriesService(
|
||||
).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) }
|
||||
|
||||
val items = seriesToSeriesListItem(seriesList = rawItems, isAdult = isAuth, contentType = contentType)
|
||||
return GetSeriesListResponse(totalCount, items)
|
||||
return GetSeriesListResponse(totalCount, getTranslatedSeriesList(items))
|
||||
}
|
||||
|
||||
fun getSeriesListByGenre(
|
||||
@@ -338,27 +343,105 @@ class ContentSeriesService(
|
||||
}
|
||||
|
||||
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 }
|
||||
.map {
|
||||
when (it) {
|
||||
SeriesPublishedDaysOfWeek.SUN -> "일"
|
||||
SeriesPublishedDaysOfWeek.MON -> "월"
|
||||
SeriesPublishedDaysOfWeek.TUE -> "화"
|
||||
SeriesPublishedDaysOfWeek.WED -> "수"
|
||||
SeriesPublishedDaysOfWeek.THU -> "목"
|
||||
SeriesPublishedDaysOfWeek.FRI -> "금"
|
||||
SeriesPublishedDaysOfWeek.SAT -> "토"
|
||||
SeriesPublishedDaysOfWeek.RANDOM -> "랜덤"
|
||||
}
|
||||
}
|
||||
.map { labels[it] ?: it.name }
|
||||
.joinToString(", ") { it }
|
||||
|
||||
return if (publishedDaysOfWeek.contains(SeriesPublishedDaysOfWeek.RANDOM)) {
|
||||
val containsRandom = publishedDaysOfWeek.contains(SeriesPublishedDaysOfWeek.RANDOM)
|
||||
return if (containsRandom) {
|
||||
dayOfWeekText
|
||||
} else if (publishedDaysOfWeek.size < 7) {
|
||||
"매주 $dayOfWeekText"
|
||||
when (lang) {
|
||||
Lang.EN -> "Every $dayOfWeekText"
|
||||
Lang.JA -> "毎週 $dayOfWeekText"
|
||||
else -> "매주 $dayOfWeekText"
|
||||
}
|
||||
} 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.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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user