캐릭터 챗봇 #338

Merged
klaus merged 119 commits from test into main 2025-09-10 06:08:47 +00:00
4 changed files with 68 additions and 14 deletions
Showing only changes of commit a05bc369b7 - Show all commits

View File

@ -54,12 +54,13 @@ class CharacterCommentController(
fun listComments( fun listComments(
@PathVariable characterId: Long, @PathVariable characterId: Long,
@RequestParam(required = false, defaultValue = "20") limit: Int, @RequestParam(required = false, defaultValue = "20") limit: Int,
@RequestParam(required = false) cursor: Long?,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
val data = service.listComments(imageHost, characterId, limit) val data = service.listComments(imageHost, characterId, cursor, limit)
ApiResponse.ok(data) ApiResponse.ok(data)
} }
@ -68,13 +69,14 @@ class CharacterCommentController(
@PathVariable characterId: Long, @PathVariable characterId: Long,
@PathVariable commentId: Long, @PathVariable commentId: Long,
@RequestParam(required = false, defaultValue = "20") limit: Int, @RequestParam(required = false, defaultValue = "20") limit: Int,
@RequestParam(required = false) cursor: Long?,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run { ) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
// characterId는 서비스 내부 검증(원본 댓글과 캐릭터 일치)에서 검증됨 // characterId는 서비스 내부 검증(원본 댓글과 캐릭터 일치)에서 검증됨
val data = service.getReplies(imageHost, commentId, limit) val data = service.getReplies(imageHost, commentId, cursor, limit)
ApiResponse.ok(data) ApiResponse.ok(data)
} }
} }

View File

@ -41,7 +41,8 @@ data class CharacterReplyResponse(
data class CharacterCommentRepliesResponse( data class CharacterCommentRepliesResponse(
val original: CharacterCommentResponse, val original: CharacterCommentResponse,
val replies: List<CharacterReplyResponse> val replies: List<CharacterReplyResponse>,
val cursor: Long?
) )
// 댓글 리스트 조회 Response 컨테이너 // 댓글 리스트 조회 Response 컨테이너
@ -50,5 +51,6 @@ data class CharacterCommentRepliesResponse(
data class CharacterCommentListResponse( data class CharacterCommentListResponse(
val totalCount: Int, val totalCount: Int,
val comments: List<CharacterCommentResponse> val comments: List<CharacterCommentResponse>,
val cursor: Long?
) )

View File

@ -9,8 +9,22 @@ interface CharacterCommentRepository : JpaRepository<CharacterComment, Long> {
pageable: Pageable pageable: Pageable
): List<CharacterComment> ): List<CharacterComment>
fun findByChatCharacter_IdAndIsActiveTrueAndParentIsNullAndIdLessThanOrderByCreatedAtDesc(
chatCharacterId: Long,
id: Long,
pageable: Pageable
): List<CharacterComment>
fun countByParent_IdAndIsActiveTrue(parentId: Long): Int fun countByParent_IdAndIsActiveTrue(parentId: Long): Int
fun findByParent_IdAndIsActiveTrueOrderByCreatedAtDesc(parentId: Long, pageable: Pageable): List<CharacterComment> fun findByParent_IdAndIsActiveTrueOrderByCreatedAtDesc(parentId: Long, pageable: Pageable): List<CharacterComment>
fun findByParent_IdAndIsActiveTrueAndIdLessThanOrderByCreatedAtDesc(
parentId: Long,
id: Long,
pageable: Pageable
): List<CharacterComment>
fun findFirstByChatCharacter_IdAndIsActiveTrueOrderByCreatedAtDesc(chatCharacterId: Long): CharacterComment? fun findFirstByChatCharacter_IdAndIsActiveTrueOrderByCreatedAtDesc(chatCharacterId: Long): CharacterComment?
// 전체(상위+답글) 활성 댓글 총 개수 // 전체(상위+답글) 활성 댓글 총 개수

View File

@ -83,31 +83,67 @@ class CharacterCommentService(
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun listComments(imageHost: String, characterId: Long, limit: Int = 20): CharacterCommentListResponse { fun listComments(
imageHost: String,
characterId: Long,
cursor: Long?,
limit: Int = 20
): CharacterCommentListResponse {
val pageable = PageRequest.of(0, limit) val pageable = PageRequest.of(0, limit)
val comments = commentRepository.findByChatCharacter_IdAndIsActiveTrueAndParentIsNullOrderByCreatedAtDesc( val comments = if (cursor == null) {
characterId, commentRepository.findByChatCharacter_IdAndIsActiveTrueAndParentIsNullOrderByCreatedAtDesc(
pageable characterId,
) pageable
)
} else {
commentRepository.findByChatCharacter_IdAndIsActiveTrueAndParentIsNullAndIdLessThanOrderByCreatedAtDesc(
characterId,
cursor,
pageable
)
}
val items = comments.map { toCommentResponse(imageHost, it) } val items = comments.map { toCommentResponse(imageHost, it) }
val total = getTotalCommentCount(characterId) val total = getTotalCommentCount(characterId)
val nextCursor = if (items.size == limit) items.lastOrNull()?.commentId else null
return CharacterCommentListResponse( return CharacterCommentListResponse(
totalCount = total, totalCount = total,
comments = items comments = items,
cursor = nextCursor
) )
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getReplies(imageHost: String, commentId: Long, limit: Int = 20): CharacterCommentRepliesResponse { fun getReplies(
val original = commentRepository.findById(commentId).orElseThrow { SodaException("댓글을 찾을 수 없습니다.") } imageHost: String,
commentId: Long,
cursor: Long?,
limit: Int = 20
): CharacterCommentRepliesResponse {
val original = commentRepository.findById(commentId).orElseThrow {
SodaException("댓글을 찾을 수 없습니다.")
}
if (!original.isActive) throw SodaException("비활성화된 댓글입니다.") if (!original.isActive) throw SodaException("비활성화된 댓글입니다.")
val pageable = PageRequest.of(0, limit) val pageable = PageRequest.of(0, limit)
val replies = commentRepository.findByParent_IdAndIsActiveTrueOrderByCreatedAtDesc(commentId, pageable) val replies = if (cursor == null) {
commentRepository.findByParent_IdAndIsActiveTrueOrderByCreatedAtDesc(commentId, pageable)
} else {
commentRepository.findByParent_IdAndIsActiveTrueAndIdLessThanOrderByCreatedAtDesc(
commentId,
cursor,
pageable
)
}
val items = replies.map { toReplyResponse(imageHost, it) }
val nextCursor = if (items.size == limit) items.lastOrNull()?.replyId else null
return CharacterCommentRepliesResponse( return CharacterCommentRepliesResponse(
original = toCommentResponse(imageHost, original, 0), original = toCommentResponse(imageHost, original, 0),
replies = replies.map { toReplyResponse(imageHost, it) } replies = items,
cursor = nextCursor
) )
} }