diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentController.kt index fadc70f..8b772e6 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentController.kt @@ -54,12 +54,13 @@ class CharacterCommentController( fun listComments( @PathVariable characterId: Long, @RequestParam(required = false, defaultValue = "20") limit: Int, + @RequestParam(required = false) cursor: Long?, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == 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) } @@ -68,13 +69,14 @@ class CharacterCommentController( @PathVariable characterId: Long, @PathVariable commentId: Long, @RequestParam(required = false, defaultValue = "20") limit: Int, + @RequestParam(required = false) cursor: Long?, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException("로그인 정보를 확인해주세요.") if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") // characterId는 서비스 내부 검증(원본 댓글과 캐릭터 일치)에서 검증됨 - val data = service.getReplies(imageHost, commentId, limit) + val data = service.getReplies(imageHost, commentId, cursor, limit) ApiResponse.ok(data) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentDto.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentDto.kt index 769719c..fd67785 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentDto.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentDto.kt @@ -41,7 +41,8 @@ data class CharacterReplyResponse( data class CharacterCommentRepliesResponse( val original: CharacterCommentResponse, - val replies: List + val replies: List, + val cursor: Long? ) // 댓글 리스트 조회 Response 컨테이너 @@ -50,5 +51,6 @@ data class CharacterCommentRepliesResponse( data class CharacterCommentListResponse( val totalCount: Int, - val comments: List + val comments: List, + val cursor: Long? ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentRepository.kt index e160fc9..d921a06 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentRepository.kt @@ -9,8 +9,22 @@ interface CharacterCommentRepository : JpaRepository { pageable: Pageable ): List + fun findByChatCharacter_IdAndIsActiveTrueAndParentIsNullAndIdLessThanOrderByCreatedAtDesc( + chatCharacterId: Long, + id: Long, + pageable: Pageable + ): List + fun countByParent_IdAndIsActiveTrue(parentId: Long): Int + fun findByParent_IdAndIsActiveTrueOrderByCreatedAtDesc(parentId: Long, pageable: Pageable): List + + fun findByParent_IdAndIsActiveTrueAndIdLessThanOrderByCreatedAtDesc( + parentId: Long, + id: Long, + pageable: Pageable + ): List + fun findFirstByChatCharacter_IdAndIsActiveTrueOrderByCreatedAtDesc(chatCharacterId: Long): CharacterComment? // 전체(상위+답글) 활성 댓글 총 개수 diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentService.kt index 7619ad7..ff54dd3 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentService.kt @@ -83,31 +83,67 @@ class CharacterCommentService( } @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 comments = commentRepository.findByChatCharacter_IdAndIsActiveTrueAndParentIsNullOrderByCreatedAtDesc( - characterId, - pageable - ) + val comments = if (cursor == null) { + commentRepository.findByChatCharacter_IdAndIsActiveTrueAndParentIsNullOrderByCreatedAtDesc( + characterId, + pageable + ) + } else { + commentRepository.findByChatCharacter_IdAndIsActiveTrueAndParentIsNullAndIdLessThanOrderByCreatedAtDesc( + characterId, + cursor, + pageable + ) + } + val items = comments.map { toCommentResponse(imageHost, it) } val total = getTotalCommentCount(characterId) + val nextCursor = if (items.size == limit) items.lastOrNull()?.commentId else null + return CharacterCommentListResponse( totalCount = total, - comments = items + comments = items, + cursor = nextCursor ) } @Transactional(readOnly = true) - fun getReplies(imageHost: String, commentId: Long, limit: Int = 20): CharacterCommentRepliesResponse { - val original = commentRepository.findById(commentId).orElseThrow { SodaException("댓글을 찾을 수 없습니다.") } + fun getReplies( + imageHost: String, + commentId: Long, + cursor: Long?, + limit: Int = 20 + ): CharacterCommentRepliesResponse { + val original = commentRepository.findById(commentId).orElseThrow { + SodaException("댓글을 찾을 수 없습니다.") + } if (!original.isActive) throw SodaException("비활성화된 댓글입니다.") 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( original = toCommentResponse(imageHost, original, 0), - replies = replies.map { toReplyResponse(imageHost, it) } + replies = items, + cursor = nextCursor ) }