feat(chat-character): 추천 캐릭터 조회 및 메인/새로고침 API 반영

This commit is contained in:
2025-11-11 17:01:50 +09:00
parent 80c44373c7
commit 16b6c13309
4 changed files with 70 additions and 0 deletions

View File

@@ -74,6 +74,13 @@ class ChatCharacterController(
size = 50
).content
// 추천 캐릭터 조회
// 최근 대화한 캐릭터를 제외한 랜덤 20개 조회
// Controller에서는 호출만
// 세부로직은 추후에 변경될 수 있으므로 Service에 별도로 생성
val excludeIds = recentCharacters.map { it.characterId }
val recommendCharacters = service.getRecommendCharacters(excludeIds, 20)
// 큐레이션 섹션 (활성화된 큐레이션 + 캐릭터)
val curationSections = curationQueryService.getActiveCurationsWithCharacters()
.map { agg ->
@@ -98,6 +105,7 @@ class ChatCharacterController(
recentCharacters = recentCharacters,
popularCharacters = popularCharacters,
newCharacters = newCharacters,
recommendCharacters = recommendCharacters,
curationSections = curationSections
)
)
@@ -193,4 +201,23 @@ class ChatCharacterController(
)
)
}
/**
* 추천 캐릭터 새로고침 API
* - 최근 대화한 캐릭터를 제외하고 랜덤 20개 반환
* - 비회원 또는 본인인증되지 않은 경우: 최근 대화 목록 없음 → 전체 활성 캐릭터 중 랜덤 20개
*/
@GetMapping("/recommend")
fun getRecommendCharacters(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
val recent = if (member == null || member.auth == null) {
emptyList()
} else {
chatRoomService
.listMyChatRooms(member, 0, 50) // 최근 기록은 최대 50개까지만 제외 대상으로 고려
.map { it.characterId }
}
ApiResponse.ok(service.getRecommendCharacters(recent, 20))
}
}

View File

@@ -7,6 +7,7 @@ data class CharacterMainResponse(
val recentCharacters: List<RecentCharacter>,
val popularCharacters: List<Character>,
val newCharacters: List<Character>,
val recommendCharacters: List<Character>,
val curationSections: List<CurationSection>
)

View File

@@ -74,5 +74,29 @@ interface ChatCharacterRepository : JpaRepository<ChatCharacter, Long> {
pageable: Pageable
): List<ChatCharacter>
/**
* 활성 캐릭터 무작위 조회
*/
@Query(
"""
SELECT c FROM ChatCharacter c
WHERE c.isActive = true
ORDER BY function('RAND')
"""
)
fun findRandomActive(pageable: Pageable): List<ChatCharacter>
/**
* 제외할 캐릭터를 뺀 활성 캐릭터 무작위 조회
*/
@Query(
"""
SELECT c FROM ChatCharacter c
WHERE c.isActive = true AND c.id NOT IN :excludeIds
ORDER BY function('RAND')
"""
)
fun findRandomActiveExcluding(@Param("excludeIds") excludeIds: List<Long>, pageable: Pageable): List<ChatCharacter>
fun findByIdInAndIsActiveTrue(ids: List<Long>): List<ChatCharacter>
}

View File

@@ -38,6 +38,24 @@ class ChatCharacterService(
@Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String
) {
@Transactional(readOnly = true)
fun getRecommendCharacters(excludeCharacterIds: List<Long> = emptyList(), limit: Int = 20): List<Character> {
val safeLimit = if (limit <= 0) 20 else if (limit > 50) 50 else limit
val chars = if (excludeCharacterIds.isNotEmpty()) {
chatCharacterRepository.findRandomActiveExcluding(excludeCharacterIds, PageRequest.of(0, safeLimit))
} else {
chatCharacterRepository.findRandomActive(PageRequest.of(0, safeLimit))
}
return chars.map {
Character(
characterId = it.id!!,
name = it.name,
description = it.description,
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}"
)
}
}
/**
* UTC 20:00 경계 기준 지난 윈도우의 메시지 수 상위 캐릭터 조회
* Spring Cache(@Cacheable) + 동적 키 + 고정 TTL(24h) 사용