1- package com .back .domain .board .comment .repository ;
1+ package com .back .domain .board .comment .repository . custom ;
22
33import com .back .domain .board .comment .dto .CommentListResponse ;
44import com .back .domain .board .comment .dto .QCommentListResponse ;
55import com .back .domain .board .comment .entity .Comment ;
66import com .back .domain .board .comment .entity .QComment ;
7- import com .back .domain .board .comment .entity .QCommentLike ;
87import com .back .domain .board .common .dto .QAuthorResponse ;
98import com .back .domain .user .entity .QUser ;
109import com .back .domain .user .entity .QUserProfile ;
1615import com .querydsl .jpa .impl .JPAQueryFactory ;
1716import lombok .RequiredArgsConstructor ;
1817import org .springframework .data .domain .*;
18+
1919import java .util .*;
2020import java .util .stream .Collectors ;
2121
2222@ RequiredArgsConstructor
2323public class CommentRepositoryImpl implements CommentRepositoryCustom {
2424 private final JPAQueryFactory queryFactory ;
2525
26- // TODO: Comment에 likeCount 필드 추가에 따른 로직 개선
26+ private static final Set <String > ALLOWED_SORT_FIELDS = Set .of ("createdAt" , "updatedAt" , "likeCount" );
27+
2728 /**
28- * 게시글 ID로 댓글 목록 조회
29- * - 부모 댓글 페이징 + 자식 댓글 전체 조회
30- * - likeCount는 부모/자식 댓글을 한 번에 조회 후 주입
31- * - likeCount 정렬은 메모리에서 처리
32- * - 총 쿼리 수: 4회 (부모조회 + 자식조회 + likeCount + count)
29+ * 특정 게시글의 댓글 목록 조회
30+ * - 총 쿼리 수: 3회
31+ * 1.부모 댓글 목록을 페이징/정렬 조건으로 조회
32+ * 2.부모 ID 목록으로 자식 댓글 전체 조회
33+ * 3.부모 총 건수( count) 조회
3334 *
34- * @param postId 게시글 Id
35+ * @param postId 게시글 Id
3536 * @param pageable 페이징 + 정렬 조건
3637 */
3738 @ Override
3839 public Page <CommentListResponse > getCommentsByPostId (Long postId , Pageable pageable ) {
3940 QComment comment = QComment .comment ;
40- QCommentLike commentLike = QCommentLike .commentLike ;
4141
42- // 1. 정렬 조건 생성 (엔티티 필드 기반)
43- List <OrderSpecifier <?>> orders = buildOrderSpecifiers (pageable , comment );
42+ // 1. 정렬 조건 생성
43+ List <OrderSpecifier <?>> orders = buildOrderSpecifiers (pageable );
4444
45- // 2. 부모 댓글 조회 (페이징)
45+ // 2. 부모 댓글 조회 (페이징 적용 )
4646 List <CommentListResponse > parents = fetchComments (
4747 comment .post .id .eq (postId ).and (comment .parent .isNull ()),
4848 orders ,
4949 pageable .getOffset (),
5050 pageable .getPageSize ()
5151 );
5252
53+ // 부모가 비어 있으면 즉시 빈 페이지 반환
5354 if (parents .isEmpty ()) {
5455 return new PageImpl <>(parents , pageable , 0 );
5556 }
5657
57- // 3. 부모 ID 목록 수집
58+ // 3. 부모 ID 수집
5859 List <Long > parentIds = parents .stream ()
5960 .map (CommentListResponse ::getCommentId )
6061 .toList ();
6162
62- // 4. 자식 댓글 조회 (부모 ID 기준 )
63+ // 4. 자식 댓글 조회 (부모 집합에 대한 전체 조회 )
6364 List <CommentListResponse > children = fetchComments (
6465 comment .parent .id .in (parentIds ),
65- List .of (comment .createdAt .asc ()),
66+ List .of (comment .createdAt .asc ()), // 시간순 정렬
6667 null ,
6768 null
6869 );
6970
70- // 5. 부모 + 자식 댓글 ID 합쳐 likeCount 조회 (쿼리 1회)
71- Map <Long , Long > likeCountMap = fetchLikeCounts (parentIds , children );
72-
73- // 6. likeCount 주입
74- parents .forEach (p -> p .setLikeCount (likeCountMap .getOrDefault (p .getCommentId (), 0L )));
75- children .forEach (c -> c .setLikeCount (likeCountMap .getOrDefault (c .getCommentId (), 0L )));
76-
77- // 7. 부모-자식 매핑
71+ // 5. 부모-자식 매핑
7872 mapChildrenToParents (parents , children );
7973
80- // 8. 정렬 후처리 (통계 필드 기반)
81- parents = sortInMemoryIfNeeded (parents , pageable );
82-
83- // 9. 전체 부모 댓글 수 조회
74+ // 6. 전체 부모 댓글 수 조회
8475 Long total = queryFactory
8576 .select (comment .count ())
8677 .from (comment )
@@ -95,7 +86,11 @@ public Page<CommentListResponse> getCommentsByPostId(Long postId, Pageable pagea
9586 /**
9687 * 댓글 조회
9788 * - User / UserProfile join (N+1 방지)
98- * - likeCount는 이후 주입
89+ *
90+ * @param condition where 조건
91+ * @param orders 정렬 조건
92+ * @param offset 페이징 offset (null이면 미적용)
93+ * @param limit 페이징 limit (null이면 미적용)
9994 */
10095 private List <CommentListResponse > fetchComments (
10196 BooleanExpression condition ,
@@ -126,42 +121,17 @@ private List<CommentListResponse> fetchComments(
126121 .where (condition )
127122 .orderBy (orders .toArray (new OrderSpecifier [0 ]));
128123
124+ // 페이징 적용
129125 if (offset != null && limit != null ) {
130126 query .offset (offset ).limit (limit );
131127 }
132128
133129 return query .fetch ();
134130 }
135131
136- /**
137- * likeCount 일괄 조회
138- * - IN 조건 기반 groupBy 쿼리 1회
139- * - 부모/자식 댓글을 한 번에 조회
140- */
141- private Map <Long , Long > fetchLikeCounts (List <Long > parentIds , List <CommentListResponse > children ) {
142- QCommentLike commentLike = QCommentLike .commentLike ;
143-
144- List <Long > allIds = new ArrayList <>(parentIds );
145- allIds .addAll (children .stream ().map (CommentListResponse ::getCommentId ).toList ());
146-
147- if (allIds .isEmpty ()) return Map .of ();
148-
149- return queryFactory
150- .select (commentLike .comment .id , commentLike .count ())
151- .from (commentLike )
152- .where (commentLike .comment .id .in (allIds ))
153- .groupBy (commentLike .comment .id )
154- .fetch ()
155- .stream ()
156- .collect (Collectors .toMap (
157- tuple -> tuple .get (commentLike .comment .id ),
158- tuple -> tuple .get (commentLike .count ())
159- ));
160- }
161-
162132 /**
163133 * 부모/자식 관계 매핑
164- * - childMap을 parentId 기준으로 그룹화 후 children 필드에 set
134+ * - 자식 목록을 parentId 기준으로 그룹화 후, 각 부모 DTO의 children에 설정
165135 */
166136 private void mapChildrenToParents (List <CommentListResponse > parents , List <CommentListResponse > children ) {
167137 if (children .isEmpty ()) return ;
@@ -170,53 +140,37 @@ private void mapChildrenToParents(List<CommentListResponse> parents, List<Commen
170140 .collect (Collectors .groupingBy (CommentListResponse ::getParentId ));
171141
172142 parents .forEach (parent ->
173- parent .setChildren (childMap .getOrDefault (parent .getCommentId (), List . of ()))
143+ parent .setChildren (childMap .getOrDefault (parent .getCommentId (), Collections . emptyList ()))
174144 );
175145 }
176146
177147 /**
178- * 정렬 처리 (DB 정렬)
179- * - createdAt, updatedAt 등 엔티티 필드
148+ * 정렬 조건 생성
149+ * - Pageable의 Sort 정보를 QueryDSL OrderSpecifier 목록으로 변환
180150 */
181- private List <OrderSpecifier <?>> buildOrderSpecifiers (Pageable pageable , QComment comment ) {
151+ private List <OrderSpecifier <?>> buildOrderSpecifiers (Pageable pageable ) {
152+ QComment comment = QComment .comment ;
182153 PathBuilder <Comment > entityPath = new PathBuilder <>(Comment .class , comment .getMetadata ());
183154 List <OrderSpecifier <?>> orders = new ArrayList <>();
184155
185156 for (Sort .Order order : pageable .getSort ()) {
186- String prop = order .getProperty ();
157+ String property = order .getProperty ();
187158
188- // 통계 필드는 메모리 정렬에서 처리
189- if (prop .equals ("likeCount" )) {
159+ // 화이트리스트에 포함된 필드만 허용
160+ if (!ALLOWED_SORT_FIELDS .contains (property )) {
161+ // 허용되지 않은 정렬 키는 무시 (런타임 예외 대신 안전하게 스킵)
190162 continue ;
191163 }
164+
192165 Order direction = order .isAscending () ? Order .ASC : Order .DESC ;
193- orders .add (new OrderSpecifier <>(direction , entityPath .getComparable (prop , Comparable .class )));
166+ orders .add (new OrderSpecifier <>(direction , entityPath .getComparable (property , Comparable .class )));
194167 }
195168
196- return orders ;
197- }
198-
199- /**
200- * 통계 기반 정렬 처리 (메모리)
201- * - likeCount 등 통계 필드
202- * - 페이지 단위라 성능에 영향 없음
203- */
204- private List <CommentListResponse > sortInMemoryIfNeeded (List <CommentListResponse > results , Pageable pageable ) {
205- if (results .isEmpty () || !pageable .getSort ().isSorted ()) return results ;
206-
207- for (Sort .Order order : pageable .getSort ()) {
208- Comparator <CommentListResponse > comparator = null ;
209-
210- if ("likeCount" .equals (order .getProperty ())) {
211- comparator = Comparator .comparing (CommentListResponse ::getLikeCount );
212- }
213-
214- if (comparator != null ) {
215- if (order .isDescending ()) comparator = comparator .reversed ();
216- results .sort (comparator );
217- }
169+ // 명시된 정렬이 없으면 기본 정렬(createdAt DESC) 적용
170+ if (orders .isEmpty ()) {
171+ orders .add (new OrderSpecifier <>(Order .DESC , comment .createdAt ));
218172 }
219173
220- return results ;
174+ return orders ;
221175 }
222176}
0 commit comments