Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,6 +1,6 @@
package com.back.domain.board.comment.dto;

import com.back.domain.board.comment.entity.Comment;
import com.querydsl.core.annotations.QueryProjection;

import java.time.LocalDateTime;

Expand Down Expand Up @@ -28,28 +28,6 @@ public record MyCommentResponse(
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
public static MyCommentResponse from(Comment comment) {
return new MyCommentResponse(
comment.getId(),
comment.getPost().getId(),
comment.getPost().getTitle(),
comment.getParent() != null
? comment.getParent().getId()
: null,
comment.getParent() != null
? truncate(comment.getParent().getContent())
: null,
comment.getContent(),
comment.getLikeCount(),
comment.getCreatedAt(),
comment.getUpdatedAt()
);
}

private static String truncate(String content) {
int length = 50;
return (content == null || content.length() <= length)
? content
: content.substring(0, length) + "...";
}
@QueryProjection
public MyCommentResponse {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@

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

@Repository
public interface CommentRepository extends JpaRepository<Comment, Long>, CommentRepositoryCustom {
Page<Comment> findAllByUserId(Long userId, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.back.domain.board.comment.repository.custom;

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

public interface CommentRepositoryCustom {
Page<CommentListResponse> getCommentsByPostId(Long postId, Pageable pageable);
Page<CommentListResponse> findCommentsByPostId(Long postId, Pageable pageable);
Page<MyCommentResponse> findCommentsByUserId(Long postId, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package com.back.domain.board.comment.repository.custom;

import com.back.domain.board.comment.dto.CommentListResponse;
import com.back.domain.board.comment.dto.MyCommentResponse;
import com.back.domain.board.comment.dto.QCommentListResponse;
import com.back.domain.board.comment.dto.QMyCommentResponse;
import com.back.domain.board.comment.entity.Comment;
import com.back.domain.board.comment.entity.QComment;
import com.back.domain.board.common.dto.QAuthorResponse;
import com.back.domain.board.post.entity.QPost;
import com.back.domain.user.entity.QUser;
import com.back.domain.user.entity.QUserProfile;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.BooleanExpression;
Expand All @@ -30,23 +34,26 @@ public class CommentRepositoryImpl implements CommentRepositoryCustom {
/**
* 특정 게시글의 댓글 목록 조회
* - 총 쿼리 수: 3회
* 1.부모 댓글 목록을 페이징/정렬 조건으로 조회
* 2.부모 ID 목록으로 자식 댓글 전체 조회
* 3.부모 총 건수(count) 조회
* 1.부모 댓글 목록 조회 (User, UserProfile join)
* 2.자식 댓글 목록 조회 (User, UserProfile join)
* 3.부모 전체 count 조회
*
* @param postId 게시글 Id
* @param postId 게시글 ID
* @param pageable 페이징 + 정렬 조건
*/
@Override
public Page<CommentListResponse> getCommentsByPostId(Long postId, Pageable pageable) {
public Page<CommentListResponse> findCommentsByPostId(Long postId, Pageable pageable) {
QComment comment = QComment.comment;

// 1. 정렬 조건 생성
// 1. 검색 조건 생성
BooleanExpression condition = comment.post.id.eq(postId).and(comment.parent.isNull());

// 2. 정렬 조건 생성
List<OrderSpecifier<?>> orders = buildOrderSpecifiers(pageable);

// 2. 부모 댓글 조회 (페이징 적용)
// 3. 부모 댓글 조회 (페이징 적용)
List<CommentListResponse> parents = fetchComments(
comment.post.id.eq(postId).and(comment.parent.isNull()),
condition,
orders,
pageable.getOffset(),
pageable.getPageSize()
Expand All @@ -57,42 +64,117 @@ public Page<CommentListResponse> getCommentsByPostId(Long postId, Pageable pagea
return new PageImpl<>(parents, pageable, 0);
}

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

// 4. 자식 댓글 조회 (부모 집합에 대한 전체 조회)
// 5. 자식 댓글 조회 (부모 집합에 대한 전체 조회)
List<CommentListResponse> children = fetchComments(
comment.parent.id.in(parentIds),
List.of(comment.createdAt.asc()), // 시간순 정렬
null,
null
);

// 5. 부모-자식 매핑
// 6. 부모-자식 매핑
mapChildrenToParents(parents, children);

// 6. 전체 부모 댓글 수 조회
Long total = queryFactory
.select(comment.count())
// 7. 전체 부모 댓글 수 조회
long total = countComments(condition);

return new PageImpl<>(parents, pageable, total);
}

/**
* 특정 사용자의 댓글 목록 조회
* - 총 쿼리 수: 2회
* 1. 댓글 목록 조회 (Comment, Post join)
* 2. 전체 count 조회
*
* @param userId 사용자 ID
* @param pageable 페이징 + 정렬 조건
*/
@Override
public Page<MyCommentResponse> findCommentsByUserId(Long userId, Pageable pageable) {
QComment comment = QComment.comment;
QComment parent = new QComment("parent");
QPost post = QPost.post;

// 1. 검색 조건 생성
BooleanExpression condition = comment.user.id.eq(userId);

// 2. 정렬 조건 생성
List<OrderSpecifier<?>> orders = buildOrderSpecifiers(pageable);

// 3. 댓글 목록 조회
List<MyCommentResponse> comments = queryFactory
.select(new QMyCommentResponse(
comment.id,
post.id,
post.title,
parent.id,
parent.content.substring(0, 50),
comment.content,
comment.likeCount,
comment.createdAt,
comment.updatedAt
))
.from(comment)
.where(comment.post.id.eq(postId).and(comment.parent.isNull()))
.fetchOne();
.leftJoin(comment.parent, parent)
.leftJoin(comment.post, post)
.where(condition)
.orderBy(orders.toArray(new OrderSpecifier[0]))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();

// 결과가 없으면 즉시 빈 페이지 반환
if (comments.isEmpty()) {
return new PageImpl<>(comments, pageable, 0);
}

// 4. 전체 댓글 수 조회
long total = countComments(condition);

return new PageImpl<>(parents, pageable, total != null ? total : 0L);
return new PageImpl<>(comments, pageable, total);
}

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

/**
* 정렬 조건 생성
* - Pageable의 Sort 정보를 QueryDSL OrderSpecifier 목록으로 변환
*/
private List<OrderSpecifier<?>> buildOrderSpecifiers(Pageable pageable) {
QComment comment = QComment.comment;
PathBuilder<Comment> entityPath = new PathBuilder<>(Comment.class, comment.getMetadata());
List<OrderSpecifier<?>> orders = new ArrayList<>();

for (Sort.Order order : pageable.getSort()) {
String property = order.getProperty();

// 화이트리스트에 포함된 필드만 허용
if (!ALLOWED_SORT_FIELDS.contains(property)) {
// 허용되지 않은 정렬 키는 무시 (런타임 예외 대신 안전하게 스킵)
continue;
}

Order direction = order.isAscending() ? Order.ASC : Order.DESC;
orders.add(new OrderSpecifier<>(direction, entityPath.getComparable(property, Comparable.class)));
}

// 명시된 정렬이 없으면 기본 정렬(createdAt DESC) 적용
if (orders.isEmpty()) {
orders.add(new OrderSpecifier<>(Order.DESC, comment.createdAt));
}

return orders;
}

/**
* 댓글 조회
* - User / UserProfile join (N+1 방지)
*
* @param condition where 조건
* @param orders 정렬 조건
* @param offset 페이징 offset (null이면 미적용)
* @param limit 페이징 limit (null이면 미적용)
*/
private List<CommentListResponse> fetchComments(
BooleanExpression condition,
Expand Down Expand Up @@ -147,32 +229,16 @@ private void mapChildrenToParents(List<CommentListResponse> parents, List<Commen
}

/**
* 정렬 조건 생성
* - Pageable의 Sort 정보를 QueryDSL OrderSpecifier 목록으로 변환
* 전체 댓글 개수 조회
* - 단순 count 쿼리 1회
*/
private List<OrderSpecifier<?>> buildOrderSpecifiers(Pageable pageable) {
private long countComments(BooleanExpression condition) {
QComment comment = QComment.comment;
PathBuilder<Comment> entityPath = new PathBuilder<>(Comment.class, comment.getMetadata());
List<OrderSpecifier<?>> orders = new ArrayList<>();

for (Sort.Order order : pageable.getSort()) {
String property = order.getProperty();

// 화이트리스트에 포함된 필드만 허용
if (!ALLOWED_SORT_FIELDS.contains(property)) {
// 허용되지 않은 정렬 키는 무시 (런타임 예외 대신 안전하게 스킵)
continue;
}

Order direction = order.isAscending() ? Order.ASC : Order.DESC;
orders.add(new OrderSpecifier<>(direction, entityPath.getComparable(property, Comparable.class)));
}

// 명시된 정렬이 없으면 기본 정렬(createdAt DESC) 적용
if (orders.isEmpty()) {
orders.add(new OrderSpecifier<>(Order.DESC, comment.createdAt));
}

return orders;
Long total = queryFactory
.select(comment.count())
.from(comment)
.where(condition)
.fetchOne();
return total != null ? total : 0L;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.back.domain.board.comment.dto.CommentRequest;
import com.back.domain.board.comment.dto.CommentResponse;
import com.back.domain.board.comment.dto.ReplyResponse;
import com.back.domain.board.comment.entity.CommentLike;
import com.back.domain.board.comment.repository.CommentLikeRepository;
import com.back.domain.board.common.dto.PageResponse;
import com.back.domain.board.comment.entity.Comment;
Expand Down Expand Up @@ -91,7 +90,7 @@ public PageResponse<CommentListResponse> getComments(Long postId, Pageable pagea
.orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND));

// 댓글 목록 조회
Page<CommentListResponse> comments = commentRepository.getCommentsByPostId(postId, pageable);
Page<CommentListResponse> comments = commentRepository.findCommentsByPostId(postId, pageable);

return PageResponse.from(comments);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@
/**
* 게시글 상세 응답 DTO
*
* @param postId 게시글 ID
* @param author 작성자 정보
* @param title 게시글 제목
* @param content 게시글 내용
* @param thumbnailUrl 썸네일 URL
* @param categories 게시글 카테고리 목록
* @param likeCount 좋아요 수
* @param bookmarkCount 북마크 수
* @param commentCount 댓글 수
* @param createdAt 게시글 생성 일시
* @param updatedAt 게시글 수정 일시
* @param postId 게시글 ID
* @param author 작성자 정보
* @param title 게시글 제목
* @param content 게시글 내용
* @param thumbnailUrl 썸네일 URL
* @param categories 게시글 카테고리 목록
* @param likeCount 좋아요 수
* @param bookmarkCount 북마크 수
* @param commentCount 댓글 수
* @param likedByMe 좋아요 여부
* @param bookmarkedByMe 북마크 여부
* @param createdAt 게시글 생성 일시
* @param updatedAt 게시글 수정 일시
*/
public record PostDetailResponse(
Long postId,
Expand Down
18 changes: 0 additions & 18 deletions src/main/java/com/back/domain/board/post/dto/PostListResponse.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.back.domain.board.post.dto;

import com.back.domain.board.common.dto.AuthorResponse;
import com.back.domain.board.post.entity.Post;
import com.querydsl.core.annotations.QueryProjection;
import lombok.Getter;
import lombok.Setter;
Expand Down Expand Up @@ -50,21 +49,4 @@ public PostListResponse(Long postId,
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}

public static PostListResponse from(Post post) {
return new PostListResponse(
post.getId(),
AuthorResponse.from(post.getUser()),
post.getTitle(),
post.getThumbnailUrl(),
post.getCategories().stream()
.map(CategoryResponse::from)
.toList(),
post.getLikeCount(),
post.getBookmarkCount(),
post.getCommentCount(),
post.getCreatedAt(),
post.getUpdatedAt()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@

import com.back.domain.board.post.entity.Post;
import com.back.domain.board.post.repository.custom.PostRepositoryCustom;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PostRepository extends JpaRepository<Post, Long>, PostRepositoryCustom {
Page<Post> findAllByUserId(Long userId, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@

public interface PostRepositoryCustom {
Page<PostListResponse> searchPosts(String keyword, String searchType, List<Long> categoryIds, Pageable pageable);
Page<PostListResponse> findPostsByUserId(Long userId, Pageable pageable);
Page<PostListResponse> findBookmarkedPostsByUserId(Long userId, Pageable pageable);
}
Loading