feat(chat-banner): 다국어 캐릭터 배너 등록과 노출을 지원한다
배너를 언어별로 저장하고 요청 언어 우선 조회 후 한국어로 fallback 하도록 맞춘다.
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package kr.co.vividnext.sodalive.chat.character.controller
|
||||
|
||||
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
|
||||
import kr.co.vividnext.sodalive.chat.character.ChatCharacterBanner
|
||||
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.RecentCharactersResponse
|
||||
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.AiCharacterTranslationRepository
|
||||
import kr.co.vividnext.sodalive.chat.room.service.ChatRoomService
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
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.data.domain.PageImpl
|
||||
import org.springframework.data.domain.PageRequest
|
||||
|
||||
class ChatCharacterControllerTest {
|
||||
private val service = Mockito.mock(ChatCharacterService::class.java)
|
||||
private val bannerService = Mockito.mock(ChatCharacterBannerService::class.java)
|
||||
private val chatRoomService = Mockito.mock(ChatRoomService::class.java)
|
||||
private val characterCommentService = Mockito.mock(CharacterCommentService::class.java)
|
||||
private val curationQueryService = Mockito.mock(CharacterCurationQueryService::class.java)
|
||||
private val translationService = Mockito.mock(PapagoTranslationService::class.java)
|
||||
private val aiCharacterTranslationRepository = Mockito.mock(AiCharacterTranslationRepository::class.java)
|
||||
private val langContext = LangContext().apply { setLang(Lang.JA) }
|
||||
private val memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
|
||||
private val controller = ChatCharacterController(
|
||||
service = service,
|
||||
bannerService = bannerService,
|
||||
chatRoomService = chatRoomService,
|
||||
characterCommentService = characterCommentService,
|
||||
curationQueryService = curationQueryService,
|
||||
translationService = translationService,
|
||||
aiCharacterTranslationRepository = aiCharacterTranslationRepository,
|
||||
langContext = langContext,
|
||||
memberContentPreferenceService = memberContentPreferenceService,
|
||||
imageHost = "https://cdn.test"
|
||||
)
|
||||
|
||||
@Test
|
||||
fun shouldUseJapaneseBannerWhenLangContextIsJapanese() {
|
||||
val pageable = PageRequest.of(0, 10)
|
||||
val banner = createBanner(id = 1L, imagePath = "banner/jp.png", lang = Lang.JA)
|
||||
|
||||
Mockito.`when`(bannerService.getDisplayBanners(pageable, Lang.JA))
|
||||
.thenReturn(PageImpl(listOf(banner), pageable, 1))
|
||||
Mockito.`when`(service.getPopularCharacters(locale = "ja")).thenReturn(emptyList())
|
||||
Mockito.`when`(service.getRecentCharactersPage(page = 0, size = 50))
|
||||
.thenReturn(RecentCharactersResponse(totalCount = 0, content = emptyList()))
|
||||
Mockito.`when`(service.getRecommendCharacters(emptyList(), 30)).thenReturn(emptyList())
|
||||
Mockito.`when`(curationQueryService.getActiveCurationsWithCharacters()).thenReturn(emptyList())
|
||||
|
||||
val response = controller.getCharacterMain(member = null)
|
||||
|
||||
assertTrue(response.success)
|
||||
assertEquals(1, response.data?.banners?.size)
|
||||
assertEquals(1L, response.data?.banners?.first()?.characterId)
|
||||
assertEquals("https://cdn.test/banner/jp.png", response.data?.banners?.first()?.imageUrl)
|
||||
Mockito.verify(bannerService).getDisplayBanners(pageable, Lang.JA)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldUseEnglishRequestWithKoreanFallbackBanner() {
|
||||
val controller = ChatCharacterController(
|
||||
service = service,
|
||||
bannerService = bannerService,
|
||||
chatRoomService = chatRoomService,
|
||||
characterCommentService = characterCommentService,
|
||||
curationQueryService = curationQueryService,
|
||||
translationService = translationService,
|
||||
aiCharacterTranslationRepository = aiCharacterTranslationRepository,
|
||||
langContext = LangContext().apply { setLang(Lang.EN) },
|
||||
memberContentPreferenceService = memberContentPreferenceService,
|
||||
imageHost = "https://cdn.test"
|
||||
)
|
||||
val pageable = PageRequest.of(0, 10)
|
||||
val banner = createBanner(id = 2L, imagePath = "banner/ko.png", lang = Lang.KO)
|
||||
|
||||
Mockito.`when`(bannerService.getDisplayBanners(pageable, Lang.EN))
|
||||
.thenReturn(PageImpl(listOf(banner), pageable, 1))
|
||||
Mockito.`when`(service.getPopularCharacters(locale = "en")).thenReturn(emptyList())
|
||||
Mockito.`when`(service.getRecentCharactersPage(page = 0, size = 50))
|
||||
.thenReturn(RecentCharactersResponse(totalCount = 0, content = emptyList()))
|
||||
Mockito.`when`(service.getRecommendCharacters(emptyList(), 30)).thenReturn(emptyList())
|
||||
Mockito.`when`(curationQueryService.getActiveCurationsWithCharacters()).thenReturn(emptyList())
|
||||
|
||||
val response = controller.getCharacterMain(member = null)
|
||||
|
||||
assertTrue(response.success)
|
||||
assertEquals("https://cdn.test/banner/ko.png", response.data?.banners?.first()?.imageUrl)
|
||||
Mockito.verify(bannerService).getDisplayBanners(pageable, Lang.EN)
|
||||
}
|
||||
|
||||
private fun createBanner(id: Long, imagePath: String, lang: Lang): 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package kr.co.vividnext.sodalive.chat.character.service
|
||||
|
||||
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
|
||||
import kr.co.vividnext.sodalive.chat.character.ChatCharacterBanner
|
||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterBannerRepository
|
||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito
|
||||
import org.springframework.data.domain.PageImpl
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import java.util.Optional
|
||||
|
||||
class ChatCharacterBannerServiceTest {
|
||||
private lateinit var bannerRepository: ChatCharacterBannerRepository
|
||||
private lateinit var characterRepository: ChatCharacterRepository
|
||||
private lateinit var service: ChatCharacterBannerService
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
bannerRepository = Mockito.mock(ChatCharacterBannerRepository::class.java)
|
||||
characterRepository = Mockito.mock(ChatCharacterRepository::class.java)
|
||||
service = ChatCharacterBannerService(
|
||||
bannerRepository = bannerRepository,
|
||||
characterRepository = characterRepository
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("일본어 배너 등록 요청은 JA 언어값으로 저장한다")
|
||||
fun shouldRegisterJapaneseBanner() {
|
||||
val character = createCharacter(id = 1L)
|
||||
|
||||
Mockito.`when`(characterRepository.findById(1L)).thenReturn(Optional.of(character))
|
||||
Mockito.`when`(bannerRepository.findMaxSortOrder()).thenReturn(3)
|
||||
Mockito.`when`(bannerRepository.save(Mockito.any(ChatCharacterBanner::class.java))).thenAnswer { it.arguments[0] }
|
||||
|
||||
val banner = service.registerBanner(
|
||||
characterId = 1L,
|
||||
imagePath = "banner/jp.png",
|
||||
lang = Lang.JA
|
||||
)
|
||||
|
||||
assertEquals(Lang.JA, banner.lang)
|
||||
assertEquals(4, banner.sortOrder)
|
||||
assertEquals("banner/jp.png", banner.imagePath)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("기본 배너 등록 요청은 언어값이 없으면 KO로 저장한다")
|
||||
fun shouldRegisterDefaultBannerAsKoreanWhenLangIsNull() {
|
||||
val character = createCharacter(id = 2L)
|
||||
|
||||
Mockito.`when`(characterRepository.findById(2L)).thenReturn(Optional.of(character))
|
||||
Mockito.`when`(bannerRepository.findMaxSortOrder()).thenReturn(null)
|
||||
Mockito.`when`(bannerRepository.save(Mockito.any(ChatCharacterBanner::class.java))).thenAnswer { it.arguments[0] }
|
||||
|
||||
val banner = service.registerBanner(
|
||||
characterId = 2L,
|
||||
imagePath = "banner/default.png",
|
||||
lang = null
|
||||
)
|
||||
|
||||
assertEquals(Lang.KO, banner.lang)
|
||||
assertEquals(1, banner.sortOrder)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("영어 배너 등록 요청은 EN 언어값으로 저장한다")
|
||||
fun shouldRegisterEnglishBanner() {
|
||||
val character = createCharacter(id = 3L)
|
||||
|
||||
Mockito.`when`(characterRepository.findById(3L)).thenReturn(Optional.of(character))
|
||||
Mockito.`when`(bannerRepository.findMaxSortOrder()).thenReturn(5)
|
||||
Mockito.`when`(bannerRepository.save(Mockito.any(ChatCharacterBanner::class.java))).thenAnswer { it.arguments[0] }
|
||||
|
||||
val banner = service.registerBanner(
|
||||
characterId = 3L,
|
||||
imagePath = "banner/en.png",
|
||||
lang = Lang.EN
|
||||
)
|
||||
|
||||
assertEquals(Lang.EN, banner.lang)
|
||||
assertEquals(6, banner.sortOrder)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("일본어 사용자는 일본어 배너만 조회한다")
|
||||
fun shouldReturnJapaneseBannersForJapaneseUser() {
|
||||
val pageable = PageRequest.of(0, 10)
|
||||
val japaneseBanner = ChatCharacterBanner(
|
||||
imagePath = "banner/jp.png",
|
||||
chatCharacter = createCharacter(id = 4L),
|
||||
sortOrder = 1,
|
||||
lang = Lang.JA
|
||||
)
|
||||
val expectedPage = PageImpl(listOf(japaneseBanner), pageable, 1)
|
||||
|
||||
Mockito.`when`(bannerRepository.findByIsActiveTrueAndLangOrderBySortOrderAsc(Lang.JA, pageable))
|
||||
.thenReturn(expectedPage)
|
||||
|
||||
val actual = service.getDisplayBanners(pageable, Lang.JA)
|
||||
|
||||
assertEquals(expectedPage, actual)
|
||||
Mockito.verify(bannerRepository).findByIsActiveTrueAndLangOrderBySortOrderAsc(Lang.JA, pageable)
|
||||
Mockito.verify(bannerRepository, Mockito.never())
|
||||
.findByIsActiveTrueAndLangOrderBySortOrderAsc(
|
||||
Lang.KO,
|
||||
pageable
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("한국어 사용자는 한국어 배너를 조회한다")
|
||||
fun shouldReturnKoreanBannersForKoreanUser() {
|
||||
val pageable = PageRequest.of(0, 10)
|
||||
val defaultBanner = ChatCharacterBanner(
|
||||
imagePath = "banner/ko.png",
|
||||
chatCharacter = createCharacter(id = 5L),
|
||||
sortOrder = 1,
|
||||
lang = Lang.KO
|
||||
)
|
||||
val expectedPage = PageImpl(listOf(defaultBanner), pageable, 1)
|
||||
|
||||
Mockito.`when`(bannerRepository.findByIsActiveTrueAndLangOrderBySortOrderAsc(Lang.KO, pageable))
|
||||
.thenReturn(expectedPage)
|
||||
|
||||
val actual = service.getDisplayBanners(pageable, Lang.KO)
|
||||
|
||||
assertEquals(expectedPage, actual)
|
||||
Mockito.verify(bannerRepository).findByIsActiveTrueAndLangOrderBySortOrderAsc(Lang.KO, pageable)
|
||||
Mockito.verify(bannerRepository, Mockito.never())
|
||||
.findByIsActiveTrueAndLangOrderBySortOrderAsc(
|
||||
Lang.JA,
|
||||
pageable
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("영어 배너가 없으면 한국어 배너로 fallback 한다")
|
||||
fun shouldFallbackToKoreanWhenEnglishBannerDoesNotExist() {
|
||||
val pageable = PageRequest.of(0, 10)
|
||||
val englishPage = PageImpl<ChatCharacterBanner>(emptyList(), pageable, 0)
|
||||
val koreanBanner = ChatCharacterBanner(
|
||||
imagePath = "banner/ko.png",
|
||||
chatCharacter = createCharacter(id = 6L),
|
||||
sortOrder = 1,
|
||||
lang = Lang.KO
|
||||
)
|
||||
val koreanPage = PageImpl(listOf(koreanBanner), pageable, 1)
|
||||
|
||||
Mockito.`when`(bannerRepository.findByIsActiveTrueAndLangOrderBySortOrderAsc(Lang.EN, pageable))
|
||||
.thenReturn(englishPage)
|
||||
Mockito.`when`(bannerRepository.findByIsActiveTrueAndLangOrderBySortOrderAsc(Lang.KO, pageable))
|
||||
.thenReturn(koreanPage)
|
||||
|
||||
val actual = service.getDisplayBanners(pageable, Lang.EN)
|
||||
|
||||
assertEquals(koreanPage, actual)
|
||||
Mockito.verify(bannerRepository).findByIsActiveTrueAndLangOrderBySortOrderAsc(Lang.EN, pageable)
|
||||
Mockito.verify(bannerRepository).findByIsActiveTrueAndLangOrderBySortOrderAsc(Lang.KO, pageable)
|
||||
}
|
||||
|
||||
private fun createCharacter(id: Long): ChatCharacter {
|
||||
val character = ChatCharacter(
|
||||
characterUUID = "character-$id",
|
||||
name = "character-$id",
|
||||
description = "description-$id",
|
||||
systemPrompt = "system-prompt-$id"
|
||||
)
|
||||
character.id = id
|
||||
return character
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user