캐릭터 번역 캐시 및 응답 필드 추가
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.chat.character.controller
|
||||
|
||||
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentService
|
||||
import kr.co.vividnext.sodalive.chat.character.curation.CharacterCurationQueryService
|
||||
import kr.co.vividnext.sodalive.chat.character.dto.Character
|
||||
import kr.co.vividnext.sodalive.chat.character.dto.CharacterBackgroundResponse
|
||||
import kr.co.vividnext.sodalive.chat.character.dto.CharacterBannerResponse
|
||||
@@ -12,9 +13,17 @@ import kr.co.vividnext.sodalive.chat.character.dto.OtherCharacter
|
||||
import kr.co.vividnext.sodalive.chat.character.dto.RecentCharacter
|
||||
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService
|
||||
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
||||
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.AiCharacterTranslationRepository
|
||||
import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterBackground
|
||||
import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterDetail
|
||||
import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterPersonality
|
||||
import kr.co.vividnext.sodalive.chat.room.service.ChatRoomService
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
|
||||
import kr.co.vividnext.sodalive.i18n.translation.TranslateRequest
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.data.domain.PageRequest
|
||||
@@ -32,7 +41,10 @@ class ChatCharacterController(
|
||||
private val bannerService: ChatCharacterBannerService,
|
||||
private val chatRoomService: ChatRoomService,
|
||||
private val characterCommentService: CharacterCommentService,
|
||||
private val curationQueryService: kr.co.vividnext.sodalive.chat.character.curation.CharacterCurationQueryService,
|
||||
private val curationQueryService: CharacterCurationQueryService,
|
||||
|
||||
private val translationService: PapagoTranslationService,
|
||||
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
|
||||
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val imageHost: String
|
||||
@@ -119,6 +131,7 @@ class ChatCharacterController(
|
||||
@GetMapping("/{characterId}")
|
||||
fun getCharacterDetail(
|
||||
@PathVariable characterId: Long,
|
||||
@RequestParam(required = false, defaultValue = "ko") languageCode: String? = "ko",
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
@@ -148,6 +161,122 @@ class ChatCharacterController(
|
||||
)
|
||||
}
|
||||
|
||||
var translated: TranslatedAiCharacterDetail? = null
|
||||
if (!languageCode.isNullOrBlank() && languageCode != character.languageCode) {
|
||||
val locale = languageCode.lowercase()
|
||||
|
||||
val existing = aiCharacterTranslationRepository
|
||||
.findByCharacterIdAndLocale(character.id!!, locale)
|
||||
|
||||
if (existing != null) {
|
||||
val payload = existing.renderedPayload
|
||||
translated = TranslatedAiCharacterDetail(
|
||||
name = payload.name,
|
||||
description = payload.description,
|
||||
gender = payload.gender,
|
||||
personality = TranslatedAiCharacterPersonality(
|
||||
trait = payload.personalityTrait,
|
||||
description = payload.personalityDescription
|
||||
).takeIf {
|
||||
(it.trait?.isNotBlank() == true) || (it.description?.isNotBlank() == true)
|
||||
},
|
||||
background = TranslatedAiCharacterBackground(
|
||||
topic = payload.backgroundTopic,
|
||||
description = payload.backgroundDescription
|
||||
).takeIf {
|
||||
(it.topic?.isNotBlank() == true) || (it.description?.isNotBlank() == true)
|
||||
},
|
||||
tags = payload.tags
|
||||
)
|
||||
} else {
|
||||
val texts = mutableListOf<String>()
|
||||
texts.add(character.name)
|
||||
texts.add(character.description)
|
||||
texts.add(character.gender ?: "")
|
||||
|
||||
val hasPersonality = personality != null
|
||||
if (hasPersonality) {
|
||||
texts.add(personality!!.trait)
|
||||
texts.add(personality.description)
|
||||
}
|
||||
|
||||
val hasBackground = background != null
|
||||
if (hasBackground) {
|
||||
texts.add(background!!.topic)
|
||||
texts.add(background.description)
|
||||
}
|
||||
|
||||
texts.add(tags)
|
||||
|
||||
val sourceLanguage = character.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 translatedName = translatedTexts[index++]
|
||||
val translatedDescription = translatedTexts[index++]
|
||||
val translatedGender = translatedTexts[index++]
|
||||
|
||||
var translatedPersonality: TranslatedAiCharacterPersonality? = null
|
||||
if (hasPersonality) {
|
||||
translatedPersonality = TranslatedAiCharacterPersonality(
|
||||
trait = translatedTexts[index++],
|
||||
description = translatedTexts[index++]
|
||||
)
|
||||
}
|
||||
|
||||
var translatedBackground: TranslatedAiCharacterBackground? = null
|
||||
if (hasBackground) {
|
||||
translatedBackground = TranslatedAiCharacterBackground(
|
||||
topic = translatedTexts[index++],
|
||||
description = translatedTexts[index++]
|
||||
)
|
||||
}
|
||||
|
||||
val translatedTags = translatedTexts[index]
|
||||
|
||||
val payload = AiCharacterTranslationRenderedPayload(
|
||||
name = translatedName,
|
||||
description = translatedDescription,
|
||||
gender = translatedGender,
|
||||
personalityTrait = translatedPersonality?.trait ?: "",
|
||||
personalityDescription = translatedPersonality?.description ?: "",
|
||||
backgroundTopic = translatedBackground?.topic ?: "",
|
||||
backgroundDescription = translatedBackground?.description ?: "",
|
||||
tags = translatedTags
|
||||
)
|
||||
|
||||
val entity = AiCharacterTranslation(
|
||||
characterId = character.id!!,
|
||||
locale = locale,
|
||||
translatedName = translatedName,
|
||||
translatedTags = translatedTags,
|
||||
renderedPayload = payload
|
||||
)
|
||||
|
||||
aiCharacterTranslationRepository.save(entity)
|
||||
|
||||
translated = TranslatedAiCharacterDetail(
|
||||
name = translatedName,
|
||||
description = translatedDescription,
|
||||
gender = translatedGender,
|
||||
personality = translatedPersonality,
|
||||
background = translatedBackground,
|
||||
tags = translatedTags
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 다른 캐릭터 조회 (태그 기반, 랜덤 10개, 현재 캐릭터 제외)
|
||||
val others = service.getOtherCharactersBySharedTags(characterId, 10)
|
||||
.map { other ->
|
||||
@@ -184,7 +313,8 @@ class ChatCharacterController(
|
||||
characterType = character.characterType,
|
||||
others = others,
|
||||
latestComment = latestComment,
|
||||
totalComments = characterCommentService.getTotalCommentCount(character.id!!)
|
||||
totalComments = characterCommentService.getTotalCommentCount(character.id!!),
|
||||
translated = translated
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.chat.character.dto
|
||||
|
||||
import kr.co.vividnext.sodalive.chat.character.CharacterType
|
||||
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentResponse
|
||||
import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterDetail
|
||||
|
||||
data class CharacterDetailResponse(
|
||||
val characterId: Long,
|
||||
@@ -20,7 +21,8 @@ data class CharacterDetailResponse(
|
||||
val characterType: CharacterType,
|
||||
val others: List<OtherCharacter>,
|
||||
val latestComment: CharacterCommentResponse?,
|
||||
val totalComments: Int
|
||||
val totalComments: Int,
|
||||
val translated: TranslatedAiCharacterDetail?
|
||||
)
|
||||
|
||||
data class OtherCharacter(
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package kr.co.vividnext.sodalive.chat.character.translate
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||
import javax.persistence.AttributeConverter
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Convert
|
||||
import javax.persistence.Converter
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Table
|
||||
import javax.persistence.UniqueConstraint
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
uniqueConstraints = [
|
||||
UniqueConstraint(columnNames = ["character_id", "locale"])
|
||||
]
|
||||
)
|
||||
class AiCharacterTranslation(
|
||||
val characterId: Long,
|
||||
val locale: String,
|
||||
val translatedName: String,
|
||||
val translatedTags: String,
|
||||
|
||||
@Column(columnDefinition = "json")
|
||||
@Convert(converter = AiCharacterTranslationRenderedPayloadConverter::class)
|
||||
val renderedPayload: AiCharacterTranslationRenderedPayload
|
||||
) : BaseEntity()
|
||||
|
||||
data class AiCharacterTranslationRenderedPayload(
|
||||
val name: String,
|
||||
val description: String,
|
||||
val gender: String,
|
||||
val personalityTrait: String,
|
||||
val personalityDescription: String,
|
||||
val backgroundTopic: String,
|
||||
val backgroundDescription: String,
|
||||
val tags: String
|
||||
)
|
||||
|
||||
@Converter(autoApply = false)
|
||||
class AiCharacterTranslationRenderedPayloadConverter :
|
||||
AttributeConverter<AiCharacterTranslationRenderedPayload, String> {
|
||||
|
||||
override fun convertToDatabaseColumn(attribute: AiCharacterTranslationRenderedPayload?): String {
|
||||
if (attribute == null) return "{}"
|
||||
return objectMapper.writeValueAsString(attribute)
|
||||
}
|
||||
|
||||
override fun convertToEntityAttribute(dbData: String?): AiCharacterTranslationRenderedPayload {
|
||||
if (dbData.isNullOrBlank()) {
|
||||
return AiCharacterTranslationRenderedPayload(
|
||||
name = "",
|
||||
description = "",
|
||||
gender = "",
|
||||
personalityTrait = "",
|
||||
personalityDescription = "",
|
||||
backgroundTopic = "",
|
||||
backgroundDescription = "",
|
||||
tags = ""
|
||||
)
|
||||
}
|
||||
return objectMapper.readValue(dbData)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val objectMapper = jacksonObjectMapper()
|
||||
}
|
||||
}
|
||||
|
||||
data class TranslatedAiCharacterDetail(
|
||||
val name: String?,
|
||||
val description: String?,
|
||||
val gender: String?,
|
||||
val personality: TranslatedAiCharacterPersonality?,
|
||||
val background: TranslatedAiCharacterBackground?,
|
||||
val tags: String?
|
||||
)
|
||||
|
||||
data class TranslatedAiCharacterPersonality(
|
||||
val trait: String?,
|
||||
val description: String?
|
||||
)
|
||||
|
||||
data class TranslatedAiCharacterBackground(
|
||||
val topic: String?,
|
||||
val description: String?
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.chat.character.translate
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
interface AiCharacterTranslationRepository : JpaRepository<AiCharacterTranslation, Long> {
|
||||
fun findByCharacterIdAndLocale(characterId: Long, locale: String): AiCharacterTranslation?
|
||||
}
|
||||
Reference in New Issue
Block a user