캐릭터 번역 캐시 및 응답 필드 추가
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package kr.co.vividnext.sodalive.chat.character.controller
|
package kr.co.vividnext.sodalive.chat.character.controller
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentService
|
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.Character
|
||||||
import kr.co.vividnext.sodalive.chat.character.dto.CharacterBackgroundResponse
|
import kr.co.vividnext.sodalive.chat.character.dto.CharacterBackgroundResponse
|
||||||
import kr.co.vividnext.sodalive.chat.character.dto.CharacterBannerResponse
|
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.dto.RecentCharacter
|
||||||
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService
|
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.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.chat.room.service.ChatRoomService
|
||||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
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 kr.co.vividnext.sodalive.member.Member
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.data.domain.PageRequest
|
import org.springframework.data.domain.PageRequest
|
||||||
@@ -32,7 +41,10 @@ class ChatCharacterController(
|
|||||||
private val bannerService: ChatCharacterBannerService,
|
private val bannerService: ChatCharacterBannerService,
|
||||||
private val chatRoomService: ChatRoomService,
|
private val chatRoomService: ChatRoomService,
|
||||||
private val characterCommentService: CharacterCommentService,
|
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}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
private val imageHost: String
|
private val imageHost: String
|
||||||
@@ -119,6 +131,7 @@ class ChatCharacterController(
|
|||||||
@GetMapping("/{characterId}")
|
@GetMapping("/{characterId}")
|
||||||
fun getCharacterDetail(
|
fun getCharacterDetail(
|
||||||
@PathVariable characterId: Long,
|
@PathVariable characterId: Long,
|
||||||
|
@RequestParam(required = false, defaultValue = "ko") languageCode: String? = "ko",
|
||||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
) = run {
|
) = run {
|
||||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
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개, 현재 캐릭터 제외)
|
// 다른 캐릭터 조회 (태그 기반, 랜덤 10개, 현재 캐릭터 제외)
|
||||||
val others = service.getOtherCharactersBySharedTags(characterId, 10)
|
val others = service.getOtherCharactersBySharedTags(characterId, 10)
|
||||||
.map { other ->
|
.map { other ->
|
||||||
@@ -184,7 +313,8 @@ class ChatCharacterController(
|
|||||||
characterType = character.characterType,
|
characterType = character.characterType,
|
||||||
others = others,
|
others = others,
|
||||||
latestComment = latestComment,
|
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.CharacterType
|
||||||
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentResponse
|
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentResponse
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterDetail
|
||||||
|
|
||||||
data class CharacterDetailResponse(
|
data class CharacterDetailResponse(
|
||||||
val characterId: Long,
|
val characterId: Long,
|
||||||
@@ -20,7 +21,8 @@ data class CharacterDetailResponse(
|
|||||||
val characterType: CharacterType,
|
val characterType: CharacterType,
|
||||||
val others: List<OtherCharacter>,
|
val others: List<OtherCharacter>,
|
||||||
val latestComment: CharacterCommentResponse?,
|
val latestComment: CharacterCommentResponse?,
|
||||||
val totalComments: Int
|
val totalComments: Int,
|
||||||
|
val translated: TranslatedAiCharacterDetail?
|
||||||
)
|
)
|
||||||
|
|
||||||
data class OtherCharacter(
|
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