fix(character): 인기 캐릭터 응답을 DTO로 변경하여 jackson 직렬화 오류 해결

- ChatCharacterService.getPopularCharacters 반환을 List<ChatCharacter> → List<Character> DTO로 변경
- 캐시 대상도 DTO로 전환(@Cacheable 유지, 동적 키/고정 TTL 그대로 사용)
- 컨트롤러에서 불필요한 매핑 제거(서비스가 DTO로 반환)
- Character DTO 직렬화 안정성 확보(@JsonProperty 추가)
- 이미지 URL 생성 로직을 서비스로 이동하고 imageHost(@Value) 주입해 구성
This commit is contained in:
Klaus 2025-09-11 18:53:27 +09:00
parent 58a46a09c3
commit 27a3f450ef
3 changed files with 22 additions and 15 deletions

View File

@ -66,14 +66,6 @@ class ChatCharacterController(
// 인기 캐릭터 조회 // 인기 캐릭터 조회
val popularCharacters = service.getPopularCharacters() val popularCharacters = service.getPopularCharacters()
.map {
Character(
characterId = it.id!!,
name = it.name,
description = it.description,
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}"
)
}
// 최신 캐릭터 조회 (최대 10개) // 최신 캐릭터 조회 (최대 10개)
val newCharacters = service.getNewCharacters(50) val newCharacters = service.getNewCharacters(50)

View File

@ -1,5 +1,7 @@
package kr.co.vividnext.sodalive.chat.character.dto package kr.co.vividnext.sodalive.chat.character.dto
import com.fasterxml.jackson.annotation.JsonProperty
data class CharacterMainResponse( data class CharacterMainResponse(
val banners: List<CharacterBannerResponse>, val banners: List<CharacterBannerResponse>,
val recentCharacters: List<RecentCharacter>, val recentCharacters: List<RecentCharacter>,
@ -15,10 +17,10 @@ data class CurationSection(
) )
data class Character( data class Character(
val characterId: Long, @JsonProperty("characterId") val characterId: Long,
val name: String, @JsonProperty("name") val name: String,
val description: String, @JsonProperty("description") val description: String,
val imageUrl: String @JsonProperty("imageUrl") val imageUrl: String
) )
data class RecentCharacter( data class RecentCharacter(

View File

@ -11,11 +11,13 @@ import kr.co.vividnext.sodalive.chat.character.ChatCharacterGoal
import kr.co.vividnext.sodalive.chat.character.ChatCharacterHobby import kr.co.vividnext.sodalive.chat.character.ChatCharacterHobby
import kr.co.vividnext.sodalive.chat.character.ChatCharacterTag import kr.co.vividnext.sodalive.chat.character.ChatCharacterTag
import kr.co.vividnext.sodalive.chat.character.ChatCharacterValue import kr.co.vividnext.sodalive.chat.character.ChatCharacterValue
import kr.co.vividnext.sodalive.chat.character.dto.Character
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterGoalRepository import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterGoalRepository
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterHobbyRepository import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterHobbyRepository
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterTagRepository import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterTagRepository
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterValueRepository import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterValueRepository
import org.springframework.beans.factory.annotation.Value
import org.springframework.cache.annotation.Cacheable import org.springframework.cache.annotation.Cacheable
import org.springframework.data.domain.PageRequest import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@ -28,7 +30,10 @@ class ChatCharacterService(
private val valueRepository: ChatCharacterValueRepository, private val valueRepository: ChatCharacterValueRepository,
private val hobbyRepository: ChatCharacterHobbyRepository, private val hobbyRepository: ChatCharacterHobbyRepository,
private val goalRepository: ChatCharacterGoalRepository, private val goalRepository: ChatCharacterGoalRepository,
private val popularCharacterQuery: PopularCharacterQuery private val popularCharacterQuery: PopularCharacterQuery,
@Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String
) { ) {
/** /**
* UTC 20:00 경계 기준 지난 윈도우의 메시지 상위 캐릭터 조회 * UTC 20:00 경계 기준 지난 윈도우의 메시지 상위 캐릭터 조회
@ -39,10 +44,18 @@ class ChatCharacterService(
cacheNames = ["popularCharacters_24h"], cacheNames = ["popularCharacters_24h"],
key = "T(kr.co.vividnext.sodalive.chat.character.service.RankingWindowCalculator).now('popular-chat-character').cacheKey" key = "T(kr.co.vividnext.sodalive.chat.character.service.RankingWindowCalculator).now('popular-chat-character').cacheKey"
) )
fun getPopularCharacters(limit: Long = 20): List<ChatCharacter> { fun getPopularCharacters(limit: Long = 20): List<Character> {
val window = RankingWindowCalculator.now("popular-chat-character") val window = RankingWindowCalculator.now("popular-chat-character")
val topIds = popularCharacterQuery.findPopularCharacterIds(window.windowStart, window.nextBoundary, limit) val topIds = popularCharacterQuery.findPopularCharacterIds(window.windowStart, window.nextBoundary, limit)
return loadCharactersInOrder(topIds) val list = loadCharactersInOrder(topIds)
return list.map {
Character(
characterId = it.id!!,
name = it.name,
description = it.description,
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}"
)
}
} }
private fun loadCharactersInOrder(ids: List<Long>): List<ChatCharacter> { private fun loadCharactersInOrder(ids: List<Long>): List<ChatCharacter> {