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 ;
78import com .back .domain .board .common .dto .QAuthorResponse ;
89import com .back .domain .user .entity .QUser ;
910import com .back .domain .user .entity .QUserProfile ;
1516import com .querydsl .jpa .impl .JPAQueryFactory ;
1617import lombok .RequiredArgsConstructor ;
1718import 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- private static final Set <String > ALLOWED_SORT_FIELDS = Set .of ("createdAt" , "updatedAt" , "likeCount" );
27-
26+ // TODO: Comment에 likeCount 필드 추가에 따른 로직 개선
2827 /**
29- * 특정 게시글의 댓글 목록 조회
30- * - 총 쿼리 수: 3회
31- * 1.부모 댓글 목록을 페이징/정렬 조건으로 조회
32- * 2.부모 ID 목록으로 자식 댓글 전체 조회
33- * 3.부모 총 건수( count) 조회
28+ * 게시글 ID로 댓글 목록 조회
29+ * - 부모 댓글 페이징 + 자식 댓글 전체 조회
30+ * - likeCount는 부모/자식 댓글을 한 번에 조회 후 주입
31+ * - likeCount 정렬은 메모리에서 처리
32+ * - 총 쿼리 수: 4회 (부모조회 + 자식조회 + likeCount + count)
3433 *
35- * @param postId 게시글 Id
34+ * @param postId 게시글 Id
3635 * @param pageable 페이징 + 정렬 조건
3736 */
3837 @ Override
3938 public Page <CommentListResponse > getCommentsByPostId (Long postId , Pageable pageable ) {
4039 QComment comment = QComment .comment ;
40+ QCommentLike commentLike = QCommentLike .commentLike ;
4141
42- // 1. 정렬 조건 생성
43- List <OrderSpecifier <?>> orders = buildOrderSpecifiers (pageable );
42+ // 1. 정렬 조건 생성 (엔티티 필드 기반)
43+ List <OrderSpecifier <?>> orders = buildOrderSpecifiers (pageable , comment );
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- // 부모가 비어 있으면 즉시 빈 페이지 반환
5453 if (parents .isEmpty ()) {
5554 return new PageImpl <>(parents , pageable , 0 );
5655 }
5756
58- // 3. 부모 ID 수집
57+ // 3. 부모 ID 목록 수집
5958 List <Long > parentIds = parents .stream ()
6059 .map (CommentListResponse ::getCommentId )
6160 .toList ();
6261
63- // 4. 자식 댓글 조회 (부모 집합에 대한 전체 조회 )
62+ // 4. 자식 댓글 조회 (부모 ID 기준 )
6463 List <CommentListResponse > children = fetchComments (
6564 comment .parent .id .in (parentIds ),
66- List .of (comment .createdAt .asc ()), // 시간순 정렬
65+ List .of (comment .createdAt .asc ()),
6766 null ,
6867 null
6968 );
7069
71- // 5. 부모-자식 매핑
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. 부모-자식 매핑
7278 mapChildrenToParents (parents , children );
7379
74- // 6. 전체 부모 댓글 수 조회
80+ // 8. 정렬 후처리 (통계 필드 기반)
81+ parents = sortInMemoryIfNeeded (parents , pageable );
82+
83+ // 9. 전체 부모 댓글 수 조회
7584 Long total = queryFactory
7685 .select (comment .count ())
7786 .from (comment )
@@ -86,11 +95,7 @@ public Page<CommentListResponse> getCommentsByPostId(Long postId, Pageable pagea
8695 /**
8796 * 댓글 조회
8897 * - User / UserProfile join (N+1 방지)
89- *
90- * @param condition where 조건
91- * @param orders 정렬 조건
92- * @param offset 페이징 offset (null이면 미적용)
93- * @param limit 페이징 limit (null이면 미적용)
98+ * - likeCount는 이후 주입
9499 */
95100 private List <CommentListResponse > fetchComments (
96101 BooleanExpression condition ,
@@ -121,17 +126,42 @@ private List<CommentListResponse> fetchComments(
121126 .where (condition )
122127 .orderBy (orders .toArray (new OrderSpecifier [0 ]));
123128
124- // 페이징 적용
125129 if (offset != null && limit != null ) {
126130 query .offset (offset ).limit (limit );
127131 }
128132
129133 return query .fetch ();
130134 }
131135
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+
132162 /**
133163 * 부모/자식 관계 매핑
134- * - 자식 목록을 parentId 기준으로 그룹화 후, 각 부모 DTO의 children에 설정
164+ * - childMap을 parentId 기준으로 그룹화 후 children 필드에 set
135165 */
136166 private void mapChildrenToParents (List <CommentListResponse > parents , List <CommentListResponse > children ) {
137167 if (children .isEmpty ()) return ;
@@ -140,37 +170,53 @@ private void mapChildrenToParents(List<CommentListResponse> parents, List<Commen
140170 .collect (Collectors .groupingBy (CommentListResponse ::getParentId ));
141171
142172 parents .forEach (parent ->
143- parent .setChildren (childMap .getOrDefault (parent .getCommentId (), Collections . emptyList ()))
173+ parent .setChildren (childMap .getOrDefault (parent .getCommentId (), List . of ()))
144174 );
145175 }
146176
147177 /**
148- * 정렬 조건 생성
149- * - Pageable의 Sort 정보를 QueryDSL OrderSpecifier 목록으로 변환
178+ * 정렬 처리 (DB 정렬)
179+ * - createdAt, updatedAt 등 엔티티 필드
150180 */
151- private List <OrderSpecifier <?>> buildOrderSpecifiers (Pageable pageable ) {
152- QComment comment = QComment .comment ;
181+ private List <OrderSpecifier <?>> buildOrderSpecifiers (Pageable pageable , QComment comment ) {
153182 PathBuilder <Comment > entityPath = new PathBuilder <>(Comment .class , comment .getMetadata ());
154183 List <OrderSpecifier <?>> orders = new ArrayList <>();
155184
156185 for (Sort .Order order : pageable .getSort ()) {
157- String property = order .getProperty ();
186+ String prop = order .getProperty ();
158187
159- // 화이트리스트에 포함된 필드만 허용
160- if (!ALLOWED_SORT_FIELDS .contains (property )) {
161- // 허용되지 않은 정렬 키는 무시 (런타임 예외 대신 안전하게 스킵)
188+ // 통계 필드는 메모리 정렬에서 처리
189+ if (prop .equals ("likeCount" )) {
162190 continue ;
163191 }
164-
165192 Order direction = order .isAscending () ? Order .ASC : Order .DESC ;
166- orders .add (new OrderSpecifier <>(direction , entityPath .getComparable (property , Comparable .class )));
193+ orders .add (new OrderSpecifier <>(direction , entityPath .getComparable (prop , Comparable .class )));
167194 }
168195
169- // 명시된 정렬이 없으면 기본 정렬(createdAt DESC) 적용
170- if (orders .isEmpty ()) {
171- orders .add (new OrderSpecifier <>(Order .DESC , comment .createdAt ));
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+ }
172218 }
173219
174- return orders ;
220+ return results ;
175221 }
176- }
222+ }
0 commit comments