Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.back.domain.board.controller;
package com.back.domain.board.comment.controller;

import com.back.domain.board.dto.CommentListResponse;
import com.back.domain.board.dto.CommentRequest;
import com.back.domain.board.dto.CommentResponse;
import com.back.domain.board.dto.PageResponse;
import com.back.domain.board.service.CommentService;
import com.back.domain.board.comment.dto.CommentListResponse;
import com.back.domain.board.comment.dto.CommentRequest;
import com.back.domain.board.comment.dto.CommentResponse;
import com.back.domain.board.common.dto.PageResponse;
import com.back.domain.board.comment.service.CommentService;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CustomUserDetails;
import jakarta.validation.Valid;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.back.domain.board.controller;
package com.back.domain.board.comment.controller;

import com.back.domain.board.dto.CommentListResponse;
import com.back.domain.board.dto.CommentRequest;
import com.back.domain.board.dto.CommentResponse;
import com.back.domain.board.dto.PageResponse;
import com.back.domain.board.comment.dto.CommentListResponse;
import com.back.domain.board.comment.dto.CommentRequest;
import com.back.domain.board.comment.dto.CommentResponse;
import com.back.domain.board.common.dto.PageResponse;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.back.domain.board.dto;
package com.back.domain.board.comment.dto;

import com.back.domain.board.common.dto.AuthorResponse;
import com.querydsl.core.annotations.QueryProjection;
import lombok.Getter;
import lombok.Setter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.back.domain.board.dto;
package com.back.domain.board.comment.dto;

import jakarta.validation.constraints.NotBlank;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.back.domain.board.dto;
package com.back.domain.board.comment.dto;

import com.back.domain.board.entity.Comment;
import com.back.domain.board.common.dto.AuthorResponse;
import com.back.domain.board.comment.entity.Comment;

import java.time.LocalDateTime;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.back.domain.board.entity;
package com.back.domain.board.comment.entity;

import com.back.domain.board.post.entity.Post;
import com.back.domain.user.entity.User;
import com.back.global.entity.BaseEntity;
import jakarta.persistence.*;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.back.domain.board.entity;
package com.back.domain.board.comment.entity;

import com.back.domain.user.entity.User;
import com.back.global.entity.BaseEntity;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.back.domain.board.repository;
package com.back.domain.board.comment.repository;

import com.back.domain.board.entity.Comment;
import com.back.domain.board.comment.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.back.domain.board.repository;
package com.back.domain.board.comment.repository;

import com.back.domain.board.dto.CommentListResponse;
import com.back.domain.board.comment.dto.CommentListResponse;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.back.domain.board.repository;

import com.back.domain.board.dto.CommentListResponse;
import com.back.domain.board.dto.QAuthorResponse;
import com.back.domain.board.dto.QCommentListResponse;
import com.back.domain.board.entity.Comment;
import com.back.domain.board.entity.QComment;
import com.back.domain.board.entity.QCommentLike;
package com.back.domain.board.comment.repository;

import com.back.domain.board.comment.dto.CommentListResponse;
import com.back.domain.board.comment.dto.QCommentListResponse;
import com.back.domain.board.comment.entity.Comment;
import com.back.domain.board.comment.entity.QComment;
import com.back.domain.board.comment.entity.QCommentLike;
import com.back.domain.board.common.dto.QAuthorResponse;
import com.back.domain.user.entity.QUser;
import com.back.domain.user.entity.QUserProfile;
import com.querydsl.core.types.Order;
Expand All @@ -15,27 +15,34 @@
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

import org.springframework.data.domain.*;
import java.util.*;
import java.util.stream.Collectors;

@RequiredArgsConstructor
public class CommentRepositoryImpl implements CommentRepositoryCustom {

private final JPAQueryFactory queryFactory;

/**
* 게시글 ID로 댓글 목록 조회
* - 부모 댓글 페이징 + 자식 댓글 전체 조회
* - likeCount는 부모/자식 댓글을 한 번에 조회 후 주입
* - likeCount 정렬은 메모리에서 처리
* - 총 쿼리 수: 4회 (부모조회 + 자식조회 + likeCount + count)
*
* @param postId 게시글 Id
* @param pageable 페이징 + 정렬 조건
*/
@Override
public Page<CommentListResponse> getCommentsByPostId(Long postId, Pageable pageable) {
QComment comment = QComment.comment;
QCommentLike commentLike = QCommentLike.commentLike;

// 정렬 조건
List<OrderSpecifier<?>> orders = buildOrderSpecifiers(pageable, comment, commentLike);
// 1. 정렬 조건 생성 (엔티티 필드 기반)
List<OrderSpecifier<?>> orders = buildOrderSpecifiers(pageable, comment);

// 부모 댓글 조회
// 2. 부모 댓글 조회 (페이징)
List<CommentListResponse> parents = fetchComments(
comment.post.id.eq(postId).and(comment.parent.isNull()),
orders,
Expand All @@ -47,48 +54,33 @@ public Page<CommentListResponse> getCommentsByPostId(Long postId, Pageable pagea
return new PageImpl<>(parents, pageable, 0);
}

// 부모 id 수집
// 3. 부모 ID 목록 수집
List<Long> parentIds = parents.stream()
.map(CommentListResponse::getCommentId)
.toList();

// 자식 댓글 조회
// 4. 자식 댓글 조회 (부모 ID 기준)
List<CommentListResponse> children = fetchComments(
comment.parent.id.in(parentIds),
List.of(comment.createdAt.asc()),
null,
null
);

// 부모 + 자식 id 합쳐서 likeCount 한 번에 조회
List<Long> allIds = new ArrayList<>(parentIds);
allIds.addAll(children.stream().map(CommentListResponse::getCommentId).toList());

Map<Long, Long> likeCountMap = queryFactory
.select(commentLike.comment.id, commentLike.count())
.from(commentLike)
.where(commentLike.comment.id.in(allIds))
.groupBy(commentLike.comment.id)
.fetch()
.stream()
.collect(Collectors.toMap(
tuple -> tuple.get(commentLike.comment.id),
tuple -> tuple.get(commentLike.count())
));
// 5. 부모 + 자식 댓글 ID 합쳐 likeCount 조회 (쿼리 1회)
Map<Long, Long> likeCountMap = fetchLikeCounts(parentIds, children);

// likeCount 세팅
// 6. likeCount 주입
parents.forEach(p -> p.setLikeCount(likeCountMap.getOrDefault(p.getCommentId(), 0L)));
children.forEach(c -> c.setLikeCount(likeCountMap.getOrDefault(c.getCommentId(), 0L)));

// parentId → children 매핑
Map<Long, List<CommentListResponse>> childMap = children.stream()
.collect(Collectors.groupingBy(CommentListResponse::getParentId));
// 7. 부모-자식 매핑
mapChildrenToParents(parents, children);

parents.forEach(p ->
p.setChildren(childMap.getOrDefault(p.getCommentId(), List.of()))
);
// 8. 정렬 후처리 (통계 필드 기반)
parents = sortInMemoryIfNeeded(parents, pageable);

// 총 개수 (부모 댓글만 카운트)
// 9. 전체 부모 댓글 수 조회
Long total = queryFactory
.select(comment.count())
.from(comment)
Expand All @@ -98,8 +90,12 @@ public Page<CommentListResponse> getCommentsByPostId(Long postId, Pageable pagea
return new PageImpl<>(parents, pageable, total != null ? total : 0L);
}

// -------------------- 내부 메서드 --------------------

/**
* 공통 댓글 조회 메서드 (부모/자식 공통)
* 댓글 조회
* - User / UserProfile join (N+1 방지)
* - likeCount는 이후 주입
*/
private List<CommentListResponse> fetchComments(
BooleanExpression condition,
Expand All @@ -118,10 +114,10 @@ private List<CommentListResponse> fetchComments(
comment.parent.id,
new QAuthorResponse(user.id, profile.nickname),
comment.content,
Expressions.constant(0L), // likeCount placeholder
Expressions.constant(0L), // likeCount는 별도 주입
comment.createdAt,
comment.updatedAt,
Expressions.constant(Collections.emptyList())
Expressions.constant(Collections.emptyList()) // children은 별도 주입
))
.from(comment)
.leftJoin(comment.user, user)
Expand All @@ -137,22 +133,89 @@ private List<CommentListResponse> fetchComments(
}

/**
* 정렬 조건 처리
* likeCount 일괄 조회
* - IN 조건 기반 groupBy 쿼리 1회
* - 부모/자식 댓글을 한 번에 조회
*/
private List<OrderSpecifier<?>> buildOrderSpecifiers(Pageable pageable, QComment comment, QCommentLike commentLike) {
private Map<Long, Long> fetchLikeCounts(List<Long> parentIds, List<CommentListResponse> children) {
QCommentLike commentLike = QCommentLike.commentLike;

List<Long> allIds = new ArrayList<>(parentIds);
allIds.addAll(children.stream().map(CommentListResponse::getCommentId).toList());

if (allIds.isEmpty()) return Map.of();

return queryFactory
.select(commentLike.comment.id, commentLike.count())
.from(commentLike)
.where(commentLike.comment.id.in(allIds))
.groupBy(commentLike.comment.id)
.fetch()
.stream()
.collect(Collectors.toMap(
tuple -> tuple.get(commentLike.comment.id),
tuple -> tuple.get(commentLike.count())
));
}

/**
* 부모/자식 관계 매핑
* - childMap을 parentId 기준으로 그룹화 후 children 필드에 set
*/
private void mapChildrenToParents(List<CommentListResponse> parents, List<CommentListResponse> children) {
if (children.isEmpty()) return;

Map<Long, List<CommentListResponse>> childMap = children.stream()
.collect(Collectors.groupingBy(CommentListResponse::getParentId));

parents.forEach(parent ->
parent.setChildren(childMap.getOrDefault(parent.getCommentId(), List.of()))
);
}

/**
* 정렬 처리 (DB 정렬)
* - createdAt, updatedAt 등 엔티티 필드
*/
private List<OrderSpecifier<?>> buildOrderSpecifiers(Pageable pageable, QComment comment) {
PathBuilder<Comment> entityPath = new PathBuilder<>(Comment.class, comment.getMetadata());
List<OrderSpecifier<?>> orders = new ArrayList<>();

for (Sort.Order order : pageable.getSort()) {
Order direction = order.isAscending() ? Order.ASC : Order.DESC;
String prop = order.getProperty();

switch (prop) {
case "likeCount" -> orders.add(new OrderSpecifier<>(direction, commentLike.id.countDistinct()));
default -> orders.add(new OrderSpecifier<>(direction,
entityPath.getComparable(prop, Comparable.class)));
// 통계 필드는 메모리 정렬에서 처리
if (prop.equals("likeCount")) {
continue;
}
Order direction = order.isAscending() ? Order.ASC : Order.DESC;
orders.add(new OrderSpecifier<>(direction, entityPath.getComparable(prop, Comparable.class)));
}

return orders;
}

/**
* 통계 기반 정렬 처리 (메모리)
* - likeCount 등 통계 필드
* - 페이지 단위라 성능에 영향 없음
*/
private List<CommentListResponse> sortInMemoryIfNeeded(List<CommentListResponse> results, Pageable pageable) {
if (results.isEmpty() || !pageable.getSort().isSorted()) return results;

for (Sort.Order order : pageable.getSort()) {
Comparator<CommentListResponse> comparator = null;

if ("likeCount".equals(order.getProperty())) {
comparator = Comparator.comparing(CommentListResponse::getLikeCount);
}

if (comparator != null) {
if (order.isDescending()) comparator = comparator.reversed();
results.sort(comparator);
}
}

return results;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.back.domain.board.service;

import com.back.domain.board.dto.CommentListResponse;
import com.back.domain.board.dto.CommentRequest;
import com.back.domain.board.dto.CommentResponse;
import com.back.domain.board.dto.PageResponse;
import com.back.domain.board.entity.Comment;
import com.back.domain.board.entity.Post;
import com.back.domain.board.repository.CommentRepository;
import com.back.domain.board.repository.PostRepository;
package com.back.domain.board.comment.service;

import com.back.domain.board.comment.dto.CommentListResponse;
import com.back.domain.board.comment.dto.CommentRequest;
import com.back.domain.board.comment.dto.CommentResponse;
import com.back.domain.board.common.dto.PageResponse;
import com.back.domain.board.comment.entity.Comment;
import com.back.domain.board.post.entity.Post;
import com.back.domain.board.comment.repository.CommentRepository;
import com.back.domain.board.post.repository.PostRepository;
import com.back.domain.user.entity.User;
import com.back.domain.user.repository.UserRepository;
import com.back.global.exception.CustomException;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.back.domain.board.dto;
package com.back.domain.board.common.dto;

import com.back.domain.user.entity.User;
import com.querydsl.core.annotations.QueryProjection;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.back.domain.board.dto;
package com.back.domain.board.common.dto;

import org.springframework.data.domain.Page;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.back.domain.board.controller;
package com.back.domain.board.post.controller;

import com.back.domain.board.dto.*;
import com.back.domain.board.service.PostService;
import com.back.domain.board.common.dto.PageResponse;
import com.back.domain.board.post.dto.PostDetailResponse;
import com.back.domain.board.post.dto.PostListResponse;
import com.back.domain.board.post.dto.PostRequest;
import com.back.domain.board.post.dto.PostResponse;
import com.back.domain.board.post.service.PostService;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CustomUserDetails;
import jakarta.validation.Valid;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.back.domain.board.controller;
package com.back.domain.board.post.controller;

import com.back.domain.board.dto.*;
import com.back.domain.board.common.dto.PageResponse;
import com.back.domain.board.post.dto.PostDetailResponse;
import com.back.domain.board.post.dto.PostListResponse;
import com.back.domain.board.post.dto.PostRequest;
import com.back.domain.board.post.dto.PostResponse;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
Expand Down
Loading