Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8c2e44d
[feat] 댓글 조회 api controller 개발 (#136)
seongjunnoh Aug 7, 2025
31c0c5e
[feat] 댓글 조회 api use case 추가 (#136)
seongjunnoh Aug 7, 2025
f20c5b4
[refactor] 더미 query dto 삭제 (#136)
seongjunnoh Aug 7, 2025
04c0dbc
[feat] 댓글 조회 api 서비스 개발 (#136)
seongjunnoh Aug 7, 2025
28c3b8b
[feat] 댓글 조회용 query dto 추가 (#136)
seongjunnoh Aug 7, 2025
33732a8
[feat] 댓글 조회용 영속성 port 추가 (#136)
seongjunnoh Aug 7, 2025
a7e1f86
[feat] 댓글 조회용 영속성 adapter 추가 (#136)
seongjunnoh Aug 7, 2025
5d8bdab
[feat] 모든 루트 댓글 조회, active 상태인 모든 자식 댓글들 조회용 QueryDSL 코드 구현 (#136)
seongjunnoh Aug 7, 2025
5be7ef0
[feat] 유저가 좋아하는 댓글들 목록 조회용 영속성 port 메서드 추가 (#136)
seongjunnoh Aug 7, 2025
ca6c4bb
[feat] 유저가 좋아하는 댓글들 목록 조회용 jpql 코드 구현 (#136)
seongjunnoh Aug 7, 2025
58189b9
[feat] CommentJpaRepository 에 QueryDSL 조회용 interface 의존성 추가 (#136)
seongjunnoh Aug 7, 2025
0c211cb
[feat] CommentQueryDto -> response 로의 매퍼 추가 (#136)
seongjunnoh Aug 7, 2025
8bec836
[feat] 테스트 jpa entity 생성하는 팩토리 메서드 추가 (#136)
seongjunnoh Aug 7, 2025
db77f03
[test] 댓글 조회 api 통합 테스트 코드 작성 (#136)
seongjunnoh Aug 7, 2025
3b3805d
[feat] 댓글 조회 api 스웨거 operation 추가 (#136)
seongjunnoh Aug 8, 2025
8668ec7
[refactor] 루트 댓글 전체 조회, active 자식 댓글들 전체 조회 QueryDSL 변수 네이밍 수정 (#136)
seongjunnoh Aug 8, 2025
97b55a1
[refactor] 루트 댓글과 연관된 모든 자식 댓글들을 Map 구조로 반환하는 메서드 정의 (#136)
seongjunnoh Aug 8, 2025
fe77395
[feat] 재귀 쿼리를 통해 루트 댓글과 연관된 모든 자식 댓글들을 조회한 후, Map 구조로 반환하는 QueryDSL 코…
seongjunnoh Aug 8, 2025
8f4651d
[refactor] 댓글 조회 서비스 코드 수정 (#136)
seongjunnoh Aug 8, 2025
c4f90f8
[refactor] 댓글 조회 시 PostType 을 추가로 요청받도록 수정 (#136)
seongjunnoh Aug 8, 2025
a4ec647
[refactor] 댓글 조회 api controller 에서 PostType 을 필수 request param 으로 지정 …
seongjunnoh Aug 8, 2025
f0b3057
[refactor] 댓글 조회 api 통합 테스트 코드 수정 (#136)
seongjunnoh Aug 8, 2025
720b0f1
[test] 댓글 조회 api controller 단위 테스트 코드 작성 (#136)
seongjunnoh Aug 8, 2025
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,41 @@
package konkuk.thip.comment.adapter.in.web;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import konkuk.thip.comment.adapter.in.web.response.CommentForSinglePostResponse;
import konkuk.thip.comment.application.port.in.CommentShowAllUseCase;
import konkuk.thip.comment.application.port.in.dto.CommentShowAllQuery;
import konkuk.thip.common.dto.BaseResponse;
import konkuk.thip.common.security.annotation.UserId;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Comment Query API", description = "댓글 조회 관련 API")
@RestController
@RequiredArgsConstructor
public class CommentQueryController {

public final CommentShowAllUseCase commentShowAllUseCase;

@Operation(
summary = "댓글 전체 조회",
description = "특정 게시글(= 피드, 기록, 투표) 의 댓글과 대댓글들을 전체 조회합니다."
)
@GetMapping("/comments/{postId}")
public BaseResponse<CommentForSinglePostResponse> showAllCommentsOfPost(
@Parameter(hidden = true) @UserId final Long userId,
@Parameter(description = "댓글을 조회할 게시글(= FEED, RECORD, VOTE)의 id값")
@PathVariable("postId") final Long postId,
@Parameter(description = "게시물 타입 (RECORD, VOTE, FEED)", example = "RECORD")
@RequestParam(value = "postType") final String postType,
@Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)")
@RequestParam(value = "cursor", required = false) final String cursor) {
return BaseResponse.ok(commentShowAllUseCase.showAllCommentsOfPost(
CommentShowAllQuery.of(postId, userId, postType, cursor)
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package konkuk.thip.comment.adapter.in.web.response;

import java.util.List;

public record CommentForSinglePostResponse(
List<RootCommentDto> commentList,
String nextCursor,
boolean isLast
) {
public record RootCommentDto(
Long commentId,
Long creatorId,
String creatorProfileImageUrl,
String creatorNickname,
String alias,
String aliasColor,
String postDate, // 댓글 작성 시각 (~ 전 형식)
String content,
int likeCount,
boolean isLike,
boolean isDeleted, // 삭제된 댓글인지 아닌지
List<ReplyDto> replyList
) {
public record ReplyDto(
Long commentId,
String parentCommentCreatorNickname,
Long creatorId,
String creatorProfileImageUrl,
String creatorNickname,
String alias,
String aliasColor,
String postDate, // 댓글 작성 시각 (~ 전 형식)
String content,
int likeCount,
boolean isLike
) {}

/**
* 삭제된 루트 댓글에 매핑되는 response dto
* isDelete 제외 나머지 데이터는 모두 쓰레기 값으로
*/
public static RootCommentDto createDeletedRootCommentDto(List<ReplyDto> replyList) {
return new RootCommentDto(
null,
null,
null,
null,
null,
null,
null,
null,
0,
false,
true, // true
replyList);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.Set;

@Repository
@RequiredArgsConstructor
public class CommentLikeQueryPersistenceAdapter implements CommentLikeQueryPort {
Expand All @@ -21,4 +23,9 @@ public class CommentLikeQueryPersistenceAdapter implements CommentLikeQueryPort
public boolean isLikedCommentByUser(Long userId, Long commentId) {
return commentLikeJpaRepository.existsByUserIdAndCommentId(userId, commentId);
}

@Override
public Set<Long> findCommentIdsLikedByUser(Set<Long> commentIds, Long userId) {
return commentLikeJpaRepository.findCommentIdsLikedByUser(commentIds, userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,44 @@
import konkuk.thip.comment.adapter.out.mapper.CommentMapper;
import konkuk.thip.comment.adapter.out.persistence.repository.CommentJpaRepository;
import konkuk.thip.comment.application.port.out.CommentQueryPort;
import konkuk.thip.comment.application.port.out.dto.CommentQueryDto;
import konkuk.thip.common.util.Cursor;
import konkuk.thip.common.util.CursorBasedList;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;

@Repository
@RequiredArgsConstructor
public class CommentQueryPersistenceAdapter implements CommentQueryPort {

private final CommentJpaRepository jpaRepository;
private final CommentMapper userMapper;
private final CommentJpaRepository commentJpaRepository;
private final CommentMapper commentMapper;

@Override
public CursorBasedList<CommentQueryDto> findLatestRootCommentsWithDeleted(Long postId, String postTypeStr, Cursor cursor) {
LocalDateTime lastCreatedAt = cursor.isFirstRequest() ? null : cursor.getLocalDateTime(0);
int size = cursor.getPageSize();

List<CommentQueryDto> commentQueryDtos = commentJpaRepository.findRootCommentsWithDeletedByCreatedAtDesc(postId, postTypeStr, lastCreatedAt, size);

return CursorBasedList.of(commentQueryDtos, size, commentQueryDto -> {
Cursor nextCursor = new Cursor(List.of(commentQueryDto.createdAt().toString()));
return nextCursor.toEncodedString();
});
}

@Override
public List<CommentQueryDto> findAllActiveChildCommentsOldestFirst(Long rootCommentId) {
return commentJpaRepository.findAllActiveChildCommentsByCreatedAtAsc(rootCommentId);
}

@Override
public Map<Long, List<CommentQueryDto>> findAllActiveChildCommentsOldestFirst(Set<Long> rootCommentIds) {
return commentJpaRepository.findAllActiveChildCommentsByCreatedAtAsc(rootCommentIds);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@

import java.util.Optional;

public interface CommentJpaRepository extends JpaRepository<CommentJpaEntity, Long> {
public interface CommentJpaRepository extends JpaRepository<CommentJpaEntity, Long>, CommentQueryRepository {
Optional<CommentJpaEntity> findByCommentIdAndStatus(Long commentId, StatusType status);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Set;

public interface CommentLikeJpaRepository extends JpaRepository<CommentLikeJpaEntity, Long> {

Expand All @@ -27,4 +28,6 @@ public interface CommentLikeJpaRepository extends JpaRepository<CommentLikeJpaEn
@Query("DELETE FROM CommentLikeJpaEntity cl WHERE cl.commentJpaEntity.commentId = :commentId")
void deleteAllByCommentId(@Param("commentId") Long commentId);

}
@Query("SELECT c.commentJpaEntity.commentId FROM CommentLikeJpaEntity c WHERE c.userJpaEntity.userId = :userId AND c.commentJpaEntity.commentId IN :commentIds")
Set<Long> findCommentIdsLikedByUser(@Param("commentIds") Set<Long> commentIds, @Param("userId") Long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package konkuk.thip.comment.adapter.out.persistence.repository;

import konkuk.thip.comment.application.port.out.dto.CommentQueryDto;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;

public interface CommentQueryRepository {

List<CommentQueryDto> findRootCommentsWithDeletedByCreatedAtDesc(Long postId, String postTypeStr, LocalDateTime lastCreatedAt, int size);

List<CommentQueryDto> findAllActiveChildCommentsByCreatedAtAsc(Long rootCommentId);

Map<Long, List<CommentQueryDto>> findAllActiveChildCommentsByCreatedAtAsc(Set<Long> rootCommentIds);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package konkuk.thip.comment.adapter.out.persistence.repository;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import konkuk.thip.comment.adapter.out.jpa.QCommentJpaEntity;
import konkuk.thip.comment.application.port.out.dto.CommentQueryDto;
import konkuk.thip.comment.application.port.out.dto.QCommentQueryDto;
import konkuk.thip.common.entity.StatusType;
import konkuk.thip.user.adapter.out.jpa.QAliasJpaEntity;
import konkuk.thip.user.adapter.out.jpa.QUserJpaEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

@Repository
@RequiredArgsConstructor
public class CommentQueryRepositoryImpl implements CommentQueryRepository {

private final JPAQueryFactory queryFactory;

private final QCommentJpaEntity comment = QCommentJpaEntity.commentJpaEntity;
private final QUserJpaEntity commentCreator = QUserJpaEntity.userJpaEntity;
private final QAliasJpaEntity aliasOfCommentCreator = QAliasJpaEntity.aliasJpaEntity;
private final QCommentJpaEntity parentComment = new QCommentJpaEntity("parentComment");
private final QUserJpaEntity parentCommentCreator = new QUserJpaEntity("parentCommentCreator");

@Override
public List<CommentQueryDto> findRootCommentsWithDeletedByCreatedAtDesc(Long postId, String postTypeStr, LocalDateTime lastCreatedAt, int size) {
// 최상위 댓글(size+1) 프로젝션 생성
QCommentQueryDto proj = new QCommentQueryDto(
comment.commentId,
commentCreator.userId,
aliasOfCommentCreator.imageUrl,
commentCreator.nickname,
aliasOfCommentCreator.value,
aliasOfCommentCreator.color,
comment.createdAt,
comment.content,
comment.likeCount,
comment.status.eq(StatusType.INACTIVE) // 루트 댓글이 삭제된 상태인지 아닌지
);

// WHERE 절 분리
BooleanExpression whereClause = comment.postJpaEntity.postId.eq(postId)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위에말한것처럼 여기에 postType에 대한 조건도 추가되어야할것같습니다! comment.postType.eq(searchPostType) 이런식으루..

.and(comment.postJpaEntity.dtype.eq(postTypeStr)) // dType 필터링 추가
Copy link
Copy Markdown
Member

@hd0rable hd0rable Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어근데 저희 post는 dtype으로 문자열이고 댓글의 postType은 enum인데 이거 나중에 통일해야할것같네욥 수정하면서 PostType.from()으로 파라미터 검증도 추가하면 좋을것같습니다 ! 일단은 이렇게 해도 좋을것같네여

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

파라미터 검증은 QueryDto 내부에서 수행하고 있긴 합니다!
희진님 말대로 현재 dType 이 String 이어서 일단 서비스에서 enum의 String value 를 파싱해서 영속성 계층까지 던지는 식으로 구현하였습니다

.and(comment.parent.isNull()) // 게시글의 최상위 댓글 조회
.and(lastCreatedAt != null // 최신순 정렬
? comment.createdAt.lt(lastCreatedAt)
: Expressions.TRUE
);

Comment on lines +32 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

postTypeStr 문자열 비교는 Enum으로 교체하는 편이 안전합니다

comment.postJpaEntity.dtype.eq(postTypeStr)
comment.postJpaEntity.dtype.eq(postType.getDtype()) 형태로 변경하면 오타·케이스 민감성 문제를 제거할 수 있습니다.

🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java
around lines 32 to 55, replace the string comparison of postTypeStr with an
Enum-based comparison to avoid typos and case sensitivity issues. Change the
condition comment.postJpaEntity.dtype.eq(postTypeStr) to use
comment.postJpaEntity.dtype.eq(postType.getDtype()) where postType is an Enum
representing the post type. This requires passing or converting postTypeStr to
the corresponding Enum before the query.

// 조회 및 반환
return queryFactory
.select(proj)
.from(comment)
.leftJoin(comment.userJpaEntity, commentCreator)
.leftJoin(commentCreator.aliasForUserJpaEntity, aliasOfCommentCreator)
.where(whereClause)
.orderBy(comment.createdAt.desc())
.limit(size + 1) // size + 1 개 조회
.fetch();
}
Comment on lines +48 to +66
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

커서 정렬 컬럼이 하나뿐이라 페이지 중복/누락 위험이 있습니다

orderBy(comment.createdAt.desc())createdAt < :lastCreatedAt 조건만 사용하면
같은 createdAt 값을 가진 댓글이 다수일 때 다음 페이지에서 중복이 발생하거나 일부가 건너뛰어질 수 있습니다.

createdAt 동 tie-breaker로 comment.commentId를 함께 사용해 안정적인 순서를 보장하세요.

.where(whereClause)
-.orderBy(comment.createdAt.desc())
+.orderBy(comment.createdAt.desc(), comment.commentId.desc())

커서 로직도 (createdAt, commentId) 복합 비교로 바꿔야 합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
BooleanExpression whereClause = comment.postJpaEntity.postId.eq(postId)
.and(comment.postJpaEntity.dtype.eq(postTypeStr)) // dType 필터링 추가
.and(comment.parent.isNull()) // 게시글의 최상위 댓글 조회
.and(lastCreatedAt != null // 최신순 정렬
? comment.createdAt.lt(lastCreatedAt)
: Expressions.TRUE
);
// 조회 및 반환
return queryFactory
.select(proj)
.from(comment)
.leftJoin(comment.userJpaEntity, commentCreator)
.leftJoin(commentCreator.aliasForUserJpaEntity, aliasOfCommentCreator)
.where(whereClause)
.orderBy(comment.createdAt.desc())
.limit(size + 1) // size + 1 개 조회
.fetch();
}
BooleanExpression whereClause = comment.postJpaEntity.postId.eq(postId)
.and(comment.postJpaEntity.dtype.eq(postTypeStr)) // dType 필터링 추가
.and(comment.parent.isNull()) // 게시글의 최상위 댓글 조회
.and(lastCreatedAt != null // 최신순 정렬
? comment.createdAt.lt(lastCreatedAt)
: Expressions.TRUE
);
// 조회 및 반환
return queryFactory
.select(proj)
.from(comment)
.leftJoin(comment.userJpaEntity, commentCreator)
.leftJoin(commentCreator.aliasForUserJpaEntity, aliasOfCommentCreator)
.where(whereClause)
- .orderBy(comment.createdAt.desc())
+ .orderBy(comment.createdAt.desc(), comment.commentId.desc())
.limit(size + 1) // size + 1 개 조회
.fetch();
}
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java
between lines 48 and 66, the current cursor pagination uses only createdAt for
ordering and filtering, which can cause duplicates or missing entries when
multiple comments share the same createdAt timestamp. To fix this, modify the
orderBy clause to order by createdAt descending and then commentId descending as
a tie-breaker. Also, update the whereClause to implement a composite cursor
condition that compares (createdAt, commentId) pairs, ensuring stable and
consistent pagination results.


@Override
public List<CommentQueryDto> findAllActiveChildCommentsByCreatedAtAsc(Long rootCommentId) {
List<CommentQueryDto> allDescendants = new ArrayList<>(); // 결과 누적용 리스트

// 1) 부모 ID 집합에 루트 댓글 ID 추가
Set<Long> parentIds = new HashSet<>();
parentIds.add(rootCommentId);

// 2) 자손 댓글용 프로젝션 (부모 댓글 ID·작성자 닉네임 포함)
QCommentQueryDto childProj = new QCommentQueryDto(
comment.commentId,
comment.parent.commentId,
parentCommentCreator.nickname,
commentCreator.userId,
aliasOfCommentCreator.imageUrl,
commentCreator.nickname,
aliasOfCommentCreator.value,
aliasOfCommentCreator.color,
comment.createdAt,
comment.content,
comment.likeCount,
comment.status.eq(StatusType.INACTIVE)
);

// 3) 단계별 자식 댓글 조회
while (!parentIds.isEmpty()) {
List<CommentQueryDto> children = queryFactory
.select(childProj)
.from(comment)
.leftJoin(comment.parent, parentComment)
.leftJoin(parentComment.userJpaEntity, parentCommentCreator)
.leftJoin(comment.userJpaEntity, commentCreator)
.leftJoin(commentCreator.aliasForUserJpaEntity, aliasOfCommentCreator)
.where(
comment.parent.commentId.in(parentIds), // parentIds 하위의 모든 자식 댓글 조회
comment.status.eq(StatusType.ACTIVE) // 자식 댓글은 ACTIVE인 것만 조회
)
.fetch();

if (children.isEmpty()) break;

// 4) 누적 및 다음 단계 부모 ID 집합 갱신
allDescendants.addAll(children);
parentIds = children.stream()
.map(CommentQueryDto::commentId)
.collect(Collectors.toSet());
}

// 5) 전체 자손 댓글을 깊이와 상관없이 작성 순으로 재정렬
allDescendants.sort(Comparator.comparing(CommentQueryDto::createdAt));
return allDescendants;
Comment on lines +92 to +118
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

자식 댓글 BFS 반복 쿼리 — 스키마 여건에 따라 단일 쿼리로 최적화 가능

현재는 깊이마다 쿼리를 반복(BFS)합니다. 최대 깊이에 비례해 쿼리 수가 증가합니다.

스키마에 comment.rootCommentId(루트 참조)가 존재한다면, 다음과 같이 단일 쿼리로 대체해 성능을 크게 개선할 수 있습니다.

  • where: comment.rootCommentId.eq(:rootId) AND comment.status = ACTIVE
  • orderBy: createdAt asc, commentId asc
  • parent 작성자 닉네임은 self join 한 번으로 해결

스키마에 해당 컬럼이 없다면 현재 방식이 합리적입니다. 대안으로는 DB가 지원한다면 재귀 CTE(WITH RECURSIVE) 사용이 있으나, JPA/QueryDSL 표준에서의 이식성은 떨어집니다. 필요 시 JPA NativeQuery로 한 번에 가져오는 구현도 제안 가능합니다.

🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java
around lines 91 to 117, the current implementation fetches child comments using
a BFS loop with repeated queries per depth level, which can degrade performance
as depth increases. If the schema includes a rootCommentId field referencing the
root comment, refactor this to a single query filtering by comment.rootCommentId
and status ACTIVE, ordering by createdAt and commentId ascending. Use a single
self join to fetch parent author nicknames. If rootCommentId does not exist,
keep the current approach or consider a recursive CTE or native query for
optimization.

}

@Override
public Map<Long, List<CommentQueryDto>> findAllActiveChildCommentsByCreatedAtAsc(Set<Long> rootCommentIds) {
// 1) 루트 ID별로 최상위 매핑 초기화
Map<Long, Long> idToRoot = new HashMap<>();
for (Long rootId : rootCommentIds) {
idToRoot.put(rootId, rootId); // 초기화
}

// 2) 결과 맵 초기화
Map<Long, List<CommentQueryDto>> resultMap = new HashMap<>();
for (Long rootId : rootCommentIds) {
resultMap.put(rootId, new ArrayList<>());
}

// 3) 단계별 조회용 parentIds 초기화
Set<Long> parentIds = new HashSet<>(rootCommentIds);

// 4) 자손 댓글용 프로젝션 정의
QCommentQueryDto childProj = new QCommentQueryDto(
comment.commentId,
comment.parent.commentId,
parentCommentCreator.nickname,
commentCreator.userId,
aliasOfCommentCreator.imageUrl,
commentCreator.nickname,
aliasOfCommentCreator.value,
aliasOfCommentCreator.color,
comment.createdAt,
comment.content,
comment.likeCount,
comment.status.eq(StatusType.INACTIVE)
);

// 5) 루프를 돌며 모든 깊이의 자식 댓글 조회 및 매핑
while (!parentIds.isEmpty()) {
List<CommentQueryDto> children = queryFactory
.select(childProj)
.from(comment)
.leftJoin(comment.parent, parentComment)
.leftJoin(parentComment.userJpaEntity, parentCommentCreator)
.leftJoin(comment.userJpaEntity, commentCreator)
.leftJoin(commentCreator.aliasForUserJpaEntity, aliasOfCommentCreator)
.where(
comment.parent.commentId.in(parentIds), // parentIds 하위의 모든 자식 댓글 조회
comment.status.eq(StatusType.ACTIVE) // 자식 댓글은 ACTIVE인 것만 조회
)
.fetch();

if (children.isEmpty()) break;

Set<Long> nextParentIds = new HashSet<>();
for (CommentQueryDto child : children) { // 조회한 자식 댓글들에 대하여
Long rootId = idToRoot.get(child.parentCommentId()); // 현재 자식댓글의 루트 댓글(부모 아님, 루트임)

resultMap.get(rootId).add(child); // 해당 루트 ID의 리스트에 자식 댓글 추가

// 현재 자식 댓글도 다음 단계의 parentIds로 사용하기 위해 매핑 저장
idToRoot.put(child.commentId(), rootId);
nextParentIds.add(child.commentId());
}
parentIds = nextParentIds; // 한단계 아래 계층에서 활용할 부모 댓글들
}

// 6) 각 루트별 value 리스트를 작성시간순으로 정렬
resultMap.values().forEach(list -> list.sort(Comparator.comparing(CommentQueryDto::createdAt)));

return resultMap;
}
Comment on lines +121 to +188
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 굉장히 복잡하네요.. 우선 쿼리 횟수는 줄었으니 현 상황에서는 최선인 것 같습니다! 추후에 카톡에서 이야기 한 것처럼 Comment의 parentComment 대신 rootComment의 fk를 갖도록 하여 부모-자식 관계 => 루트-하위 관계로 풀어나가면 Querydsl의 로직이 조금 간단해질 것 같습니다!

저희가 초기에 Comment를 트리 구조로 잡은 이유가 '언급'이라는 기능에 따른 요구사항 때문이였는데, 이것도 다시보니 Comment에 'mentionUser'라는 User 테이블의 fk를 하나 가지고 있으면 해결 될 것 같네요! 추후 리팩토링에서 고려해봅시다!!
정말 수고하셨습니다~~!!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵넵 좋습니다! 현재 답글을 보여주는 로직이 자신의 부모 댓글 자체 보다는 부모 댓글을 작성한 작성자와 자식 댓글의 최상위 루트 댓글이 중요하니 엔티티 구조를 수정하는 것도 좋은 것 같습니다!
이러면 query dsl 에서 복잡한 로직을 수행하지 않아도 쿼리 호출 횟수를 줄일수 있을 것 같네요!

}
Loading