AI Character API에서 api 마다 languageCode를 별도로 받던 것을 LangContext를 사용하도록 리팩토링

This commit is contained in:
2025-12-12 19:09:33 +09:00
parent ba1844a6c2
commit 165640201f

View File

@@ -23,6 +23,7 @@ import kr.co.vividnext.sodalive.chat.character.translate.TranslatedAiCharacterPe
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.LangContext
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
import kr.co.vividnext.sodalive.i18n.translation.TranslateRequest
import kr.co.vividnext.sodalive.member.Member
@@ -34,7 +35,6 @@ import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import kotlin.collections.map
@RestController
@RequestMapping("/api/chat/character")
@@ -48,12 +48,13 @@ class ChatCharacterController(
private val translationService: PapagoTranslationService,
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
private val langContext: LangContext,
@Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String
) {
@GetMapping("/main")
fun getCharacterMain(
@RequestParam(required = false) languageCode: String? = null,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
): ApiResponse<CharacterMainResponse> = run {
// 배너 조회 (최대 10개)
@@ -80,32 +81,19 @@ class ChatCharacterController(
}
}
/**
* 최근 대화한 캐릭터 이름 번역 데이터 조회
*
* languageCode != null
* aiCharacterTranslationRepository 이용해 번역 콘텐츠를 조회한다. - characterId, locale
*
* 한 번에 조회하고 characterId 매핑하여 recentCharacters 캐릭터 이름을 번역 데이터로 변경한다
*/
val translatedRecentCharacters = if (!languageCode.isNullOrBlank()) {
val characterIds = recentCharacters.map { it.characterId }
val characterIds = recentCharacters.map { it.characterId }
val translatedRecentCharacters = if (characterIds.isNotEmpty()) {
val translations = aiCharacterTranslationRepository
.findByCharacterIdInAndLocale(characterIds = characterIds, locale = langContext.lang.code)
.associateBy { it.characterId }
if (characterIds.isNotEmpty()) {
val translations = aiCharacterTranslationRepository
.findByCharacterIdInAndLocale(characterIds = characterIds, locale = languageCode)
.associateBy { it.characterId }
recentCharacters.map { character ->
val translatedName = translations[character.characterId]?.renderedPayload?.name
if (translatedName.isNullOrBlank()) {
character
} else {
character.copy(name = translatedName)
}
recentCharacters.map { character ->
val translatedName = translations[character.characterId]?.renderedPayload?.name
if (translatedName.isNullOrBlank()) {
character
} else {
character.copy(name = translatedName)
}
} else {
recentCharacters
}
} else {
recentCharacters
@@ -114,76 +102,12 @@ class ChatCharacterController(
// 인기 캐릭터 조회
val popularCharacters = service.getPopularCharacters()
/**
* popularCharacters 캐릭터 이름과 description 번역 데이터 조회
*
* languageCode != null
* aiCharacterTranslationRepository 이용해 번역 콘텐츠를 조회한다. - characterId, locale
*
* 한 번에 조회하고 characterId 매핑하여 popularCharacters의 캐릭터 이름과 description을 번역 데이터로 변경한다
*/
val translatedPopularCharacters = if (!languageCode.isNullOrBlank()) {
val characterIds = popularCharacters.map { it.characterId }
if (characterIds.isNotEmpty()) {
val translations = aiCharacterTranslationRepository
.findByCharacterIdInAndLocale(characterIds = characterIds, locale = languageCode)
.associateBy { it.characterId }
popularCharacters.map { character ->
val translatedName = translations[character.characterId]?.renderedPayload?.name
val translatedDesc = translations[character.characterId]?.renderedPayload?.description
if (translatedName.isNullOrBlank() || translatedDesc.isNullOrBlank()) {
character
} else {
character.copy(name = translatedName, description = translatedDesc)
}
}
} else {
popularCharacters
}
} else {
popularCharacters
}
// 최근 등록된 캐릭터 리스트 조회
val newCharacters = service.getRecentCharactersPage(
page = 0,
size = 50
).content
/**
* 최근 등록된 캐릭터 이름 번역 데이터 조회
*
* languageCode != null
* aiCharacterTranslationRepository 이용해 번역 콘텐츠를 조회한다. - characterId, locale
*
* 한 번에 조회하고 characterId 매핑하여 newCharacters 캐릭터 이름과 description을 번역 데이터로 변경한다
*/
val translatedNewCharacters = if (!languageCode.isNullOrBlank()) {
val characterIds = newCharacters.map { it.characterId }
if (characterIds.isNotEmpty()) {
val translations = aiCharacterTranslationRepository
.findByCharacterIdInAndLocale(characterIds = characterIds, locale = languageCode)
.associateBy { it.characterId }
newCharacters.map { character ->
val translatedName = translations[character.characterId]?.renderedPayload?.name
val translatedDesc = translations[character.characterId]?.renderedPayload?.description
if (translatedName.isNullOrBlank() || translatedDesc.isNullOrBlank()) {
character
} else {
character.copy(name = translatedName, description = translatedDesc)
}
}
} else {
newCharacters
}
} else {
newCharacters
}
// 추천 캐릭터 조회
// 최근 대화한 캐릭터를 제외한 랜덤 30개 조회
// Controller에서는 호출만
@@ -191,38 +115,6 @@ class ChatCharacterController(
val excludeIds = recentCharacters.map { it.characterId }
val recommendCharacters = service.getRecommendCharacters(excludeIds, 30)
/**
* 추천 캐릭터 이름 번역 데이터 조회
*
* languageCode != null
* aiCharacterTranslationRepository 이용해 번역 콘텐츠를 조회한다. - characterId, locale
*
* 한 번에 조회하고 characterId 매핑하여 recommendCharacters 캐릭터 이름과 description을 번역 데이터로 변경한다
*/
val translatedRecommendCharacters = if (!languageCode.isNullOrBlank()) {
val characterIds = recommendCharacters.map { it.characterId }
if (characterIds.isNotEmpty()) {
val translations = aiCharacterTranslationRepository
.findByCharacterIdInAndLocale(characterIds = characterIds, locale = languageCode)
.associateBy { it.characterId }
recommendCharacters.map { character ->
val translatedName = translations[character.characterId]?.renderedPayload?.name
val translatedDesc = translations[character.characterId]?.renderedPayload?.description
if (translatedName.isNullOrBlank() || translatedDesc.isNullOrBlank()) {
character
} else {
character.copy(name = translatedName, description = translatedDesc)
}
}
} else {
recommendCharacters
}
} else {
recommendCharacters
}
// 큐레이션 섹션 (활성화된 큐레이션 + 캐릭터)
val curationSections = curationQueryService.getActiveCurationsWithCharacters()
.map { agg ->
@@ -246,9 +138,9 @@ class ChatCharacterController(
CharacterMainResponse(
banners = banners,
recentCharacters = translatedRecentCharacters,
popularCharacters = translatedPopularCharacters,
newCharacters = translatedNewCharacters,
recommendCharacters = translatedRecommendCharacters,
popularCharacters = getTranslatedAiCharacterList(popularCharacters),
newCharacters = getTranslatedAiCharacterList(newCharacters),
recommendCharacters = getTranslatedAiCharacterList(recommendCharacters),
curationSections = curationSections
)
)
@@ -261,7 +153,6 @@ 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("로그인 정보를 확인해주세요.")
@@ -292,11 +183,9 @@ class ChatCharacterController(
}
var translated: TranslatedAiCharacterDetail? = null
if (!languageCode.isNullOrBlank() && languageCode != character.languageCode) {
val locale = languageCode.lowercase()
if (langContext.lang.code != character.languageCode) {
val existing = aiCharacterTranslationRepository
.findByCharacterIdAndLocale(character.id!!, locale)
.findByCharacterIdAndLocale(character.id!!, langContext.lang.code)
if (existing != null) {
val payload = existing.renderedPayload
@@ -344,7 +233,7 @@ class ChatCharacterController(
request = TranslateRequest(
texts = texts,
sourceLanguage = sourceLanguage,
targetLanguage = locale
targetLanguage = langContext.lang.code
)
)
@@ -387,7 +276,7 @@ class ChatCharacterController(
val entity = AiCharacterTranslation(
characterId = character.id!!,
locale = locale,
locale = langContext.lang.code,
renderedPayload = payload
)
@@ -427,27 +316,22 @@ class ChatCharacterController(
*
* 한 번에 조회하고 characterId 매핑하여 others 캐릭터 이름과 tags 번역 데이터로 변경한다
*/
val translatedOthers = if (!languageCode.isNullOrBlank()) {
val characterIds = others.map { it.characterId }
val characterIds = others.map { it.characterId }
val translatedOthers = if (characterIds.isNotEmpty()) {
val translations = aiCharacterTranslationRepository
.findByCharacterIdInAndLocale(characterIds = characterIds, locale = langContext.lang.code)
.associateBy { it.characterId }
if (characterIds.isNotEmpty()) {
val translations = aiCharacterTranslationRepository
.findByCharacterIdInAndLocale(characterIds = characterIds, locale = languageCode)
.associateBy { it.characterId }
others.map { other ->
val payload = translations[other.characterId]?.renderedPayload
val translatedName = payload?.name
val translatedTags = payload?.tags
others.map { other ->
val payload = translations[other.characterId]?.renderedPayload
val translatedName = payload?.name
val translatedTags = payload?.tags
if (translatedName.isNullOrBlank() || translatedTags.isNullOrBlank()) {
other
} else {
other.copy(name = translatedName, tags = translatedTags)
}
if (translatedName.isNullOrBlank() || translatedTags.isNullOrBlank()) {
other
} else {
other.copy(name = translatedName, tags = translatedTags)
}
} else {
others
}
} else {
others
@@ -488,7 +372,6 @@ class ChatCharacterController(
*/
@GetMapping("/recent")
fun getRecentCharacters(
@RequestParam(required = false) languageCode: String? = null,
@RequestParam("page", required = false) page: Int?
): ApiResponse<RecentCharactersResponse> = run {
val characterPage = service.getRecentCharactersPage(
@@ -496,41 +379,9 @@ class ChatCharacterController(
size = 20
)
/**
* 최근 등록된 캐릭터 이름 번역 데이터 조회
*
* languageCode != null
* aiCharacterTranslationRepository 이용해 번역 콘텐츠를 조회한다. - characterId, locale
*
* 한 번에 조회하고 characterId 매핑하여 characterList 캐릭터 이름과 description을 번역 데이터로 변경한다
*/
val translatedContent = if (!languageCode.isNullOrBlank()) {
val characterIds = characterPage.content.map { it.characterId }
if (characterIds.isNotEmpty()) {
val translations = aiCharacterTranslationRepository
.findByCharacterIdInAndLocale(characterIds = characterIds, locale = languageCode)
.associateBy { it.characterId }
characterPage.content.map { character ->
val translatedName = translations[character.characterId]?.renderedPayload?.name
val translatedDesc = translations[character.characterId]?.renderedPayload?.description
if (translatedName.isNullOrBlank() || translatedDesc.isNullOrBlank()) {
character
} else {
character.copy(name = translatedName, description = translatedDesc)
}
}
} else {
characterPage.content
}
} else {
characterPage.content
}
val translatedCharacterPage = RecentCharactersResponse(
totalCount = characterPage.totalCount,
content = translatedContent
content = getTranslatedAiCharacterList(characterPage.content)
)
ApiResponse.ok(translatedCharacterPage)
@@ -543,7 +394,6 @@ class ChatCharacterController(
*/
@GetMapping("/recommend")
fun getRecommendCharacters(
@RequestParam(required = false) languageCode: String? = null,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
val recent = if (member == null || member.auth == null) {
@@ -553,40 +403,48 @@ class ChatCharacterController(
.listMyChatRooms(member, 0, 50) // 최근 기록은 최대 50개까지만 제외 대상으로 고려
.map { it.characterId }
}
val characterList = service.getRecommendCharacters(recent, 20)
/**
* 추천 캐릭터 이름 번역 데이터 조회
*
* languageCode != null
* aiCharacterTranslationRepository 이용해 번역 콘텐츠를 조회한다. - characterId, locale
*
* 한 번에 조회하고 characterId 매핑하여 characterList 캐릭터 이름과 description을 번역 데이터로 변경한다
*/
val translatedCharacterList = if (!languageCode.isNullOrBlank()) {
val characterIds = characterList.map { it.characterId }
ApiResponse.ok(
getTranslatedAiCharacterList(
service.getRecommendCharacters(
recent,
20
)
)
)
}
if (characterIds.isNotEmpty()) {
val translations = aiCharacterTranslationRepository
.findByCharacterIdInAndLocale(characterIds = characterIds, locale = languageCode)
.associateBy { it.characterId }
/**
* AI 캐릭터 리스트의 이름/설명을 현재 언어(locale)에 맞춰 일괄 번역한다.
*
* 처리 절차:
* - characterId 목록을 추출하고, 요청 언어 코드로 aiCharacterTranslationRepository에서
* 번역 데이터를 한 번에 조회한다.
* - 각 캐릭터에 대해 name과 description 모두 번역 값이 존재하고 비어있지 않을 때에만
* 해당 필드를 교체한다. 둘 중 하나라도 없으면 원본 캐릭터를 그대로 유지한다.
*
* @param aiCharacterList 번역 대상 캐릭터 목록
* @return 가능한 경우 name/description이 번역된 캐릭터 목록, 그 외는 원본 유지
*/
private fun getTranslatedAiCharacterList(aiCharacterList: List<Character>): List<Character> {
val characterIds = aiCharacterList.map { it.characterId }
characterList.map { character ->
val translatedName = translations[character.characterId]?.renderedPayload?.name
val translatedDesc = translations[character.characterId]?.renderedPayload?.description
if (translatedName.isNullOrBlank() || translatedDesc.isNullOrBlank()) {
character
} else {
character.copy(name = translatedName, description = translatedDesc)
}
return if (characterIds.isNotEmpty()) {
val translations = aiCharacterTranslationRepository
.findByCharacterIdInAndLocale(characterIds = characterIds, locale = langContext.lang.code)
.associateBy { it.characterId }
aiCharacterList.map { character ->
val translatedName = translations[character.characterId]?.renderedPayload?.name
val translatedDesc = translations[character.characterId]?.renderedPayload?.description
if (translatedName.isNullOrBlank() || translatedDesc.isNullOrBlank()) {
character
} else {
character.copy(name = translatedName, description = translatedDesc)
}
} else {
characterList
}
} else {
characterList
aiCharacterList
}
ApiResponse.ok(translatedCharacterList)
}
}