feat(chat-character): 캐릭터 상세 조회 응답 확장 및 ‘다른 캐릭터’ 추천 추가

- 상세 페이지 정보 강화 및 탐색성 향상을 위해 응답 필드를 확장
- CharacterDetailResponse에 originalTitle, originalLink, characterType, others 추가
- OtherCharacter DTO 추가 (characterId, name, imageUrl, tags)
- 공유 태그 기반으로 현재 캐릭터를 제외한 랜덤 10개 캐릭터 조회 JPA 쿼리 추가
  - ChatCharacterRepository.findRandomBySharedTags(@Query, RAND 정렬, 페이징)
- 서비스 계층에 getOtherCharactersBySharedTags 추가 및 태그 지연 로딩 초기화
- 컨트롤러에서:
  - others 리스트를 조회/매핑하여 응답에 포함
  - originalTitle, originalLink, characterType을 응답에 포함
This commit is contained in:
Klaus 2025-08-12 03:47:48 +09:00
parent 423cbe7315
commit 01ef738d31
4 changed files with 68 additions and 1 deletions

View File

@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.chat.character.dto.CharacterDetailResponse
import kr.co.vividnext.sodalive.chat.character.dto.CharacterMainResponse
import kr.co.vividnext.sodalive.chat.character.dto.CharacterPersonalityResponse
import kr.co.vividnext.sodalive.chat.character.dto.CurationSection
import kr.co.vividnext.sodalive.chat.character.dto.OtherCharacter
import kr.co.vividnext.sodalive.chat.character.dto.RecentCharacter
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
@ -127,6 +128,20 @@ class ChatCharacterController(
)
}
// 다른 캐릭터 조회 (태그 기반, 랜덤 10개, 현재 캐릭터 제외)
val others = service.getOtherCharactersBySharedTags(characterId, 10)
.map { other ->
val otherTags = other.tagMappings
.map { it.tag.tag }
.joinToString(" ") { if (it.startsWith("#")) it else "#$it" }
OtherCharacter(
characterId = other.id!!,
name = other.name,
imageUrl = "$imageHost/${other.imagePath ?: "profile/default-profile.png"}",
tags = otherTags
)
}
// 응답 생성
ApiResponse.ok(
CharacterDetailResponse(
@ -137,7 +152,11 @@ class ChatCharacterController(
imageUrl = "$imageHost/${character.imagePath ?: "profile/default-profile.png"}",
personalities = personality,
backgrounds = background,
tags = tags
tags = tags,
originalTitle = character.originalTitle,
originalLink = character.originalLink,
characterType = character.characterType,
others = others
)
)
}

View File

@ -1,5 +1,7 @@
package kr.co.vividnext.sodalive.chat.character.dto
import kr.co.vividnext.sodalive.chat.character.CharacterType
data class CharacterDetailResponse(
val characterId: Long,
val name: String,
@ -8,6 +10,17 @@ data class CharacterDetailResponse(
val imageUrl: String,
val personalities: CharacterPersonalityResponse?,
val backgrounds: CharacterBackgroundResponse?,
val tags: String,
val originalTitle: String?,
val originalLink: String?,
val characterType: CharacterType,
val others: List<OtherCharacter>
)
data class OtherCharacter(
val characterId: Long,
val name: String,
val imageUrl: String,
val tags: String
)

View File

@ -66,4 +66,25 @@ interface ChatCharacterRepository : JpaRepository<ChatCharacter, Long> {
@Param("member") member: Member,
pageable: Pageable
): List<ChatCharacter>
/**
* 특정 캐릭터와 태그를 공유하는 다른 캐릭터를 무작위로 조회 (현재 캐릭터 제외)
*/
@Query(
"""
SELECT DISTINCT c FROM ChatCharacter c
JOIN c.tagMappings tm
JOIN tm.tag t
WHERE c.isActive = true
AND c.id <> :characterId
AND t.id IN (
SELECT t2.id FROM ChatCharacterTagMapping tm2 JOIN tm2.tag t2 WHERE tm2.chatCharacter.id = :characterId
)
ORDER BY function('RAND')
"""
)
fun findRandomBySharedTags(
@Param("characterId") characterId: Long,
pageable: Pageable
): List<ChatCharacter>
}

View File

@ -53,6 +53,20 @@ class ChatCharacterService(
return chatCharacterRepository.findByIsActiveTrueOrderByCreatedAtDesc(PageRequest.of(0, limit))
}
/**
* 특정 캐릭터와 태그를 공유하는 다른 캐릭터를 무작위로 조회 (현재 캐릭터 제외)
*/
@Transactional(readOnly = true)
fun getOtherCharactersBySharedTags(characterId: Long, limit: Int = 10): List<ChatCharacter> {
val others = chatCharacterRepository.findRandomBySharedTags(
characterId,
PageRequest.of(0, limit)
)
// 태그 초기화 (지연 로딩 문제 방지)
others.forEach { it.tagMappings.size }
return others
}
/**
* 태그를 찾거나 생성하여 캐릭터에 연결
*/