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