feat(chat-banner): 다국어 캐릭터 배너 등록과 노출을 지원한다
배너를 언어별로 저장하고 요청 언어 우선 조회 후 한국어로 fallback 하도록 맞춘다.
This commit is contained in:
@@ -127,7 +127,8 @@ class AdminChatBannerController(
|
||||
// 1. 먼저 빈 이미지 경로로 배너 등록 (정렬 순서 포함)
|
||||
val banner = bannerService.registerBanner(
|
||||
characterId = request.characterId,
|
||||
imagePath = ""
|
||||
imagePath = "",
|
||||
lang = request.lang
|
||||
)
|
||||
|
||||
// 2. 배너 ID를 사용하여 이미지 업로드
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package kr.co.vividnext.sodalive.admin.chat.dto
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
|
||||
/**
|
||||
* 캐릭터 배너 등록 요청 DTO
|
||||
*/
|
||||
data class ChatCharacterBannerRegisterRequest(
|
||||
// 캐릭터 ID
|
||||
@JsonProperty("characterId") val characterId: Long
|
||||
@JsonProperty("characterId") val characterId: Long,
|
||||
@JsonProperty("lang") val lang: Lang? = null
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package kr.co.vividnext.sodalive.chat.character
|
||||
|
||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.EnumType
|
||||
import javax.persistence.Enumerated
|
||||
import javax.persistence.FetchType
|
||||
import javax.persistence.JoinColumn
|
||||
import javax.persistence.ManyToOne
|
||||
@@ -24,6 +28,10 @@ class ChatCharacterBanner(
|
||||
// 정렬 순서 (낮을수록 먼저 표시)
|
||||
var sortOrder: Int = 0,
|
||||
|
||||
@Column(nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
var lang: Lang = Lang.KO,
|
||||
|
||||
// 활성화 여부 (소프트 삭제용)
|
||||
var isActive: Boolean = true
|
||||
) : BaseEntity()
|
||||
|
||||
@@ -62,7 +62,7 @@ class ChatCharacterController(
|
||||
val isAdultAccessible = resolveIsAdultAccessible(member)
|
||||
|
||||
// 배너 조회 (최대 10개)
|
||||
val banners = bannerService.getActiveBanners(PageRequest.of(0, 10))
|
||||
val banners = bannerService.getDisplayBanners(PageRequest.of(0, 10), langContext.lang)
|
||||
.content
|
||||
.map {
|
||||
CharacterBannerResponse(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.chat.character.repository
|
||||
|
||||
import kr.co.vividnext.sodalive.chat.character.ChatCharacterBanner
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
@@ -12,6 +13,8 @@ interface ChatCharacterBannerRepository : JpaRepository<ChatCharacterBanner, Lon
|
||||
// 활성화된 배너 목록 조회 (정렬 순서대로)
|
||||
fun findByIsActiveTrueOrderBySortOrderAsc(pageable: Pageable): Page<ChatCharacterBanner>
|
||||
|
||||
fun findByIsActiveTrueAndLangOrderBySortOrderAsc(lang: Lang, pageable: Pageable): Page<ChatCharacterBanner>
|
||||
|
||||
// 활성화된 배너 중 최대 정렬 순서 값 조회
|
||||
@Query("SELECT MAX(b.sortOrder) FROM ChatCharacterBanner b WHERE b.isActive = true")
|
||||
fun findMaxSortOrder(): Int?
|
||||
|
||||
@@ -4,6 +4,7 @@ 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.common.SodaException
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.stereotype.Service
|
||||
@@ -21,6 +22,19 @@ class ChatCharacterBannerService(
|
||||
return bannerRepository.findByIsActiveTrueOrderBySortOrderAsc(pageable)
|
||||
}
|
||||
|
||||
fun getDisplayBanners(pageable: Pageable, lang: Lang): Page<ChatCharacterBanner> {
|
||||
if (lang == Lang.KO) {
|
||||
return bannerRepository.findByIsActiveTrueAndLangOrderBySortOrderAsc(Lang.KO, pageable)
|
||||
}
|
||||
|
||||
val localizedBanners = bannerRepository.findByIsActiveTrueAndLangOrderBySortOrderAsc(lang, pageable)
|
||||
return if (localizedBanners.hasContent()) {
|
||||
localizedBanners
|
||||
} else {
|
||||
bannerRepository.findByIsActiveTrueAndLangOrderBySortOrderAsc(Lang.KO, pageable)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배너 상세 조회
|
||||
*/
|
||||
@@ -37,7 +51,10 @@ class ChatCharacterBannerService(
|
||||
* @return 등록된 배너
|
||||
*/
|
||||
@Transactional
|
||||
fun registerBanner(characterId: Long, imagePath: String): ChatCharacterBanner {
|
||||
fun registerBanner(characterId: Long, imagePath: String, lang: Lang? = null): ChatCharacterBanner {
|
||||
val finalLang = lang ?: Lang.KO
|
||||
validateRegisterLang(finalLang)
|
||||
|
||||
val character = characterRepository.findById(characterId)
|
||||
.orElseThrow { SodaException(messageKey = "chat.character.not_found") }
|
||||
|
||||
@@ -51,12 +68,21 @@ class ChatCharacterBannerService(
|
||||
val banner = ChatCharacterBanner(
|
||||
imagePath = imagePath,
|
||||
chatCharacter = character,
|
||||
sortOrder = finalSortOrder
|
||||
sortOrder = finalSortOrder,
|
||||
lang = finalLang
|
||||
)
|
||||
|
||||
return bannerRepository.save(banner)
|
||||
}
|
||||
|
||||
private fun validateRegisterLang(lang: Lang) {
|
||||
if (lang == Lang.KO || lang == Lang.EN || lang == Lang.JA) {
|
||||
return
|
||||
}
|
||||
|
||||
throw SodaException(messageKey = "common.error.invalid_request")
|
||||
}
|
||||
|
||||
/**
|
||||
* 배너 수정
|
||||
*
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.i18n
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator
|
||||
import java.util.Locale
|
||||
|
||||
enum class Lang(val code: String, val locale: Locale) {
|
||||
@@ -8,6 +9,14 @@ enum class Lang(val code: String, val locale: Locale) {
|
||||
JA("ja", Locale.JAPANESE);
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
|
||||
fun fromCode(value: String): Lang {
|
||||
return values().find {
|
||||
it.code.equals(value.trim(), ignoreCase = true) || it.name.equals(value.trim(), ignoreCase = true)
|
||||
} ?: throw IllegalArgumentException("Unknown language code: $value")
|
||||
}
|
||||
|
||||
fun fromAcceptLanguage(header: String?): Lang {
|
||||
if (header.isNullOrBlank()) return KO
|
||||
val two = header.trim().lowercase().take(2) // 앱은 2자리만 보내지만 안전하게 처리
|
||||
|
||||
Reference in New Issue
Block a user