feat(chat-banner): 다국어 캐릭터 배너 등록과 노출을 지원한다

배너를 언어별로 저장하고 요청 언어 우선 조회 후 한국어로 fallback 하도록 맞춘다.
This commit is contained in:
2026-04-02 15:32:42 +09:00
parent 06acfae1c9
commit ee14389786
12 changed files with 543 additions and 5 deletions
@@ -0,0 +1,128 @@
package kr.co.vividnext.sodalive.admin.chat
import com.amazonaws.services.s3.AmazonS3Client
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.admin.chat.character.service.AdminChatCharacterService
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
import kr.co.vividnext.sodalive.chat.character.ChatCharacterBanner
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.springframework.mock.web.MockMultipartFile
import java.net.URL
class AdminChatBannerControllerTest {
private val bannerService = Mockito.mock(ChatCharacterBannerService::class.java)
private val adminCharacterService = Mockito.mock(AdminChatCharacterService::class.java)
private val amazonS3Client = Mockito.mock(AmazonS3Client::class.java)
private val s3Uploader = S3Uploader(amazonS3Client)
private val controller = AdminChatBannerController(
bannerService = bannerService,
adminCharacterService = adminCharacterService,
s3Uploader = s3Uploader,
langContext = LangContext(),
messageSource = SodaMessageSource(),
s3Bucket = "test-bucket",
imageHost = "https://cdn.test"
)
@Test
fun shouldRegisterJapaneseBannerThroughAdminApi() {
val image = MockMultipartFile("image", "banner.png", "image/png", "image".toByteArray())
val registeredBanner = createBanner(id = 10L, lang = Lang.JA, imagePath = "")
val updatedBanner = createBanner(id = 10L, lang = Lang.JA, imagePath = "")
Mockito.`when`(amazonS3Client.getUrl(Mockito.eq("test-bucket"), Mockito.anyString()))
.thenAnswer { URL("https://cdn.test/${it.arguments[1]}") }
Mockito.`when`(
bannerService.registerBanner(
characterId = 1L,
imagePath = "",
lang = Lang.JA
)
).thenReturn(registeredBanner)
Mockito.doAnswer {
updatedBanner.apply {
imagePath = it.arguments[1] as String
}
}.`when`(bannerService).updateBanner(Mockito.eq(10L), Mockito.anyString(), Mockito.isNull())
val response = controller.registerBanner(
image = image,
requestString = "{\"characterId\":1,\"lang\":\"ja\"}"
)
assertTrue(response.success)
assertEquals(10L, response.data?.id)
assertTrue(response.data?.imagePath?.startsWith("https://cdn.test/characters/banners/10/") == true)
Mockito.verify(bannerService).registerBanner(1L, "", Lang.JA)
}
@Test
fun shouldDeserializeIso639LanguageCodeToLangEnum() {
val request = ObjectMapper().readValue(
"{\"characterId\":1,\"lang\":\"en\"}",
kr.co.vividnext.sodalive.admin.chat.dto.ChatCharacterBannerRegisterRequest::class.java
)
assertEquals(Lang.EN, request.lang)
}
@Test
fun shouldRegisterKoreanBannerByDefaultWhenLangIsMissing() {
val image = MockMultipartFile("image", "banner.png", "image/png", "image".toByteArray())
val registeredBanner = createBanner(id = 11L, lang = Lang.KO, imagePath = "")
val updatedBanner = createBanner(id = 11L, lang = Lang.KO, imagePath = "")
Mockito.`when`(amazonS3Client.getUrl(Mockito.eq("test-bucket"), Mockito.anyString()))
.thenAnswer { URL("https://cdn.test/${it.arguments[1]}") }
Mockito.`when`(
bannerService.registerBanner(
characterId = 2L,
imagePath = "",
lang = null
)
).thenReturn(registeredBanner)
Mockito.doAnswer {
updatedBanner.apply {
imagePath = it.arguments[1] as String
}
}.`when`(bannerService).updateBanner(Mockito.eq(11L), Mockito.anyString(), Mockito.isNull())
val response = controller.registerBanner(
image = image,
requestString = "{\"characterId\":2}"
)
assertTrue(response.success)
assertEquals(11L, response.data?.id)
Mockito.verify(bannerService).registerBanner(2L, "", null)
}
private fun createBanner(id: Long, lang: Lang, imagePath: String): ChatCharacterBanner {
val character = ChatCharacter(
characterUUID = "character-$id",
name = "character-$id",
description = "description-$id",
systemPrompt = "system-prompt-$id"
)
character.id = id
return ChatCharacterBanner(
imagePath = imagePath,
chatCharacter = character,
sortOrder = 1,
lang = lang
).also {
it.id = id
}
}
}