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+ import org .springframework .stereotype .Repository ;
19+
1920import java .util .*;
2021import java .util .stream .Collectors ;
2122
23+ @ Repository
2224@ RequiredArgsConstructor
2325public class CommentRepositoryImpl implements CommentRepositoryCustom {
2426 private final JPAQueryFactory queryFactory ;
2527
26- // TODO: Comment에 likeCount 필드 추가에 따른 로직 개선
28+ private static final Set <String > ALLOWED_SORT_FIELDS = Set .of ("createdAt" , "updatedAt" , "likeCount" );
29+
2730 /**
28- * 게시글 ID로 댓글 목록 조회
29- * - 부모 댓글 페이징 + 자식 댓글 전체 조회
30- * - likeCount는 부모/자식 댓글을 한 번에 조회 후 주입
31- * - likeCount 정렬은 메모리에서 처리
32- * - 총 쿼리 수: 4회 (부모조회 + 자식조회 + likeCount + count)
31+ * 특정 게시글의 댓글 목록 조회
32+ * - 총 쿼리 수: 3회
33+ * 1.부모 댓글 목록을 페이징/정렬 조건으로 조회
34+ * 2.부모 ID 목록으로 자식 댓글 전체 조회
35+ * 3.부모 총 건수( count) 조회
3336 *
34- * @param postId 게시글 Id
37+ * @param postId 게시글 Id
3538 * @param pageable 페이징 + 정렬 조건
3639 */
3740 @ Override
3841 public Page <CommentListResponse > getCommentsByPostId (Long postId , Pageable pageable ) {
3942 QComment comment = QComment .comment ;
40- QCommentLike commentLike = QCommentLike .commentLike ;
4143
42- // 1. 정렬 조건 생성 (엔티티 필드 기반)
43- List <OrderSpecifier <?>> orders = buildOrderSpecifiers (pageable , comment );
44+ // 1. 정렬 조건 생성
45+ List <OrderSpecifier <?>> orders = buildOrderSpecifiers (pageable );
4446
45- // 2. 부모 댓글 조회 (페이징)
47+ // 2. 부모 댓글 조회 (페이징 적용 )
4648 List <CommentListResponse > parents = fetchComments (
4749 comment .post .id .eq (postId ).and (comment .parent .isNull ()),
4850 orders ,
4951 pageable .getOffset (),
5052 pageable .getPageSize ()
5153 );
5254
55+ // 부모가 비어 있으면 즉시 빈 페이지 반환
5356 if (parents .isEmpty ()) {
5457 return new PageImpl <>(parents , pageable , 0 );
5558 }
5659
57- // 3. 부모 ID 목록 수집
60+ // 3. 부모 ID 수집
5861 List <Long > parentIds = parents .stream ()
5962 .map (CommentListResponse ::getCommentId )
6063 .toList ();
6164
62- // 4. 자식 댓글 조회 (부모 ID 기준 )
65+ // 4. 자식 댓글 조회 (부모 집합에 대한 전체 조회 )
6366 List <CommentListResponse > children = fetchComments (
6467 comment .parent .id .in (parentIds ),
65- List .of (comment .createdAt .asc ()),
68+ List .of (comment .createdAt .asc ()), // 시간순 정렬
6669 null ,
6770 null
6871 );
6972
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. 부모-자식 매핑
73+ // 5. 부모-자식 매핑
7874 mapChildrenToParents (parents , children );
7975
80- // 8. 정렬 후처리 (통계 필드 기반)
81- parents = sortInMemoryIfNeeded (parents , pageable );
82-
83- // 9. 전체 부모 댓글 수 조회
76+ // 6. 전체 부모 댓글 수 조회
8477 Long total = queryFactory
8578 .select (comment .count ())
8679 .from (comment )
@@ -95,7 +88,11 @@ public Page<CommentListResponse> getCommentsByPostId(Long postId, Pageable pagea
9588 /**
9689 * 댓글 조회
9790 * - User / UserProfile join (N+1 방지)
98- * - likeCount는 이후 주입
91+ *
92+ * @param condition where 조건
93+ * @param orders 정렬 조건
94+ * @param offset 페이징 offset (null이면 미적용)
95+ * @param limit 페이징 limit (null이면 미적용)
9996 */
10097 private List <CommentListResponse > fetchComments (
10198 BooleanExpression condition ,
@@ -105,7 +102,7 @@ private List<CommentListResponse> fetchComments(
105102 ) {
106103 QComment comment = QComment .comment ;
107104 QUser user = QUser .user ;
108- QUserProfile profile = QUserProfile .userProfile ;
105+ QUserProfile profile = QUserProfile .userProfile ;
109106
110107 var query = queryFactory
111108 .select (new QCommentListResponse (
@@ -114,8 +111,8 @@ private List<CommentListResponse> fetchComments(
114111 comment .parent .id ,
115112 new QAuthorResponse (user .id , profile .nickname , profile .profileImageUrl ),
116113 comment .content ,
117- Expressions . constant ( 0L ), // likeCount는 별도 주입
118- Expressions .constant (false ),
114+ comment . likeCount ,
115+ Expressions .constant (false ), // likedByMe는 별도 주입
119116 comment .createdAt ,
120117 comment .updatedAt ,
121118 Expressions .constant (Collections .emptyList ()) // children은 별도 주입
@@ -126,42 +123,17 @@ private List<CommentListResponse> fetchComments(
126123 .where (condition )
127124 .orderBy (orders .toArray (new OrderSpecifier [0 ]));
128125
126+ // 페이징 적용
129127 if (offset != null && limit != null ) {
130128 query .offset (offset ).limit (limit );
131129 }
132130
133131 return query .fetch ();
134132 }
135133
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-
162134 /**
163135 * 부모/자식 관계 매핑
164- * - childMap을 parentId 기준으로 그룹화 후 children 필드에 set
136+ * - 자식 목록을 parentId 기준으로 그룹화 후, 각 부모 DTO의 children에 설정
165137 */
166138 private void mapChildrenToParents (List <CommentListResponse > parents , List <CommentListResponse > children ) {
167139 if (children .isEmpty ()) return ;
@@ -170,53 +142,37 @@ private void mapChildrenToParents(List<CommentListResponse> parents, List<Commen
170142 .collect (Collectors .groupingBy (CommentListResponse ::getParentId ));
171143
172144 parents .forEach (parent ->
173- parent .setChildren (childMap .getOrDefault (parent .getCommentId (), List . of ()))
145+ parent .setChildren (childMap .getOrDefault (parent .getCommentId (), Collections . emptyList ()))
174146 );
175147 }
176148
177149 /**
178- * 정렬 처리 (DB 정렬)
179- * - createdAt, updatedAt 등 엔티티 필드
150+ * 정렬 조건 생성
151+ * - Pageable의 Sort 정보를 QueryDSL OrderSpecifier 목록으로 변환
180152 */
181- private List <OrderSpecifier <?>> buildOrderSpecifiers (Pageable pageable , QComment comment ) {
153+ private List <OrderSpecifier <?>> buildOrderSpecifiers (Pageable pageable ) {
154+ QComment comment = QComment .comment ;
182155 PathBuilder <Comment > entityPath = new PathBuilder <>(Comment .class , comment .getMetadata ());
183156 List <OrderSpecifier <?>> orders = new ArrayList <>();
184157
185158 for (Sort .Order order : pageable .getSort ()) {
186- String prop = order .getProperty ();
159+ String property = order .getProperty ();
187160
188- // 통계 필드는 메모리 정렬에서 처리
189- if (prop .equals ("likeCount" )) {
161+ // 화이트리스트에 포함된 필드만 허용
162+ if (!ALLOWED_SORT_FIELDS .contains (property )) {
163+ // 허용되지 않은 정렬 키는 무시 (런타임 예외 대신 안전하게 스킵)
190164 continue ;
191165 }
166+
192167 Order direction = order .isAscending () ? Order .ASC : Order .DESC ;
193- orders .add (new OrderSpecifier <>(direction , entityPath .getComparable (prop , Comparable .class )));
168+ orders .add (new OrderSpecifier <>(direction , entityPath .getComparable (property , Comparable .class )));
194169 }
195170
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- }
171+ // 명시된 정렬이 없으면 기본 정렬(createdAt DESC) 적용
172+ if (orders .isEmpty ()) {
173+ orders .add (new OrderSpecifier <>(Order .DESC , comment .createdAt ));
218174 }
219175
220- return results ;
176+ return orders ;
221177 }
222178}
0 commit comments