Compare commits
2 Commits
920a866ae0
...
13029ab8d2
| Author | SHA1 | Date | |
|---|---|---|---|
| 13029ab8d2 | |||
| 6f0619e482 |
@@ -5,8 +5,11 @@ 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.theme.AudioContentTheme
|
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
|
||||||
import kr.co.vividnext.sodalive.content.theme.GetAudioContentThemeResponse
|
import kr.co.vividnext.sodalive.content.theme.GetAudioContentThemeResponse
|
||||||
|
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationEvent
|
||||||
|
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
|
||||||
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
|
||||||
@@ -18,6 +21,8 @@ class AdminContentThemeService(
|
|||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
private val repository: AdminContentThemeRepository,
|
private val repository: AdminContentThemeRepository,
|
||||||
|
|
||||||
|
private val applicationEventPublisher: ApplicationEventPublisher,
|
||||||
|
|
||||||
@Value("\${cloud.aws.s3.bucket}")
|
@Value("\${cloud.aws.s3.bucket}")
|
||||||
private val bucket: String
|
private val bucket: String
|
||||||
) {
|
) {
|
||||||
@@ -37,7 +42,14 @@ class AdminContentThemeService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createTheme(theme: String, imagePath: String) {
|
fun createTheme(theme: String, imagePath: String) {
|
||||||
repository.save(AudioContentTheme(theme = theme, image = imagePath))
|
val savedTheme = repository.save(AudioContentTheme(theme = theme, image = imagePath))
|
||||||
|
|
||||||
|
applicationEventPublisher.publishEvent(
|
||||||
|
LanguageTranslationEvent(
|
||||||
|
id = savedTheme.id!!,
|
||||||
|
targetType = LanguageTranslationTargetType.CONTENT_THEME
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun themeExistCheck(request: CreateContentThemeRequest) {
|
fun themeExistCheck(request: CreateContentThemeRequest) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import javax.persistence.Table
|
|||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "content_theme")
|
@Table(name = "content_theme")
|
||||||
data class AudioContentTheme(
|
class AudioContentTheme(
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
var theme: String,
|
var theme: String,
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ class AudioContentThemeQueryRepository(
|
|||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
private val cloudFrontHost: String
|
private val cloudFrontHost: String
|
||||||
) {
|
) {
|
||||||
|
data class ThemeIdAndName(
|
||||||
|
val id: Long,
|
||||||
|
val theme: String
|
||||||
|
)
|
||||||
fun getActiveThemes(): List<GetAudioContentThemeResponse> {
|
fun getActiveThemes(): List<GetAudioContentThemeResponse> {
|
||||||
return queryFactory
|
return queryFactory
|
||||||
.select(
|
.select(
|
||||||
@@ -88,6 +92,69 @@ class AudioContentThemeQueryRepository(
|
|||||||
return query.fetch()
|
return query.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getActiveThemeWithIdsOfContent(
|
||||||
|
isAdult: Boolean = false,
|
||||||
|
isFree: Boolean = false,
|
||||||
|
isPointAvailableOnly: Boolean = false,
|
||||||
|
contentType: ContentType
|
||||||
|
): List<ThemeIdAndName> {
|
||||||
|
var where = audioContent.isActive.isTrue
|
||||||
|
.and(audioContentTheme.isActive.isTrue)
|
||||||
|
|
||||||
|
if (!isAdult) {
|
||||||
|
where = where.and(audioContent.isAdult.isFalse)
|
||||||
|
} else {
|
||||||
|
if (contentType != ContentType.ALL) {
|
||||||
|
where = where.and(
|
||||||
|
audioContent.member.isNull.or(
|
||||||
|
audioContent.member.auth.gender.eq(
|
||||||
|
if (contentType == ContentType.MALE) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFree) {
|
||||||
|
where = where.and(audioContent.price.loe(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPointAvailableOnly) {
|
||||||
|
where = where.and(audioContent.isPointAvailable.isTrue)
|
||||||
|
}
|
||||||
|
|
||||||
|
val query = queryFactory
|
||||||
|
.select(audioContentTheme.id, audioContentTheme.theme)
|
||||||
|
.from(audioContent)
|
||||||
|
.innerJoin(audioContent.member, member)
|
||||||
|
.innerJoin(audioContent.theme, audioContentTheme)
|
||||||
|
.where(where)
|
||||||
|
.groupBy(audioContentTheme.id)
|
||||||
|
|
||||||
|
if (isFree) {
|
||||||
|
query.orderBy(
|
||||||
|
CaseBuilder()
|
||||||
|
.`when`(audioContentTheme.theme.eq("자기소개")).then(0)
|
||||||
|
.otherwise(1)
|
||||||
|
.asc(),
|
||||||
|
audioContentTheme.orders.asc()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
query.orderBy(audioContentTheme.orders.asc())
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.fetch().map { tuple ->
|
||||||
|
ThemeIdAndName(
|
||||||
|
id = tuple.get(audioContentTheme.id)!!,
|
||||||
|
theme = tuple.get(audioContentTheme.theme)!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun findThemeByIdAndActive(id: Long): AudioContentTheme? {
|
fun findThemeByIdAndActive(id: Long): AudioContentTheme? {
|
||||||
return queryFactory
|
return queryFactory
|
||||||
.selectFrom(audioContentTheme)
|
.selectFrom(audioContentTheme)
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ import kr.co.vividnext.sodalive.content.AudioContentRepository
|
|||||||
import kr.co.vividnext.sodalive.content.ContentType
|
import kr.co.vividnext.sodalive.content.ContentType
|
||||||
import kr.co.vividnext.sodalive.content.SortType
|
import kr.co.vividnext.sodalive.content.SortType
|
||||||
import kr.co.vividnext.sodalive.content.theme.content.GetContentByThemeResponse
|
import kr.co.vividnext.sodalive.content.theme.content.GetContentByThemeResponse
|
||||||
|
import kr.co.vividnext.sodalive.content.theme.translation.ContentThemeTranslation
|
||||||
|
import kr.co.vividnext.sodalive.content.theme.translation.ContentThemeTranslationRepository
|
||||||
|
import kr.co.vividnext.sodalive.i18n.Lang
|
||||||
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
|
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
|
||||||
|
import kr.co.vividnext.sodalive.i18n.translation.TranslateRequest
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
@@ -12,26 +18,94 @@ import org.springframework.transaction.annotation.Transactional
|
|||||||
@Service
|
@Service
|
||||||
class AudioContentThemeService(
|
class AudioContentThemeService(
|
||||||
private val queryRepository: AudioContentThemeQueryRepository,
|
private val queryRepository: AudioContentThemeQueryRepository,
|
||||||
private val contentRepository: AudioContentRepository
|
private val contentRepository: AudioContentRepository,
|
||||||
|
private val contentThemeTranslationRepository: ContentThemeTranslationRepository,
|
||||||
|
|
||||||
|
private val papagoTranslationService: PapagoTranslationService,
|
||||||
|
private val langContext: LangContext
|
||||||
) {
|
) {
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun getThemes(): List<GetAudioContentThemeResponse> {
|
fun getThemes(): List<GetAudioContentThemeResponse> {
|
||||||
return queryRepository.getActiveThemes()
|
return queryRepository.getActiveThemes()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional
|
||||||
fun getActiveThemeOfContent(
|
fun getActiveThemeOfContent(
|
||||||
isAdult: Boolean = false,
|
isAdult: Boolean = false,
|
||||||
isFree: Boolean = false,
|
isFree: Boolean = false,
|
||||||
isPointAvailableOnly: Boolean = false,
|
isPointAvailableOnly: Boolean = false,
|
||||||
contentType: ContentType
|
contentType: ContentType
|
||||||
): List<String> {
|
): List<String> {
|
||||||
return queryRepository.getActiveThemeOfContent(
|
val themesWithIds = queryRepository.getActiveThemeWithIdsOfContent(
|
||||||
isAdult = isAdult,
|
isAdult = isAdult,
|
||||||
isFree = isFree,
|
isFree = isFree,
|
||||||
isPointAvailableOnly = isPointAvailableOnly,
|
isPointAvailableOnly = isPointAvailableOnly,
|
||||||
contentType = contentType
|
contentType = contentType
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* langContext.lang == Lang.EN || Lang.JA 일 때 번역된 콘텐츠 테마 반환
|
||||||
|
* 번역이 없으면 번역 API 호출 후 저장하고 반환
|
||||||
|
*/
|
||||||
|
val currentLang = langContext.lang
|
||||||
|
if (currentLang == Lang.EN || currentLang == Lang.JA) {
|
||||||
|
val targetLocale = currentLang.code
|
||||||
|
// 1) 기존 번역을 한 번에 조회
|
||||||
|
val ids = themesWithIds.map { it.id }
|
||||||
|
val existingTranslations = if (ids.isNotEmpty()) {
|
||||||
|
contentThemeTranslationRepository.findByContentThemeIdInAndLocale(ids, targetLocale)
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val existingMap = existingTranslations.associateBy { it.contentThemeId }
|
||||||
|
|
||||||
|
// 2) 미번역 항목만 수집하여 한 번에 번역 요청
|
||||||
|
val untranslatedPairs = themesWithIds.filter { existingMap[it.id] == null }
|
||||||
|
|
||||||
|
if (untranslatedPairs.isNotEmpty()) {
|
||||||
|
val texts = untranslatedPairs.map { it.theme }
|
||||||
|
|
||||||
|
val response = papagoTranslationService.translate(
|
||||||
|
request = TranslateRequest(
|
||||||
|
texts = texts,
|
||||||
|
sourceLanguage = "ko",
|
||||||
|
targetLanguage = targetLocale
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val translatedTexts = response.translatedText
|
||||||
|
val entitiesToSave = mutableListOf<ContentThemeTranslation>()
|
||||||
|
|
||||||
|
// translatedTexts 크기가 다르면 안전하게 원문으로 대체
|
||||||
|
untranslatedPairs.forEachIndexed { index, pair ->
|
||||||
|
val translated = translatedTexts.getOrNull(index) ?: pair.theme
|
||||||
|
entitiesToSave.add(
|
||||||
|
ContentThemeTranslation(
|
||||||
|
contentThemeId = pair.id,
|
||||||
|
locale = targetLocale,
|
||||||
|
theme = translated
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entitiesToSave.isNotEmpty()) {
|
||||||
|
contentThemeTranslationRepository.saveAll(entitiesToSave)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 저장 후 맵을 갱신
|
||||||
|
entitiesToSave.forEach { entity ->
|
||||||
|
(existingMap as MutableMap)[entity.contentThemeId] = entity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 원래 순서대로 결과 조립 (번역 없으면 원문 fallback)
|
||||||
|
return themesWithIds.map { pair ->
|
||||||
|
existingMap[pair.id]?.theme ?: pair.theme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return themesWithIds.map { it.theme }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package kr.co.vividnext.sodalive.content.theme.translation
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import javax.persistence.Entity
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
class ContentThemeTranslation(
|
||||||
|
val contentThemeId: Long,
|
||||||
|
val locale: String,
|
||||||
|
var theme: String
|
||||||
|
) : BaseEntity()
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package kr.co.vividnext.sodalive.content.theme.translation
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
|
||||||
|
interface ContentThemeTranslationRepository : JpaRepository<ContentThemeTranslation, Long> {
|
||||||
|
fun findByContentThemeIdAndLocale(contentThemeId: Long, locale: String): ContentThemeTranslation?
|
||||||
|
|
||||||
|
fun findByContentThemeIdInAndLocale(contentThemeIds: Collection<Long>, locale: String): List<ContentThemeTranslation>
|
||||||
|
}
|
||||||
@@ -7,6 +7,9 @@ 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.theme.AudioContentThemeQueryRepository
|
||||||
|
import kr.co.vividnext.sodalive.content.theme.translation.ContentThemeTranslation
|
||||||
|
import kr.co.vividnext.sodalive.content.theme.translation.ContentThemeTranslationRepository
|
||||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslation
|
import kr.co.vividnext.sodalive.content.translation.ContentTranslation
|
||||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslationPayload
|
import kr.co.vividnext.sodalive.content.translation.ContentTranslationPayload
|
||||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
|
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
|
||||||
@@ -21,7 +24,8 @@ import org.springframework.transaction.event.TransactionalEventListener
|
|||||||
|
|
||||||
enum class LanguageTranslationTargetType {
|
enum class LanguageTranslationTargetType {
|
||||||
CONTENT,
|
CONTENT,
|
||||||
CHARACTER
|
CHARACTER,
|
||||||
|
CONTENT_THEME
|
||||||
}
|
}
|
||||||
|
|
||||||
class LanguageTranslationEvent(
|
class LanguageTranslationEvent(
|
||||||
@@ -33,9 +37,11 @@ class LanguageTranslationEvent(
|
|||||||
class LanguageTranslationListener(
|
class LanguageTranslationListener(
|
||||||
private val audioContentRepository: AudioContentRepository,
|
private val audioContentRepository: AudioContentRepository,
|
||||||
private val chatCharacterRepository: ChatCharacterRepository,
|
private val chatCharacterRepository: ChatCharacterRepository,
|
||||||
|
private val audioContentThemeRepository: AudioContentThemeQueryRepository,
|
||||||
|
|
||||||
private val contentTranslationRepository: ContentTranslationRepository,
|
private val contentTranslationRepository: ContentTranslationRepository,
|
||||||
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
|
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
|
||||||
|
private val contentThemeTranslationRepository: ContentThemeTranslationRepository,
|
||||||
|
|
||||||
private val translationService: PapagoTranslationService
|
private val translationService: PapagoTranslationService
|
||||||
) {
|
) {
|
||||||
@@ -46,6 +52,7 @@ class LanguageTranslationListener(
|
|||||||
when (event.targetType) {
|
when (event.targetType) {
|
||||||
LanguageTranslationTargetType.CONTENT -> handleContentLanguageTranslation(event)
|
LanguageTranslationTargetType.CONTENT -> handleContentLanguageTranslation(event)
|
||||||
LanguageTranslationTargetType.CHARACTER -> handleCharacterLanguageTranslation(event)
|
LanguageTranslationTargetType.CHARACTER -> handleCharacterLanguageTranslation(event)
|
||||||
|
LanguageTranslationTargetType.CONTENT_THEME -> handleContentThemeLanguageTranslation(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,4 +209,43 @@ class LanguageTranslationListener(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleContentThemeLanguageTranslation(event: LanguageTranslationEvent) {
|
||||||
|
val contentTheme = audioContentThemeRepository.findThemeByIdAndActive(event.id) ?: return
|
||||||
|
|
||||||
|
val sourceLanguage = "ko"
|
||||||
|
getTranslatableLanguageCodes(sourceLanguage).forEach { locale ->
|
||||||
|
val texts = mutableListOf<String>()
|
||||||
|
texts.add(contentTheme.theme)
|
||||||
|
|
||||||
|
val response = translationService.translate(
|
||||||
|
request = TranslateRequest(
|
||||||
|
texts = texts,
|
||||||
|
sourceLanguage = sourceLanguage,
|
||||||
|
targetLanguage = locale
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val translatedTexts = response.translatedText
|
||||||
|
if (translatedTexts.size == texts.size) {
|
||||||
|
val translatedTheme = translatedTexts[0]
|
||||||
|
|
||||||
|
val existing = contentThemeTranslationRepository
|
||||||
|
.findByContentThemeIdAndLocale(contentTheme.id!!, locale)
|
||||||
|
|
||||||
|
if (existing == null) {
|
||||||
|
contentThemeTranslationRepository.save(
|
||||||
|
ContentThemeTranslation(
|
||||||
|
contentThemeId = contentTheme.id!!,
|
||||||
|
locale = locale,
|
||||||
|
theme = translatedTheme
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
existing.theme = translatedTheme
|
||||||
|
contentThemeTranslationRepository.save(existing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user