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:
		| @@ -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) | ||||||
|   | |||||||
| @@ -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( | ||||||
|   | |||||||
| @@ -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> { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user