Skip to content

Commit 9fdf2e8

Browse files
authored
feat(coll): 댓글 조회 API에 페이징 추가 (#290)
* feat(coll): 댓글 조회 페이징을 위한 커맨드 dto 생성 * feat(coll): 댓글 조회 api 페이징 구현 * feat: admin에서도 페이징 적용 * test: 댓글 조회 페이징 관련 테스트 수정 * refactor: 페이징 커맨드 dto를 공통 유틸로 묶어서 추상화 * feat(coll): 댓글 조회 응답에 hasNext 필드 추가 "더보기"와 같은 기능 도입 시 편의를 위해 * test(coll): hasNext 필드 추가에 따른 테스트 수정
1 parent cca0ee8 commit 9fdf2e8

15 files changed

Lines changed: 148 additions & 84 deletions

File tree

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
package org.devkor.apu.saerok_server.domain.admin.audit.application.dto;
22

3-
public record AdminAuditQueryCommand(Integer page, Integer size) {
3+
import org.devkor.apu.saerok_server.global.shared.util.Pageable;
44

5-
public boolean hasValidPagination() {
6-
if (page == null && size == null) return true;
7-
if (page == null || size == null) return false;
8-
return page >= 1 && size > 0;
9-
}
10-
}
5+
public record AdminAuditQueryCommand(
6+
Integer page,
7+
Integer size
8+
) implements Pageable {}

src/main/java/org/devkor/apu/saerok_server/domain/admin/report/application/AdminReportQueryService.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.GetCollectionCommentsResponse;
99
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.GetCollectionDetailResponse;
1010
import org.devkor.apu.saerok_server.domain.collection.application.CollectionCommentQueryService;
11+
import org.devkor.apu.saerok_server.domain.collection.application.dto.CommentQueryCommand;
1112
import org.devkor.apu.saerok_server.domain.collection.application.helper.CollectionImageUrlService;
1213
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollection;
1314
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollectionComment;
@@ -99,7 +100,7 @@ public ReportedCollectionDetailResponse getReportedCollectionDetail(Long reportI
99100
);
100101

101102
// 댓글 목록 (관리자 기준 isLiked/isMine 계산 불필요)
102-
GetCollectionCommentsResponse comments = commentQueryService.getComments(collection.getId(), null);
103+
GetCollectionCommentsResponse comments = commentQueryService.getComments(collection.getId(), null, new CommentQueryCommand(null, null));
103104

104105
return new ReportedCollectionDetailResponse(report.getId(), collectionDetail, comments);
105106
}
@@ -126,7 +127,7 @@ public ReportedCommentDetailResponse getReportedCommentDetail(Long reportId) {
126127
);
127128

128129
// 댓글 목록
129-
GetCollectionCommentsResponse comments = commentQueryService.getComments(parentCollection.getId(), null);
130+
GetCollectionCommentsResponse comments = commentQueryService.getComments(parentCollection.getId(), null, new CommentQueryCommand(null, null));
130131

131132
// 신고된 댓글 정보
132133
ReportedCommentDetailResponse.ReportedComment commentDto =

src/main/java/org/devkor/apu/saerok_server/domain/collection/api/CollectionCommentController.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.devkor.apu.saerok_server.domain.collection.api;
22

33
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.Parameter;
45
import io.swagger.v3.oas.annotations.media.Content;
56
import io.swagger.v3.oas.annotations.media.Schema;
67
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@@ -13,7 +14,9 @@
1314
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.*;
1415
import org.devkor.apu.saerok_server.domain.collection.application.CollectionCommentCommandService;
1516
import org.devkor.apu.saerok_server.domain.collection.application.CollectionCommentQueryService;
17+
import org.devkor.apu.saerok_server.domain.collection.application.dto.CommentQueryCommand;
1618
import org.devkor.apu.saerok_server.global.security.principal.UserPrincipal;
19+
import org.devkor.apu.saerok_server.global.shared.exception.BadRequestException;
1720
import org.springframework.http.HttpStatus;
1821
import org.springframework.security.access.prepost.PreAuthorize;
1922
import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -102,19 +105,34 @@ public void deleteComment(
102105
@PermitAll
103106
@Operation(
104107
summary = "컬렉션 댓글 목록 조회 (인증: optional)",
108+
description = """
109+
컬렉션의 댓글 목록을 조회합니다.
110+
111+
📄 **페이징 (선택)**
112+
- `page`와 `size`는 둘 다 제공해야 하며, 하나만 제공 시 Bad Request가 발생합니다.
113+
- 생략하면 전체 결과를 반환합니다.
114+
""",
105115
security = @SecurityRequirement(name = "bearerAuth"),
106116
responses = {
107117
@ApiResponse(responseCode = "200", description = "댓글 목록 조회 성공",
108118
content = @Content(schema = @Schema(implementation = GetCollectionCommentsResponse.class))),
119+
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
109120
@ApiResponse(responseCode = "404", description = "컬렉션이 존재하지 않음", content = @Content)
110121
}
111122
)
112123
public GetCollectionCommentsResponse listComments(
113124
@PathVariable Long collectionId,
114-
@AuthenticationPrincipal UserPrincipal userPrincipal
125+
@AuthenticationPrincipal UserPrincipal userPrincipal,
126+
@Parameter(description = "페이지 번호 (1부터 시작)", example = "1") @RequestParam(required = false) Integer page,
127+
@Parameter(description = "페이지 크기", example = "20") @RequestParam(required = false) Integer size
115128
) {
129+
CommentQueryCommand command = new CommentQueryCommand(page, size);
130+
if (!command.hasValidPagination()) {
131+
throw new BadRequestException("page와 size 값이 유효하지 않아요.");
132+
}
133+
116134
Long userId = userPrincipal == null ? null : userPrincipal.getId();
117-
return commentQueryService.getComments(collectionId, userId);
135+
return commentQueryService.getComments(collectionId, userId, command);
118136
}
119137

120138
/* 댓글 개수 */

src/main/java/org/devkor/apu/saerok_server/domain/collection/api/dto/response/GetCollectionCommentsResponse.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
public record GetCollectionCommentsResponse(
99
List<Item> items,
1010
@Schema(description = "내 컬렉션인지 여부", example = "true")
11-
Boolean isMyCollection
11+
Boolean isMyCollection,
12+
@Schema(description = "다음 페이지 존재 여부 (페이징 요청 시에만 유효)", example = "true")
13+
Boolean hasNext
1214
) {
1315

1416
public record Item(

src/main/java/org/devkor/apu/saerok_server/domain/collection/application/CollectionCommentQueryService.java

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import lombok.RequiredArgsConstructor;
44
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.GetCollectionCommentCountResponse;
55
import org.devkor.apu.saerok_server.domain.collection.api.dto.response.GetCollectionCommentsResponse;
6+
import org.devkor.apu.saerok_server.domain.collection.application.dto.CommentQueryCommand;
67
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollection;
78
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollectionComment;
89
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionCommentLikeRepository;
@@ -16,6 +17,7 @@
1617
import org.springframework.stereotype.Service;
1718
import org.springframework.transaction.annotation.Transactional;
1819

20+
import java.util.ArrayList;
1921
import java.util.List;
2022
import java.util.Map;
2123
import java.util.stream.Collectors;
@@ -33,18 +35,27 @@ public class CollectionCommentQueryService {
3335
private final CommentContentResolver commentContentResolver;
3436

3537
/* 댓글 목록 (createdAt ASC) */
36-
public GetCollectionCommentsResponse getComments(Long collectionId, Long userId) {
38+
public GetCollectionCommentsResponse getComments(Long collectionId, Long userId, CommentQueryCommand command) {
3739

3840
UserBirdCollection collection = collectionRepository.findById(collectionId)
3941
.orElseThrow(() -> new NotFoundException("해당 id의 컬렉션이 존재하지 않아요"));
40-
42+
4143
// 내 컬렉션인지 여부 판단 (비회원인 경우 false)
4244
boolean isMyCollection = userId != null && userId.equals(collection.getUser().getId());
4345

44-
// 1. 댓글 목록 조회
45-
List<UserBirdCollectionComment> comments = commentRepository.findByCollectionId(collectionId);
46-
47-
// 2. 댓글 ID 목록 추출
46+
// 1. 댓글 목록 조회 (페이징 시 size+1개 조회됨)
47+
List<UserBirdCollectionComment> comments = commentRepository.findByCollectionId(collectionId, command);
48+
49+
// 2. hasNext 판단 및 초과분 제거
50+
Boolean hasNext = null;
51+
if (command.hasPagination()) {
52+
hasNext = comments.size() > command.size();
53+
if (hasNext) {
54+
comments = new ArrayList<>(comments.subList(0, command.size()));
55+
}
56+
}
57+
58+
// 3. 댓글 ID 목록 추출
4859
List<Long> commentIds = comments.stream()
4960
.map(UserBirdCollectionComment::getId)
5061
.toList();
@@ -73,8 +84,8 @@ public GetCollectionCommentsResponse getComments(Long collectionId, Long userId)
7384
Map<Long, String> profileImageUrls = userProfileImageUrlService.getProfileImageUrlsFor(users);
7485
Map<Long, String> thumbnailProfileImageUrls = userProfileImageUrlService.getProfileThumbnailImageUrlsFor(users);
7586

76-
// 7. 응답 생성
77-
return collectionCommentWebMapper.toGetCollectionCommentsResponse(comments, likeCounts, likeStatuses, mineStatuses, profileImageUrls, thumbnailProfileImageUrls, isMyCollection, commentContentResolver);
87+
// 8. 응답 생성
88+
return collectionCommentWebMapper.toGetCollectionCommentsResponse(comments, likeCounts, likeStatuses, mineStatuses, profileImageUrls, thumbnailProfileImageUrls, isMyCollection, hasNext, commentContentResolver);
7889
}
7990

8091
/* 댓글 개수 */
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.devkor.apu.saerok_server.domain.collection.application.dto;
2+
3+
import org.devkor.apu.saerok_server.global.shared.util.Pageable;
4+
5+
public record CommentQueryCommand(
6+
Integer page,
7+
Integer size
8+
) implements Pageable {}

src/main/java/org/devkor/apu/saerok_server/domain/collection/core/repository/CollectionCommentRepository.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package org.devkor.apu.saerok_server.domain.collection.core.repository;
22

33
import jakarta.persistence.EntityManager;
4+
import jakarta.persistence.Query;
45
import lombok.RequiredArgsConstructor;
6+
import org.devkor.apu.saerok_server.domain.collection.application.dto.CommentQueryCommand;
57
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollectionComment;
68
import org.springframework.stereotype.Repository;
79

@@ -24,16 +26,27 @@ public Optional<UserBirdCollectionComment> findById(Long id) {
2426

2527
public void remove(UserBirdCollectionComment comment) { em.remove(comment); }
2628

27-
public List<UserBirdCollectionComment> findByCollectionId(Long collectionId) {
28-
return em.createQuery(
29+
public List<UserBirdCollectionComment> findByCollectionId(Long collectionId, CommentQueryCommand command) {
30+
Query query = em.createQuery(
2931
"SELECT DISTINCT c FROM UserBirdCollectionComment c " +
3032
"LEFT JOIN FETCH c.user " +
3133
"LEFT JOIN FETCH c.parent " +
3234
"WHERE c.collection.id = :collectionId " +
3335
"ORDER BY c.createdAt ASC",
3436
UserBirdCollectionComment.class)
35-
.setParameter("collectionId", collectionId)
36-
.getResultList();
37+
.setParameter("collectionId", collectionId);
38+
39+
applyPagination(query, command);
40+
return query.getResultList();
41+
}
42+
43+
// 헬퍼 메서드 (hasNext 판단을 위해 size+1개 조회)
44+
private void applyPagination(Query query, CommentQueryCommand command) {
45+
if (command.hasPagination()) {
46+
int offset = (command.page() - 1) * command.size();
47+
query.setFirstResult(offset);
48+
query.setMaxResults(command.size() + 1);
49+
}
3750
}
3851

3952
public long countByCollectionId(Long collectionId) {

src/main/java/org/devkor/apu/saerok_server/domain/collection/mapper/CollectionCommentWebMapper.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ default GetCollectionCommentsResponse toGetCollectionCommentsResponse(
2323
Map<Long, String> profileImageUrls,
2424
Map<Long, String> thumbnailProfileImageUrls,
2525
Boolean isMyCollection,
26+
Boolean hasNext,
2627
@Context CommentContentResolver commentContentResolver) {
2728
if (entities == null || entities.isEmpty()) {
28-
return new GetCollectionCommentsResponse(List.of(), isMyCollection);
29+
return new GetCollectionCommentsResponse(List.of(), isMyCollection, hasNext);
2930
}
3031

3132
for (UserBirdCollectionComment entity : entities) {
@@ -73,7 +74,7 @@ default GetCollectionCommentsResponse toGetCollectionCommentsResponse(
7374
return buildCommentItem(comment, likeCounts, likeStatuses, mineStatuses, profileImageUrls, thumbnailProfileImageUrls, replies, commentContentResolver);
7475
})
7576
.toList();
76-
return new GetCollectionCommentsResponse(items, isMyCollection);
77+
return new GetCollectionCommentsResponse(items, isMyCollection, hasNext);
7778
}
7879

7980
/* 댓글 엔티티 → Item DTO (공통 매핑 로직) */
Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,9 @@
11
package org.devkor.apu.saerok_server.domain.community.application.dto;
22

3+
import org.devkor.apu.saerok_server.global.shared.util.Pageable;
4+
35
public record CommunityQueryCommand(
46
Integer page,
57
Integer size,
68
String query
7-
) {
8-
public boolean hasValidPagination() {
9-
if ((page != null && size == null) || (page == null && size != null)) {
10-
return false;
11-
}
12-
13-
if (page == null) { // page == null && size == null
14-
return true;
15-
}
16-
17-
return page >= 1 && size >= 1;
18-
}
19-
20-
public boolean hasPagination() {
21-
return page != null && size != null;
22-
}
23-
}
9+
) implements Pageable {}

src/main/java/org/devkor/apu/saerok_server/domain/dex/bird/application/dto/BirdSearchCommand.java

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package org.devkor.apu.saerok_server.domain.dex.bird.application.dto;
22

3+
import org.devkor.apu.saerok_server.global.shared.util.Pageable;
4+
35
import java.util.List;
46

5-
public record BirdSearchCommand (
7+
public record BirdSearchCommand(
68
Integer page,
79
Integer size,
810
String q,
@@ -11,23 +13,7 @@ public record BirdSearchCommand (
1113
List<String> seasons,
1214
String sort,
1315
String sortDir
14-
){
15-
/**
16-
* size와 page 중 한쪽만 null일 수 없고,
17-
* size >= 1, page >= 1
18-
* @return 해당 조건을 만족하는지
19-
*/
20-
public boolean hasValidPagination() {
21-
if ((page != null && size == null) || (page == null && size != null)) {
22-
return false;
23-
}
24-
25-
if (page == null) { // page == null && size == null
26-
return true;
27-
}
28-
29-
return page >= 1 && size >= 1;
30-
}
16+
) implements Pageable {
3117

3218
public List<String> getSizeCategories() {
3319
return sizeCategories == null ? List.of() : sizeCategories;

0 commit comments

Comments
 (0)