feat(character-comment): 댓글/대댓글 API
- 커서를 추가하여 페이징 처리
This commit is contained in:
		| @@ -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) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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? | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -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? | ||||||
|  |  | ||||||
|     // 전체(상위+답글) 활성 댓글 총 개수 |     // 전체(상위+답글) 활성 댓글 총 개수 | ||||||
|   | |||||||
| @@ -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) { | ||||||
|  |             commentRepository.findByChatCharacter_IdAndIsActiveTrueAndParentIsNullOrderByCreatedAtDesc( | ||||||
|                 characterId, |                 characterId, | ||||||
|                 pageable |                 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 | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user