From 8c2e44dc4eb63e2de0973e148f5b7b552ebd307d Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 7 Aug 2025 20:16:18 +0900 Subject: [PATCH 01/23] =?UTF-8?q?[feat]=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20api=20controller=20=EA=B0=9C=EB=B0=9C=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/CommentQueryController.java | 24 ++++++++ .../CommentForSinglePostResponse.java | 58 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 src/main/java/konkuk/thip/comment/adapter/in/web/response/CommentForSinglePostResponse.java diff --git a/src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java b/src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java index 00cc4c052..d32b1248e 100644 --- a/src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java +++ b/src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java @@ -1,10 +1,34 @@ package konkuk.thip.comment.adapter.in.web; +import io.swagger.v3.oas.annotations.Parameter; +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; @RestController @RequiredArgsConstructor public class CommentQueryController { + public final CommentShowAllUseCase commentShowAllUseCase; + + @GetMapping("/comments/{postId}") + public BaseResponse 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", required = false) 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) + )); + } } diff --git a/src/main/java/konkuk/thip/comment/adapter/in/web/response/CommentForSinglePostResponse.java b/src/main/java/konkuk/thip/comment/adapter/in/web/response/CommentForSinglePostResponse.java new file mode 100644 index 000000000..56ad5678a --- /dev/null +++ b/src/main/java/konkuk/thip/comment/adapter/in/web/response/CommentForSinglePostResponse.java @@ -0,0 +1,58 @@ +package konkuk.thip.comment.adapter.in.web.response; + +import java.util.List; + +public record CommentForSinglePostResponse( + List 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 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 replyList) { + return new RootCommentDto( + null, + null, + null, + null, + null, + null, + null, + null, + 0, + false, + true, // true + replyList); + } + } +} From 31c0c5e4152a19209ab7d291b4399e28a784c31c Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 7 Aug 2025 20:17:55 +0900 Subject: [PATCH 02/23] =?UTF-8?q?[feat]=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20api=20use=20case=20=EC=B6=94=EA=B0=80=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/port/in/CommentShowAllUseCase.java | 9 +++++++++ .../port/in/dto/CommentShowAllQuery.java | 13 +++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/main/java/konkuk/thip/comment/application/port/in/CommentShowAllUseCase.java create mode 100644 src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java diff --git a/src/main/java/konkuk/thip/comment/application/port/in/CommentShowAllUseCase.java b/src/main/java/konkuk/thip/comment/application/port/in/CommentShowAllUseCase.java new file mode 100644 index 000000000..f9d3bd21e --- /dev/null +++ b/src/main/java/konkuk/thip/comment/application/port/in/CommentShowAllUseCase.java @@ -0,0 +1,9 @@ +package konkuk.thip.comment.application.port.in; + +import konkuk.thip.comment.adapter.in.web.response.CommentForSinglePostResponse; +import konkuk.thip.comment.application.port.in.dto.CommentShowAllQuery; + +public interface CommentShowAllUseCase { + + CommentForSinglePostResponse showAllCommentsOfPost(CommentShowAllQuery query); +} diff --git a/src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java b/src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java new file mode 100644 index 000000000..57b75cb3a --- /dev/null +++ b/src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java @@ -0,0 +1,13 @@ +package konkuk.thip.comment.application.port.in.dto; + + +public record CommentShowAllQuery( + Long postId, + Long userId, + String postType, + String cursorStr +) { + public static CommentShowAllQuery of(Long postId, Long userId, String postType, String cursorStr) { + return new CommentShowAllQuery(postId, userId, postType, cursorStr); + } +} From f20c5b43fae1b17968d477486d4b0a5947cd261a Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 7 Aug 2025 20:18:13 +0900 Subject: [PATCH 03/23] =?UTF-8?q?[refactor]=20=EB=8D=94=EB=AF=B8=20query?= =?UTF-8?q?=20dto=20=EC=82=AD=EC=A0=9C=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/comment/application/port/in/dto/DummyQuery.java | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/main/java/konkuk/thip/comment/application/port/in/dto/DummyQuery.java diff --git a/src/main/java/konkuk/thip/comment/application/port/in/dto/DummyQuery.java b/src/main/java/konkuk/thip/comment/application/port/in/dto/DummyQuery.java deleted file mode 100644 index 4a80da494..000000000 --- a/src/main/java/konkuk/thip/comment/application/port/in/dto/DummyQuery.java +++ /dev/null @@ -1,9 +0,0 @@ -package konkuk.thip.comment.application.port.in.dto; - -import lombok.Builder; -import lombok.Getter; - -@Builder -@Getter -public class DummyQuery { -} From 04c0dbc39dd2fa2f63d6171c349aadb3f0be8eaa Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 7 Aug 2025 20:18:30 +0900 Subject: [PATCH 04/23] =?UTF-8?q?[feat]=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20api=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CommentShowAllService.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java diff --git a/src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java b/src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java new file mode 100644 index 000000000..d374627c0 --- /dev/null +++ b/src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java @@ -0,0 +1,76 @@ +package konkuk.thip.comment.application.service; + +import konkuk.thip.comment.adapter.in.web.response.CommentForSinglePostResponse; +import konkuk.thip.comment.application.mapper.CommentQueryMapper; +import konkuk.thip.comment.application.port.in.CommentShowAllUseCase; +import konkuk.thip.comment.application.port.in.dto.CommentShowAllQuery; +import konkuk.thip.comment.application.port.out.CommentLikeQueryPort; +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.Service; + +import java.util.*; + +@Service +@RequiredArgsConstructor +public class CommentShowAllService implements CommentShowAllUseCase { + + private static final int PAGE_SIZE = 10; + private final CommentQueryPort commentQueryPort; + private final CommentLikeQueryPort commentLikeQueryPort; + private final CommentQueryMapper commentQueryMapper; + + @Override + public CommentForSinglePostResponse showAllCommentsOfPost(CommentShowAllQuery query) { + Cursor cursor = Cursor.from(query.cursorStr(), PAGE_SIZE); + + // 1. size 크기만큼의 루트 댓글 최신순 조회 -> 삭제된 루트 댓글 포함해서 전부 조회 + CursorBasedList commentQueryDtoCursorBasedList = commentQueryPort.findLatestRootCommentsWithDeleted(query.postId(), cursor); + List rootsInOrder = commentQueryDtoCursorBasedList.contents(); + + // 2. 조회한 루트 댓글의 전체 active 답글들 작성순 조회 -> map 구조로 저장 + Set allCommentIds = new HashSet<>(); // 반환할 모든 댓글들의 id set + Map> childrenMap = new HashMap<>(); + for (CommentQueryDto root : rootsInOrder) { + List allActiveChildrenInOrder = commentQueryPort.findAllActiveChildrenComments(root.commentId()); + + childrenMap.put(root.commentId(), allActiveChildrenInOrder); + + allCommentIds.add(root.commentId()); + allActiveChildrenInOrder.stream() + .map(CommentQueryDto::commentId) + .forEach(allCommentIds::add); + } + + // 3. 반환할 모든 댓글(루트 + 자식 모두 포함) 중 유저가 좋아한 댓글 조회 + Set likedCommentIds = commentLikeQueryPort.findCommentIdsLikedByUser(allCommentIds, query.userId()); + + // 4. response 매핑 + List rootCommentResponses = buildRootCommentResponses(rootsInOrder, childrenMap, likedCommentIds); + + return new CommentForSinglePostResponse( + rootCommentResponses, + commentQueryDtoCursorBasedList.nextCursor(), + commentQueryDtoCursorBasedList.isLast() + ); + } + + private List buildRootCommentResponses( + List roots, + Map> childrenMap, + Set likedCommentIds) { + List responses = new ArrayList<>(); + for (CommentQueryDto root : roots) { + List children = childrenMap.getOrDefault(root.commentId(), Collections.emptyList()); + // 삭제된 루트 댓글이면서 자식이 없는 경우 건너뛰기 + if (root.isDeleted() && children.isEmpty()) { + continue; + } + responses.add(commentQueryMapper.toRootCommentResponseWithChildren(root, children, likedCommentIds)); + } + return responses; + } +} From 28c3b8b3363c25dd26e79b98199e07ca8a2a5489 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 7 Aug 2025 20:18:46 +0900 Subject: [PATCH 05/23] =?UTF-8?q?[feat]=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EC=9A=A9=20query=20dto=20=EC=B6=94=EA=B0=80=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../port/out/dto/CommentQueryDto.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/main/java/konkuk/thip/comment/application/port/out/dto/CommentQueryDto.java diff --git a/src/main/java/konkuk/thip/comment/application/port/out/dto/CommentQueryDto.java b/src/main/java/konkuk/thip/comment/application/port/out/dto/CommentQueryDto.java new file mode 100644 index 000000000..05475f02a --- /dev/null +++ b/src/main/java/konkuk/thip/comment/application/port/out/dto/CommentQueryDto.java @@ -0,0 +1,47 @@ +package konkuk.thip.comment.application.port.out.dto; + +import com.querydsl.core.annotations.QueryProjection; +import jakarta.annotation.Nullable; + +import java.time.LocalDateTime; + +public record CommentQueryDto( + Long commentId, + @Nullable Long parentCommentId, + @Nullable String parentCommentCreatorNickname, + Long creatorId, + String creatorProfileImageUrl, + String creatorNickname, + String alias, + String aliasColor, + LocalDateTime createdAt, // 댓글 작성 시각 + String content, + int likeCount, + Boolean isDeleted +) { + /** + * child comment + */ + @QueryProjection + public CommentQueryDto {} + + /** + * root comment + */ + @QueryProjection + public CommentQueryDto ( + Long commentId, + Long creatorId, + String creatorProfileImageUrl, + String creatorNickname, + String alias, + String aliasColor, + LocalDateTime createdAt, // 댓글 작성 시각 + String content, + int likeCount, + boolean isDeleted + ) { + this(commentId, null, null, creatorId, creatorProfileImageUrl, creatorNickname, + alias, aliasColor, createdAt, content, likeCount, isDeleted); + } +} From 33732a8f63cfdd145f89f68060b708aeb681c98e Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 7 Aug 2025 20:19:10 +0900 Subject: [PATCH 06/23] =?UTF-8?q?[feat]=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EC=9A=A9=20=EC=98=81=EC=86=8D=EC=84=B1=20port=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/port/out/CommentQueryPort.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java b/src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java index cce75f165..3c30b59c1 100644 --- a/src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java +++ b/src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java @@ -1,5 +1,14 @@ package konkuk.thip.comment.application.port.out; +import konkuk.thip.comment.application.port.out.dto.CommentQueryDto; +import konkuk.thip.common.util.Cursor; +import konkuk.thip.common.util.CursorBasedList; + +import java.util.List; + public interface CommentQueryPort { + CursorBasedList findLatestRootCommentsWithDeleted(Long postId, Cursor cursor); + + List findAllActiveChildrenComments(Long rootCommentId); } From a7e1f866c01dcbe22e61973650adc77849452b35 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 7 Aug 2025 20:19:30 +0900 Subject: [PATCH 07/23] =?UTF-8?q?[feat]=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EC=9A=A9=20=EC=98=81=EC=86=8D=EC=84=B1=20adapter=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommentQueryPersistenceAdapter.java | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java index a40d2e714..bc1f5750d 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java @@ -3,14 +3,37 @@ 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; + @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 findLatestRootCommentsWithDeleted(Long postId, Cursor cursor) { + LocalDateTime lastCreatedAt = cursor.isFirstRequest() ? null : cursor.getLocalDateTime(0); + int size = cursor.getPageSize(); + + List commentQueryDtos = commentJpaRepository.findRootCommentsWithDeletedByCreatedAtDesc(postId, lastCreatedAt, size); + + return CursorBasedList.of(commentQueryDtos, size, commentQueryDto -> { + Cursor nextCursor = new Cursor(List.of(commentQueryDto.createdAt().toString())); + return nextCursor.toEncodedString(); + }); + } + @Override + public List findAllActiveChildrenComments(Long rootCommentId) { + return commentJpaRepository.findAllActiveChildrenCommentsByCreatedAtAsc(rootCommentId); + } } From 5d8bdabde543455cac97d42012f171b4daaab263 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 7 Aug 2025 20:20:18 +0900 Subject: [PATCH 08/23] =?UTF-8?q?[feat]=20=EB=AA=A8=EB=93=A0=20=EB=A3=A8?= =?UTF-8?q?=ED=8A=B8=20=EB=8C=93=EA=B8=80=20=EC=A1=B0=ED=9A=8C,=20active?= =?UTF-8?q?=20=EC=83=81=ED=83=9C=EC=9D=B8=20=EB=AA=A8=EB=93=A0=20=EC=9E=90?= =?UTF-8?q?=EC=8B=9D=20=EB=8C=93=EA=B8=80=EB=93=A4=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=9A=A9=20QueryDSL=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/CommentQueryRepository.java | 13 ++ .../CommentQueryRepositoryImpl.java | 119 ++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java create mode 100644 src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java new file mode 100644 index 000000000..3e384d39c --- /dev/null +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java @@ -0,0 +1,13 @@ +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; + +public interface CommentQueryRepository { + + List findRootCommentsWithDeletedByCreatedAtDesc(Long postId, LocalDateTime lastCreatedAt, int size); + + List findAllActiveChildrenCommentsByCreatedAtAsc(Long rootCommentId); +} diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java new file mode 100644 index 000000000..77301eb8b --- /dev/null +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java @@ -0,0 +1,119 @@ +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 user = QUserJpaEntity.userJpaEntity; + private final QAliasJpaEntity alias = QAliasJpaEntity.aliasJpaEntity; + private final QCommentJpaEntity parentComment = new QCommentJpaEntity("parentComment"); + private final QUserJpaEntity parentUser = new QUserJpaEntity("parentUser"); + + @Override + public List findRootCommentsWithDeletedByCreatedAtDesc(Long postId, LocalDateTime lastCreatedAt, int size) { + // 최상위 댓글(size+1) 프로젝션 생성 + QCommentQueryDto proj = new QCommentQueryDto( + comment.commentId, + user.userJpaEntity.userId, + alias.imageUrl, + user.nickname, + alias.value, + alias.color, + comment.createdAt, + comment.content, + comment.likeCount, + comment.status.eq(StatusType.INACTIVE) // 루트 댓글이 삭제된 상태인지 아닌지 + ); + + // WHERE 절 분리 + BooleanExpression whereClause = comment.postJpaEntity.postId.eq(postId) + .and(comment.parent.isNull()) // 게시글의 최상위 댓글 조회 + .and(lastCreatedAt != null // 최신순 정렬 + ? comment.createdAt.lt(lastCreatedAt) + : Expressions.TRUE + ); + + // 조회 및 반환 + return queryFactory + .select(proj) + .from(comment) + .leftJoin(comment.userJpaEntity, user) + .leftJoin(user.aliasForUserJpaEntity, alias) + .where(whereClause) + .orderBy(comment.createdAt.desc()) + .limit(size + 1) // size + 1 개 조회 + .fetch(); + } + + @Override + public List findAllActiveChildrenCommentsByCreatedAtAsc(Long rootCommentId) { + List allDescendants = new ArrayList<>(); // 결과 누적용 리스트 + + // 1) 부모 ID 집합에 루트 댓글 ID 추가 + Set parentIds = new HashSet<>(); + parentIds.add(rootCommentId); + + // 2) 자손 댓글용 프로젝션 (부모 댓글 ID·작성자 닉네임 포함) + QCommentQueryDto childProj = new QCommentQueryDto( + comment.commentId, + comment.parent.commentId, + parentUser.nickname, + user.userJpaEntity.userId, + alias.imageUrl, + user.nickname, + alias.value, + alias.color, + comment.createdAt, + comment.content, + comment.likeCount, + comment.status.eq(StatusType.INACTIVE) + ); + + // 3) 단계별 자식 댓글 조회 + while (!parentIds.isEmpty()) { + List children = queryFactory + .select(childProj) + .from(comment) + .leftJoin(comment.parent, parentComment) + .leftJoin(parentComment.userJpaEntity, parentUser) + .leftJoin(comment.userJpaEntity, user) + .leftJoin(user.aliasForUserJpaEntity, alias) + .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; + } +} From 5be7ef060837571ee16f5bc32338d8dc76825b09 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 7 Aug 2025 20:20:55 +0900 Subject: [PATCH 09/23] =?UTF-8?q?[feat]=20=EC=9C=A0=EC=A0=80=EA=B0=80=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=ED=95=98=EB=8A=94=20=EB=8C=93=EA=B8=80?= =?UTF-8?q?=EB=93=A4=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=EC=9A=A9=20?= =?UTF-8?q?=EC=98=81=EC=86=8D=EC=84=B1=20port=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/application/port/out/CommentLikeQueryPort.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/konkuk/thip/comment/application/port/out/CommentLikeQueryPort.java b/src/main/java/konkuk/thip/comment/application/port/out/CommentLikeQueryPort.java index 9de640d96..7281c109d 100644 --- a/src/main/java/konkuk/thip/comment/application/port/out/CommentLikeQueryPort.java +++ b/src/main/java/konkuk/thip/comment/application/port/out/CommentLikeQueryPort.java @@ -1,5 +1,9 @@ package konkuk.thip.comment.application.port.out; +import java.util.Set; + public interface CommentLikeQueryPort { boolean isLikedCommentByUser(Long userId, Long commentId); + + Set findCommentIdsLikedByUser(Set commentIds, Long userId); } From ca6c4bb6c7f9a7acd2a3bc6eab1625424a68a76b Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 7 Aug 2025 20:21:36 +0900 Subject: [PATCH 10/23] =?UTF-8?q?[feat]=20=EC=9C=A0=EC=A0=80=EA=B0=80=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=ED=95=98=EB=8A=94=20=EB=8C=93=EA=B8=80?= =?UTF-8?q?=EB=93=A4=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=EC=9A=A9=20?= =?UTF-8?q?jpql=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/CommentLikeQueryPersistenceAdapter.java | 7 +++++++ .../persistence/repository/CommentLikeJpaRepository.java | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeQueryPersistenceAdapter.java index ce173d259..fb4ed6b36 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeQueryPersistenceAdapter.java @@ -8,6 +8,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.util.Set; + @Repository @RequiredArgsConstructor public class CommentLikeQueryPersistenceAdapter implements CommentLikeQueryPort { @@ -21,4 +23,9 @@ public class CommentLikeQueryPersistenceAdapter implements CommentLikeQueryPort public boolean isLikedCommentByUser(Long userId, Long commentId) { return commentLikeJpaRepository.existsByUserIdAndCommentId(userId, commentId); } + + @Override + public Set findCommentIdsLikedByUser(Set commentIds, Long userId) { + return commentLikeJpaRepository.findCommentIdsLikedByUser(commentIds, userId); + } } diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java index a79283150..939e7f279 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.repository.query.Param; import java.util.List; +import java.util.Set; public interface CommentLikeJpaRepository extends JpaRepository { @@ -27,4 +28,6 @@ public interface CommentLikeJpaRepository extends JpaRepository findCommentIdsLikedByUser(@Param("commentIds") Set commentIds, @Param("userId") Long userId); +} From 58189b9e486e89bdbf4ed9b5fa0586d54d1ccf9c Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 7 Aug 2025 20:21:59 +0900 Subject: [PATCH 11/23] =?UTF-8?q?[feat]=20CommentJpaRepository=20=EC=97=90?= =?UTF-8?q?=20QueryDSL=20=EC=A1=B0=ED=9A=8C=EC=9A=A9=20interface=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/persistence/repository/CommentJpaRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java index 4e08628f9..c3761e06a 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java @@ -6,6 +6,6 @@ import java.util.Optional; -public interface CommentJpaRepository extends JpaRepository { +public interface CommentJpaRepository extends JpaRepository, CommentQueryRepository { Optional findByCommentIdAndStatus(Long commentId, StatusType status); } From 0c211cbbfd04da6df87ea9cedc8dbcd7ac3ebd59 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 7 Aug 2025 20:22:17 +0900 Subject: [PATCH 12/23] =?UTF-8?q?[feat]=20CommentQueryDto=20->=20response?= =?UTF-8?q?=20=EB=A1=9C=EC=9D=98=20=EB=A7=A4=ED=8D=BC=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/CommentQueryMapper.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/main/java/konkuk/thip/comment/application/mapper/CommentQueryMapper.java diff --git a/src/main/java/konkuk/thip/comment/application/mapper/CommentQueryMapper.java b/src/main/java/konkuk/thip/comment/application/mapper/CommentQueryMapper.java new file mode 100644 index 000000000..b7661feb6 --- /dev/null +++ b/src/main/java/konkuk/thip/comment/application/mapper/CommentQueryMapper.java @@ -0,0 +1,59 @@ +package konkuk.thip.comment.application.mapper; + +import konkuk.thip.comment.adapter.in.web.response.CommentForSinglePostResponse; +import konkuk.thip.comment.application.port.out.dto.CommentQueryDto; +import konkuk.thip.common.util.DateUtil; +import org.mapstruct.*; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +@Mapper( + componentModel = "spring", + imports = DateUtil.class, + unmappedTargetPolicy = ReportingPolicy.IGNORE // 명시적으로 매핑하지 않은 필드를 무시하도록 설정 +) +public interface CommentQueryMapper { + + /** + * 정상(root) 댓글 매핑 (답글 제외) + */ + @Mapping(target = "replyList", expression = "java(new java.util.ArrayList<>())") + @Mapping(target = "isLike", expression = "java(likedCommentIds.contains(root.commentId()))") + @Mapping(target = "isDeleted", constant = "false") + @Mapping(target = "postDate", expression = "java(DateUtil.formatBeforeTime(root.createdAt()))") + CommentForSinglePostResponse.RootCommentDto toRoot(CommentQueryDto root, + @Context Set likedCommentIds); + + /** + * 개별 답글 매핑 + */ + @Mapping(target = "isLike", expression = "java(likedCommentIds.contains(child.commentId()))") + @Mapping(target = "postDate", expression = "java(DateUtil.formatBeforeTime(child.createdAt()))") + CommentForSinglePostResponse.RootCommentDto.ReplyDto toReply(CommentQueryDto child, @Context Set likedCommentIds); + + /** + * 답글 리스트 헬퍼 + */ + default List mapReplies(List children, @Context Set likedCommentIds) { + if (children == null || children.isEmpty()) { + return Collections.emptyList(); + } + return children.stream() + .map(child -> toReply(child, likedCommentIds)) + .toList(); + } + + default CommentForSinglePostResponse.RootCommentDto toRootCommentResponseWithChildren(CommentQueryDto root, List children, @Context Set likedCommentIds) { + List replyDtos = mapReplies(children, likedCommentIds); + + if (root.isDeleted()) { // 삭제된 루트 & children 이 존재하는 경우 + return CommentForSinglePostResponse.RootCommentDto.createDeletedRootCommentDto(replyDtos); + } + + CommentForSinglePostResponse.RootCommentDto rootDto = toRoot(root, likedCommentIds); + rootDto.replyList().addAll(replyDtos); + return rootDto; + } +} From 8bec83669350175c1bd5e3bfddfb73856e727d2d Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 7 Aug 2025 20:22:37 +0900 Subject: [PATCH 13/23] =?UTF-8?q?[feat]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20jp?= =?UTF-8?q?a=20entity=20=EC=83=9D=EC=84=B1=ED=95=98=EB=8A=94=20=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/util/TestEntityFactory.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java index dd9fc44fe..c68069fcc 100644 --- a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java +++ b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java @@ -185,6 +185,20 @@ public static CommentJpaEntity createComment(PostJpaEntity post, UserJpaEntity u .build(); } + /** + * 댓글 내용, likeCount 커스텀 + */ + public static CommentJpaEntity createComment(PostJpaEntity post, UserJpaEntity user,PostType postType, String content, int likeCount) { + return CommentJpaEntity.builder() + .content(content) + .postJpaEntity(post) + .userJpaEntity(user) + .likeCount(likeCount) + .reportCount(0) + .postType(postType) + .build(); + } + public static CommentJpaEntity createReplyComment(PostJpaEntity post, UserJpaEntity user,PostType postType,CommentJpaEntity parentComment) { return CommentJpaEntity.builder() .content("댓글 내용") @@ -197,6 +211,21 @@ public static CommentJpaEntity createReplyComment(PostJpaEntity post, UserJpaEnt .build(); } + /** + * 자식 댓글 내용, likeCount 커스텀 + */ + public static CommentJpaEntity createReplyComment(PostJpaEntity post, UserJpaEntity user, PostType postType, CommentJpaEntity parentComment, String content, int likeCount) { + return CommentJpaEntity.builder() + .content(content) + .postJpaEntity(post) + .userJpaEntity(user) + .likeCount(likeCount) + .reportCount(0) + .postType(postType) + .parent(parentComment) + .build(); + } + public static CommentLikeJpaEntity createCommentLike(CommentJpaEntity comment, UserJpaEntity user) { return CommentLikeJpaEntity.builder() .userJpaEntity(user) From db77f03be9afa90270395c858d89a1df30a4b427 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 7 Aug 2025 20:22:50 +0900 Subject: [PATCH 14/23] =?UTF-8?q?[test]=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20api=20=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/CommentShowAllApiTest.java | 360 ++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java diff --git a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java new file mode 100644 index 000000000..d1acbe0cf --- /dev/null +++ b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java @@ -0,0 +1,360 @@ +package konkuk.thip.comment.adapter.in.web; + +import com.jayway.jsonpath.JsonPath; +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; +import konkuk.thip.comment.adapter.out.jpa.CommentJpaEntity; +import konkuk.thip.comment.adapter.out.persistence.repository.CommentJpaRepository; +import konkuk.thip.comment.adapter.out.persistence.repository.CommentLikeJpaRepository; +import konkuk.thip.common.post.PostType; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; +import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; +import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.adapter.out.persistence.repository.alias.AliasJpaRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; + +import static org.hamcrest.Matchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 댓글 조회 api 통합 테스트") +class CommentShowAllApiTest { + + @Autowired private MockMvc mockMvc; + @Autowired private AliasJpaRepository aliasJpaRepository; + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private FeedJpaRepository feedJpaRepository; + @Autowired private BookJpaRepository bookJpaRepository; + @Autowired private CommentJpaRepository commentJpaRepository; + @Autowired private CommentLikeJpaRepository commentLikeJpaRepository; + @Autowired private JdbcTemplate jdbcTemplate; + + @AfterEach + void tearDown() { + commentLikeJpaRepository.deleteAllInBatch(); + commentJpaRepository.deleteAllInBatch(); + feedJpaRepository.deleteAllInBatch(); + userJpaRepository.deleteAllInBatch(); + aliasJpaRepository.deleteAllInBatch(); + bookJpaRepository.deleteAllInBatch(); + } + + @Test + @DisplayName("댓글 조회 요청에 대하여, 특정 게시글(= 피드, 기록, 투표)의 루트 댓글, 루트 댓글의 모든 자식 댓글의 데이터를 구분하여 반환한다.") + void comment_show_all_test() throws Exception { + //given + AliasJpaEntity a0 = aliasJpaRepository.save(TestEntityFactory.createScienceAlias()); + UserJpaEntity me = userJpaRepository.save(TestEntityFactory.createUser(a0, "me")); + UserJpaEntity user1 = userJpaRepository.save(TestEntityFactory.createUser(a0, "user1")); + + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBook()); // 공통 Book + + // 피드, 댓글, 자식 댓글 생성 및 생성일 직접 설정 + LocalDateTime base = LocalDateTime.now(); + FeedJpaEntity f1 = feedJpaRepository.save(TestEntityFactory.createFeed(me, book, true, 10, 5, List.of())); + CommentJpaEntity comment1 = commentJpaRepository.save(TestEntityFactory.createComment(f1, user1, PostType.FEED, "댓글1", 5)); + CommentJpaEntity comment1_1 = commentJpaRepository.save(TestEntityFactory.createReplyComment(f1, me, PostType.FEED, comment1, "댓글1_답글1", 8)); + + commentLikeJpaRepository.save(TestEntityFactory.createCommentLike(comment1, me)); // me가 comment1을 좋아함 + + feedJpaRepository.flush(); + jdbcTemplate.update( + "UPDATE posts SET created_at = ? WHERE post_id = ?", + Timestamp.valueOf(base.minusMinutes(50)), f1.getPostId()); + + commentJpaRepository.flush(); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(40)), comment1.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(30)), comment1_1.getCommentId()); + + //when //then + mockMvc.perform(get("/comments/{postId}", f1.getPostId().intValue()) + .requestAttr("userId", me.getUserId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.commentList", hasSize(1))) + /** + * 루트 댓글 : 댓글 정보, 댓글 작성자 정보, 좋아요 수, 삭제된 댓글 여부 등을 반환한다 + * 자식 댓글 : 부모 댓글의 작성자 정보(@ 표시를 위함), 댓글 정보, 댓글 작성자 정보, 좋아요 수 등을 반환한다 + */ + .andExpect(jsonPath("$.data.commentList[0].commentId", is(comment1.getCommentId().intValue()))) + .andExpect(jsonPath("$.data.commentList[0].creatorNickname", is(user1.getNickname()))) + .andExpect(jsonPath("$.data.commentList[0].content", is(comment1.getContent()))) + .andExpect(jsonPath("$.data.commentList[0].likeCount", is(comment1.getLikeCount()))) + .andExpect(jsonPath("$.data.commentList[0].isLike", is(true))) // me가 comment1을 좋아함 + .andExpect(jsonPath("$.data.commentList[0].replyList", hasSize(1))) // 자식 댓글 1개 존재 + + .andExpect(jsonPath("$.data.commentList[0].replyList[0].parentCommentCreatorNickname", is(user1.getNickname()))) // comment1_1의 부모 댓글(= comment1) 의 작성자 = user1 + .andExpect(jsonPath("$.data.commentList[0].replyList[0].commentId", is(comment1_1.getCommentId().intValue()))) + .andExpect(jsonPath("$.data.commentList[0].replyList[0].creatorNickname", is(me.getNickname()))) + .andExpect(jsonPath("$.data.commentList[0].replyList[0].content", is(comment1_1.getContent()))) + .andExpect(jsonPath("$.data.commentList[0].replyList[0].likeCount", is(comment1_1.getLikeCount()))) + .andExpect(jsonPath("$.data.commentList[0].replyList[0].isLike", is(false))); // me가 comment1_1을 좋아하지 않음 + } + + @Test + @DisplayName("루트 댓글은 최신순, 루트 댓글의 모든 자식 댓글은 작성 시각순으로 정렬하여 반환한다.") + void comment_show_all_ordering_test() throws Exception { + //given + AliasJpaEntity a0 = aliasJpaRepository.save(TestEntityFactory.createScienceAlias()); + UserJpaEntity me = userJpaRepository.save(TestEntityFactory.createUser(a0, "me")); + UserJpaEntity user1 = userJpaRepository.save(TestEntityFactory.createUser(a0, "user1")); + UserJpaEntity user2 = userJpaRepository.save(TestEntityFactory.createUser(a0, "user2")); + UserJpaEntity user3 = userJpaRepository.save(TestEntityFactory.createUser(a0, "user3")); + + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBook()); // 공통 Book + + // 피드, 댓글, 자식 댓글 생성 및 생성일 직접 설정 + LocalDateTime base = LocalDateTime.now(); + FeedJpaEntity f1 = feedJpaRepository.save(TestEntityFactory.createFeed(me, book, true, 10, 5, List.of())); + CommentJpaEntity comment1 = commentJpaRepository.save(TestEntityFactory.createComment(f1, user1, PostType.FEED, "댓글1", 5)); + CommentJpaEntity comment2 = commentJpaRepository.save(TestEntityFactory.createComment(f1, user2, PostType.FEED, "댓글2", 10)); + CommentJpaEntity comment1_1 = commentJpaRepository.save(TestEntityFactory.createReplyComment(f1, user3, PostType.FEED, comment1, "댓글1_답글1", 2)); + CommentJpaEntity comment1_2 = commentJpaRepository.save(TestEntityFactory.createReplyComment(f1, me, PostType.FEED, comment1, "댓글1_답글2", 8)); + CommentJpaEntity comment1_1_1 = commentJpaRepository.save(TestEntityFactory.createReplyComment(f1, user1, PostType.FEED, comment1_1, "댓글1_답글1_답글1", 3)); + + feedJpaRepository.flush(); + jdbcTemplate.update( + "UPDATE posts SET created_at = ? WHERE post_id = ?", + Timestamp.valueOf(base.minusMinutes(50)), f1.getPostId()); + + commentJpaRepository.flush(); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(40)), comment1.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(30)), comment2.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(20)), comment1_1.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(10)), comment1_2.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(5)), comment1_1_1.getCommentId()); + + + //when //then + mockMvc.perform(get("/comments/{postId}", f1.getPostId().intValue()) + .requestAttr("userId", me.getUserId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.commentList", hasSize(2))) + /** + * 정렬 조건 + * 게시글에 바로 달린 댓글들(= 루트 댓글) : 최신순 정렬 + * 루트 댓글의 모든 하위 댓글들 : 작성 시간 순 정렬 (최신순 역순) + */ + // 루트 댓글 정렬 확인 + .andExpect(jsonPath("$.data.commentList[0].commentId", is(comment2.getCommentId().intValue()))) + .andExpect(jsonPath("$.data.commentList[1].commentId", is(comment1.getCommentId().intValue()))) + // 루트 댓글의 모든 자식 댓글 정렬 확인 + .andExpect(jsonPath("$.data.commentList[0].replyList", hasSize(0))) // comment2 는 자식 댓글 없음 + .andExpect(jsonPath("$.data.commentList[1].replyList", hasSize(3))) // comment1 은 총 3개의 자식 댓글 있음 + + .andExpect(jsonPath("$.data.commentList[1].replyList[0].commentId", is(comment1_1.getCommentId().intValue()))) + .andExpect(jsonPath("$.data.commentList[1].replyList[0].parentCommentCreatorNickname", is(user1.getNickname()))) // comment1_1의 부모 댓글(= comment1) 작성자 = user1 + + .andExpect(jsonPath("$.data.commentList[1].replyList[1].commentId", is(comment1_2.getCommentId().intValue()))) + .andExpect(jsonPath("$.data.commentList[1].replyList[1].parentCommentCreatorNickname", is(user1.getNickname()))) // comment1_1의 부모 댓글(= comment1) 작성자 = user1 + + .andExpect(jsonPath("$.data.commentList[1].replyList[2].commentId", is(comment1_1_1.getCommentId().intValue()))) + .andExpect(jsonPath("$.data.commentList[1].replyList[2].parentCommentCreatorNickname", is(user3.getNickname()))); // comment1_1_1의 부모 댓글(= comment1_1) 작성자 = user3 + } + + @Test + @DisplayName("삭제된 루트 댓글의 경우, 자식 댓글이 있으면 반환하고, 없으면 반환하지 않는다.") + void comment_show_all_deleted_root_comment_test() throws Exception { + //given + AliasJpaEntity a0 = aliasJpaRepository.save(TestEntityFactory.createScienceAlias()); + UserJpaEntity me = userJpaRepository.save(TestEntityFactory.createUser(a0, "me")); + UserJpaEntity user1 = userJpaRepository.save(TestEntityFactory.createUser(a0, "user1")); + + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBook()); // 공통 Book + + // 피드, 댓글, 자식 댓글 생성 및 생성일 직접 설정 + LocalDateTime base = LocalDateTime.now(); + FeedJpaEntity f1 = feedJpaRepository.save(TestEntityFactory.createFeed(me, book, true, 10, 5, List.of())); + CommentJpaEntity comment1 = commentJpaRepository.save(TestEntityFactory.createComment(f1, user1, PostType.FEED, "댓글1", 5)); + CommentJpaEntity comment1_1 = commentJpaRepository.save(TestEntityFactory.createReplyComment(f1, me, PostType.FEED, comment1, "댓글1_답글1", 8)); + CommentJpaEntity comment2 = commentJpaRepository.save(TestEntityFactory.createComment(f1, user1, PostType.FEED, "댓글2", 5)); + + feedJpaRepository.flush(); + jdbcTemplate.update( + "UPDATE posts SET created_at = ? WHERE post_id = ?", + Timestamp.valueOf(base.minusMinutes(50)), f1.getPostId()); + + commentJpaRepository.flush(); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(40)), comment1.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(30)), comment1_1.getCommentId()); + + // comment1, 2 soft delete + jdbcTemplate.update( + "UPDATE comments SET status = 'INACTIVE' WHERE comment_id = ?", + comment1.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET status = 'INACTIVE' WHERE comment_id = ?", + comment2.getCommentId()); + + //when //then + mockMvc.perform(get("/comments/{postId}", f1.getPostId().intValue()) + .requestAttr("userId", me.getUserId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.commentList", hasSize(1))) // comment1 만 조회된다 + /** + * 루트 댓글 : + * 자식 댓글 : 부모 댓글의 작성자 정보(@ 표시를 위함), 댓글 정보, 댓글 작성자 정보, 좋아요 수 등을 반환한다 + */ + .andExpect(jsonPath("$.data.commentList[0].commentId", nullValue())) + .andExpect(jsonPath("$.data.commentList[0].creatorNickname", nullValue())) + .andExpect(jsonPath("$.data.commentList[0].content", nullValue())) + .andExpect(jsonPath("$.data.commentList[0].isDeleted", is(true))) + .andExpect(jsonPath("$.data.commentList[0].replyList", hasSize(1))) // 자식 댓글 1개 존재 + + .andExpect(jsonPath("$.data.commentList[0].replyList[0].parentCommentCreatorNickname", is(user1.getNickname()))) // comment1_1의 부모 댓글(= comment1) 의 작성자 = user1 + .andExpect(jsonPath("$.data.commentList[0].replyList[0].commentId", is(comment1_1.getCommentId().intValue()))) + .andExpect(jsonPath("$.data.commentList[0].replyList[0].creatorNickname", is(me.getNickname()))) + .andExpect(jsonPath("$.data.commentList[0].replyList[0].content", is(comment1_1.getContent()))) + .andExpect(jsonPath("$.data.commentList[0].replyList[0].likeCount", is(comment1_1.getLikeCount()))) + .andExpect(jsonPath("$.data.commentList[0].replyList[0].isLike", is(false))); // me가 comment1_1을 좋아하지 않음 + } + + @Test + @DisplayName("게시글에 달린 댓글이 많을 경우, 루트 댓글을 기준으로 페이징 처리 한다.") + void comment_show_all_page_test() throws Exception { + //given + AliasJpaEntity a0 = aliasJpaRepository.save(TestEntityFactory.createScienceAlias()); + UserJpaEntity me = userJpaRepository.save(TestEntityFactory.createUser(a0, "me")); + UserJpaEntity user1 = userJpaRepository.save(TestEntityFactory.createUser(a0, "user1")); + + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBook()); // 공통 Book + + // 피드, 댓글, 자식 댓글 생성 및 생성일 직접 설정 + LocalDateTime base = LocalDateTime.now(); + FeedJpaEntity f1 = feedJpaRepository.save(TestEntityFactory.createFeed(me, book, true, 10, 5, List.of())); + CommentJpaEntity comment1 = commentJpaRepository.save(TestEntityFactory.createComment(f1, user1, PostType.FEED, "댓글1", 5)); + CommentJpaEntity comment2 = commentJpaRepository.save(TestEntityFactory.createComment(f1, user1, PostType.FEED, "댓글2", 5)); + CommentJpaEntity comment3 = commentJpaRepository.save(TestEntityFactory.createComment(f1, user1, PostType.FEED, "댓글3", 5)); + CommentJpaEntity comment4 = commentJpaRepository.save(TestEntityFactory.createComment(f1, user1, PostType.FEED, "댓글4", 5)); + CommentJpaEntity comment5 = commentJpaRepository.save(TestEntityFactory.createComment(f1, user1, PostType.FEED, "댓글5", 5)); + CommentJpaEntity comment6 = commentJpaRepository.save(TestEntityFactory.createComment(f1, user1, PostType.FEED, "댓글6", 5)); + CommentJpaEntity comment7 = commentJpaRepository.save(TestEntityFactory.createComment(f1, user1, PostType.FEED, "댓글7", 5)); + CommentJpaEntity comment8 = commentJpaRepository.save(TestEntityFactory.createComment(f1, user1, PostType.FEED, "댓글8", 5)); + CommentJpaEntity comment9 = commentJpaRepository.save(TestEntityFactory.createComment(f1, user1, PostType.FEED, "댓글9", 5)); + CommentJpaEntity comment10 = commentJpaRepository.save(TestEntityFactory.createComment(f1, user1, PostType.FEED, "댓글10", 5)); + CommentJpaEntity comment11 = commentJpaRepository.save(TestEntityFactory.createComment(f1, user1, PostType.FEED, "댓글11", 5)); + CommentJpaEntity comment12 = commentJpaRepository.save(TestEntityFactory.createComment(f1, user1, PostType.FEED, "댓글12", 5)); + + feedJpaRepository.flush(); + jdbcTemplate.update( + "UPDATE posts SET created_at = ? WHERE post_id = ?", + Timestamp.valueOf(base.minusMinutes(50)), f1.getPostId()); + + commentJpaRepository.flush(); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(40)), comment1.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(35)), comment2.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(30)), comment3.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(25)), comment4.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(20)), comment5.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(15)), comment6.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(10)), comment7.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(9)), comment8.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(8)), comment9.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(7)), comment10.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(6)), comment11.getCommentId()); + jdbcTemplate.update( + "UPDATE comments SET created_at = ? WHERE comment_id = ?", + Timestamp.valueOf(base.minusMinutes(5)), comment12.getCommentId()); + + //when //then + MvcResult firstResult = mockMvc.perform(get("/comments/{postId}", f1.getPostId().intValue()) + .requestAttr("userId", me.getUserId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.nextCursor", notNullValue())) + .andExpect(jsonPath("$.data.isLast", is(false))) + .andExpect(jsonPath("$.data.commentList", hasSize(10))) + /** + * 루트 댓글 : 댓글 정보, 댓글 작성자 정보, 좋아요 수, 삭제된 댓글 여부 등을 반환한다 + * 자식 댓글 : 부모 댓글의 작성자 정보(@ 표시를 위함), 댓글 정보, 댓글 작성자 정보, 좋아요 수 등을 반환한다 + */ + // 루트 댓글 정렬 확인 + .andExpect(jsonPath("$.data.commentList[0].commentId", is(comment12.getCommentId().intValue()))) + .andExpect(jsonPath("$.data.commentList[1].commentId", is(comment11.getCommentId().intValue()))) + .andExpect(jsonPath("$.data.commentList[2].commentId", is(comment10.getCommentId().intValue()))) + .andExpect(jsonPath("$.data.commentList[3].commentId", is(comment9.getCommentId().intValue()))) + .andExpect(jsonPath("$.data.commentList[4].commentId", is(comment8.getCommentId().intValue()))) + .andExpect(jsonPath("$.data.commentList[5].commentId", is(comment7.getCommentId().intValue()))) + .andExpect(jsonPath("$.data.commentList[6].commentId", is(comment6.getCommentId().intValue()))) + .andExpect(jsonPath("$.data.commentList[7].commentId", is(comment5.getCommentId().intValue()))) + .andExpect(jsonPath("$.data.commentList[8].commentId", is(comment4.getCommentId().intValue()))) + .andExpect(jsonPath("$.data.commentList[9].commentId", is(comment3.getCommentId().intValue()))) + .andReturn(); + + String responseBody = firstResult.getResponse().getContentAsString(); + String nextCursor = JsonPath.read(responseBody, "$.data.nextCursor"); + + mockMvc.perform(get("/comments/{postId}", f1.getPostId().intValue()) + .requestAttr("userId", me.getUserId()) + .param("cursor", nextCursor)) // 2페이지 요청 + + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.isLast", is(true))) + .andExpect(jsonPath("$.data.commentList", hasSize(2))) + /** + * 루트 댓글 : 댓글 정보, 댓글 작성자 정보, 좋아요 수, 삭제된 댓글 여부 등을 반환한다 + * 자식 댓글 : 부모 댓글의 작성자 정보(@ 표시를 위함), 댓글 정보, 댓글 작성자 정보, 좋아요 수 등을 반환한다 + */ + // 루트 댓글 정렬 확인 + .andExpect(jsonPath("$.data.commentList[0].commentId", is(comment2.getCommentId().intValue()))) + .andExpect(jsonPath("$.data.commentList[1].commentId", is(comment1.getCommentId().intValue()))); + } +} From 3b3805d599a5df5004831bd6fd623d1017ae456e Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 8 Aug 2025 15:19:27 +0900 Subject: [PATCH 15/23] =?UTF-8?q?[feat]=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20api=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20operation=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/adapter/in/web/CommentQueryController.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java b/src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java index d32b1248e..f5bbb2107 100644 --- a/src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java +++ b/src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java @@ -1,6 +1,8 @@ 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; @@ -12,19 +14,24 @@ 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 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", required = false) final String postType, + @RequestParam(value = "postType", required = false) final String postType, // 이거 우짤지 ?? @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)") @RequestParam(value = "cursor", required = false) final String cursor) { return BaseResponse.ok(commentShowAllUseCase.showAllCommentsOfPost( From 8668ec71b77012d30ca48cd2a6497c87e8860ac4 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 8 Aug 2025 15:44:08 +0900 Subject: [PATCH 16/23] =?UTF-8?q?[refactor]=20=EB=A3=A8=ED=8A=B8=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C,?= =?UTF-8?q?=20active=20=EC=9E=90=EC=8B=9D=20=EB=8C=93=EA=B8=80=EB=93=A4=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=20QueryDSL=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommentQueryRepositoryImpl.java | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java index 77301eb8b..d6fc5a544 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java @@ -23,21 +23,21 @@ public class CommentQueryRepositoryImpl implements CommentQueryRepository { private final JPAQueryFactory queryFactory; private final QCommentJpaEntity comment = QCommentJpaEntity.commentJpaEntity; - private final QUserJpaEntity user = QUserJpaEntity.userJpaEntity; - private final QAliasJpaEntity alias = QAliasJpaEntity.aliasJpaEntity; + private final QUserJpaEntity commentCreator = QUserJpaEntity.userJpaEntity; + private final QAliasJpaEntity aliasOfCommentCreator = QAliasJpaEntity.aliasJpaEntity; private final QCommentJpaEntity parentComment = new QCommentJpaEntity("parentComment"); - private final QUserJpaEntity parentUser = new QUserJpaEntity("parentUser"); + private final QUserJpaEntity parentCommentCreator = new QUserJpaEntity("parentCommentCreator"); @Override public List findRootCommentsWithDeletedByCreatedAtDesc(Long postId, LocalDateTime lastCreatedAt, int size) { // 최상위 댓글(size+1) 프로젝션 생성 QCommentQueryDto proj = new QCommentQueryDto( comment.commentId, - user.userJpaEntity.userId, - alias.imageUrl, - user.nickname, - alias.value, - alias.color, + commentCreator.userId, + aliasOfCommentCreator.imageUrl, + commentCreator.nickname, + aliasOfCommentCreator.value, + aliasOfCommentCreator.color, comment.createdAt, comment.content, comment.likeCount, @@ -56,8 +56,8 @@ public List findRootCommentsWithDeletedByCreatedAtDesc(Long pos return queryFactory .select(proj) .from(comment) - .leftJoin(comment.userJpaEntity, user) - .leftJoin(user.aliasForUserJpaEntity, alias) + .leftJoin(comment.userJpaEntity, commentCreator) + .leftJoin(commentCreator.aliasForUserJpaEntity, aliasOfCommentCreator) .where(whereClause) .orderBy(comment.createdAt.desc()) .limit(size + 1) // size + 1 개 조회 @@ -76,12 +76,12 @@ public List findAllActiveChildrenCommentsByCreatedAtAsc(Long ro QCommentQueryDto childProj = new QCommentQueryDto( comment.commentId, comment.parent.commentId, - parentUser.nickname, - user.userJpaEntity.userId, - alias.imageUrl, - user.nickname, - alias.value, - alias.color, + parentCommentCreator.nickname, + commentCreator.userId, + aliasOfCommentCreator.imageUrl, + commentCreator.nickname, + aliasOfCommentCreator.value, + aliasOfCommentCreator.color, comment.createdAt, comment.content, comment.likeCount, @@ -94,9 +94,9 @@ public List findAllActiveChildrenCommentsByCreatedAtAsc(Long ro .select(childProj) .from(comment) .leftJoin(comment.parent, parentComment) - .leftJoin(parentComment.userJpaEntity, parentUser) - .leftJoin(comment.userJpaEntity, user) - .leftJoin(user.aliasForUserJpaEntity, alias) + .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인 것만 조회 From 97b55a10a6af50d1ca3f4d01b25003bd1cdfa026 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 8 Aug 2025 17:29:55 +0900 Subject: [PATCH 17/23] =?UTF-8?q?[refactor]=20=EB=A3=A8=ED=8A=B8=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=EA=B3=BC=20=EC=97=B0=EA=B4=80=EB=90=9C=20?= =?UTF-8?q?=EB=AA=A8=EB=93=A0=20=EC=9E=90=EC=8B=9D=20=EB=8C=93=EA=B8=80?= =?UTF-8?q?=EB=93=A4=EC=9D=84=20Map=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=ED=95=98=EB=8A=94=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=A0=95=EC=9D=98=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - value 에 해당하는 모든 자식 댓글들을 작성시간순으로 조회한다는 의미를 담기 위해 OldestFirst 네이밍 채택 --- .../persistence/CommentQueryPersistenceAdapter.java | 11 +++++++++-- .../application/port/out/CommentQueryPort.java | 6 +++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java index bc1f5750d..8d305dbbd 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java @@ -11,6 +11,8 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Map; +import java.util.Set; @Repository @RequiredArgsConstructor @@ -33,7 +35,12 @@ public CursorBasedList findLatestRootCommentsWithDeleted(Long p } @Override - public List findAllActiveChildrenComments(Long rootCommentId) { - return commentJpaRepository.findAllActiveChildrenCommentsByCreatedAtAsc(rootCommentId); + public List findAllActiveChildCommentsOldestFirst(Long rootCommentId) { + return commentJpaRepository.findAllActiveChildCommentsByCreatedAtAsc(rootCommentId); + } + + @Override + public Map> findAllActiveChildCommentsOldestFirst(Set rootCommentIds) { + return commentJpaRepository.findAllActiveChildCommentsByCreatedAtAsc(rootCommentIds); } } diff --git a/src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java b/src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java index 3c30b59c1..69fc24818 100644 --- a/src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java +++ b/src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java @@ -5,10 +5,14 @@ import konkuk.thip.common.util.CursorBasedList; import java.util.List; +import java.util.Map; +import java.util.Set; public interface CommentQueryPort { CursorBasedList findLatestRootCommentsWithDeleted(Long postId, Cursor cursor); - List findAllActiveChildrenComments(Long rootCommentId); + List findAllActiveChildCommentsOldestFirst(Long rootCommentId); + + Map> findAllActiveChildCommentsOldestFirst(Set rootCommentIds); } From fe773954c6fc71250d88bf4032b2bf9db4666881 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 8 Aug 2025 17:31:08 +0900 Subject: [PATCH 18/23] =?UTF-8?q?[feat]=20=EC=9E=AC=EA=B7=80=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=ED=86=B5=ED=95=B4=20=EB=A3=A8=ED=8A=B8=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=EA=B3=BC=20=EC=97=B0=EA=B4=80=EB=90=9C=20?= =?UTF-8?q?=EB=AA=A8=EB=93=A0=20=EC=9E=90=EC=8B=9D=20=EB=8C=93=EA=B8=80?= =?UTF-8?q?=EB=93=A4=EC=9D=84=20=EC=A1=B0=ED=9A=8C=ED=95=9C=20=ED=9B=84,?= =?UTF-8?q?=20Map=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=95=98=EB=8A=94=20QueryDSL=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 전체 쿼리 호출 횟수 = 가장 깊은 트리의 깊이 --- .../repository/CommentQueryRepository.java | 6 +- .../CommentQueryRepositoryImpl.java | 71 ++++++++++++++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java index 3e384d39c..0b3f1d256 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java @@ -4,10 +4,14 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Map; +import java.util.Set; public interface CommentQueryRepository { List findRootCommentsWithDeletedByCreatedAtDesc(Long postId, LocalDateTime lastCreatedAt, int size); - List findAllActiveChildrenCommentsByCreatedAtAsc(Long rootCommentId); + List findAllActiveChildCommentsByCreatedAtAsc(Long rootCommentId); + + Map> findAllActiveChildCommentsByCreatedAtAsc(Set rootCommentIds); } diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java index d6fc5a544..04e889911 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java @@ -65,7 +65,7 @@ public List findRootCommentsWithDeletedByCreatedAtDesc(Long pos } @Override - public List findAllActiveChildrenCommentsByCreatedAtAsc(Long rootCommentId) { + public List findAllActiveChildCommentsByCreatedAtAsc(Long rootCommentId) { List allDescendants = new ArrayList<>(); // 결과 누적용 리스트 // 1) 부모 ID 집합에 루트 댓글 ID 추가 @@ -116,4 +116,73 @@ public List findAllActiveChildrenCommentsByCreatedAtAsc(Long ro allDescendants.sort(Comparator.comparing(CommentQueryDto::createdAt)); return allDescendants; } + + @Override + public Map> findAllActiveChildCommentsByCreatedAtAsc(Set rootCommentIds) { + // 1) 루트 ID별로 최상위 매핑 초기화 + Map idToRoot = new HashMap<>(); + for (Long rootId : rootCommentIds) { + idToRoot.put(rootId, rootId); // 초기화 + } + + // 2) 결과 맵 초기화 + Map> resultMap = new HashMap<>(); + for (Long rootId : rootCommentIds) { + resultMap.put(rootId, new ArrayList<>()); + } + + // 3) 단계별 조회용 parentIds 초기화 + Set 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 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 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; + } } From 8f4651d507795f70bafebe008709281ccfb527cf Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 8 Aug 2025 17:31:57 +0900 Subject: [PATCH 19/23] =?UTF-8?q?[refactor]=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - adapter 로부터 정렬된 value를 가지는 Map 구조를 반환받아 이후 로직을 이어나가도록 수정 --- .../service/CommentShowAllService.java | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java b/src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java index d374627c0..daad5408f 100644 --- a/src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java +++ b/src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java @@ -13,6 +13,7 @@ import org.springframework.stereotype.Service; import java.util.*; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -31,21 +32,15 @@ public CommentForSinglePostResponse showAllCommentsOfPost(CommentShowAllQuery qu CursorBasedList commentQueryDtoCursorBasedList = commentQueryPort.findLatestRootCommentsWithDeleted(query.postId(), cursor); List rootsInOrder = commentQueryDtoCursorBasedList.contents(); - // 2. 조회한 루트 댓글의 전체 active 답글들 작성순 조회 -> map 구조로 저장 - Set allCommentIds = new HashSet<>(); // 반환할 모든 댓글들의 id set - Map> childrenMap = new HashMap<>(); - for (CommentQueryDto root : rootsInOrder) { - List allActiveChildrenInOrder = commentQueryPort.findAllActiveChildrenComments(root.commentId()); - - childrenMap.put(root.commentId(), allActiveChildrenInOrder); - - allCommentIds.add(root.commentId()); - allActiveChildrenInOrder.stream() - .map(CommentQueryDto::commentId) - .forEach(allCommentIds::add); - } + // 2. 조회한 루트 댓글들의 전체 자식 댓귿들을(깊이 무관) 작성 시간순으로 조회 + Set rootCommentIds = rootsInOrder.stream() + .map(CommentQueryDto::commentId) + .collect(Collectors.toUnmodifiableSet()); + Map> childrenMap = commentQueryPort.findAllActiveChildCommentsOldestFirst(rootCommentIds); + // 3. 반환할 모든 댓글(루트 + 자식 모두 포함) 중 유저가 좋아한 댓글 조회 + Set allCommentIds = parseAllCommentIds(childrenMap); Set likedCommentIds = commentLikeQueryPort.findCommentIdsLikedByUser(allCommentIds, query.userId()); // 4. response 매핑 @@ -58,6 +53,16 @@ public CommentForSinglePostResponse showAllCommentsOfPost(CommentShowAllQuery qu ); } + private Set parseAllCommentIds(Map> childrenMap) { + Set allCommentIds = new HashSet<>(childrenMap.keySet()); // 루트 댓글들 + for (Long rootCommentId : childrenMap.keySet()) { + childrenMap.get(rootCommentId).stream() + .map(CommentQueryDto::commentId) + .forEach(allCommentIds::add); + } + return allCommentIds; + } + private List buildRootCommentResponses( List roots, Map> childrenMap, From c4f90f80c269642220853aff09fc3456158128f9 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 8 Aug 2025 22:37:32 +0900 Subject: [PATCH 20/23] =?UTF-8?q?[refactor]=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20PostType=20=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EB=A1=9C=20=EC=9A=94=EC=B2=AD=EB=B0=9B?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - QueryDSL 코드까지 postTypeStr을 전달 - 이를 받아 필터링하도록 QueryDSL 코드 수정 --- .../out/persistence/CommentQueryPersistenceAdapter.java | 4 ++-- .../persistence/repository/CommentQueryRepository.java | 2 +- .../repository/CommentQueryRepositoryImpl.java | 3 ++- .../application/port/in/dto/CommentShowAllQuery.java | 8 +++++--- .../comment/application/port/out/CommentQueryPort.java | 2 +- .../application/service/CommentShowAllService.java | 2 +- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java index 8d305dbbd..a4549ad20 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentQueryPersistenceAdapter.java @@ -22,11 +22,11 @@ public class CommentQueryPersistenceAdapter implements CommentQueryPort { private final CommentMapper commentMapper; @Override - public CursorBasedList findLatestRootCommentsWithDeleted(Long postId, Cursor cursor) { + public CursorBasedList findLatestRootCommentsWithDeleted(Long postId, String postTypeStr, Cursor cursor) { LocalDateTime lastCreatedAt = cursor.isFirstRequest() ? null : cursor.getLocalDateTime(0); int size = cursor.getPageSize(); - List commentQueryDtos = commentJpaRepository.findRootCommentsWithDeletedByCreatedAtDesc(postId, lastCreatedAt, size); + List commentQueryDtos = commentJpaRepository.findRootCommentsWithDeletedByCreatedAtDesc(postId, postTypeStr, lastCreatedAt, size); return CursorBasedList.of(commentQueryDtos, size, commentQueryDto -> { Cursor nextCursor = new Cursor(List.of(commentQueryDto.createdAt().toString())); diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java index 0b3f1d256..fe8b9b492 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepository.java @@ -9,7 +9,7 @@ public interface CommentQueryRepository { - List findRootCommentsWithDeletedByCreatedAtDesc(Long postId, LocalDateTime lastCreatedAt, int size); + List findRootCommentsWithDeletedByCreatedAtDesc(Long postId, String postTypeStr, LocalDateTime lastCreatedAt, int size); List findAllActiveChildCommentsByCreatedAtAsc(Long rootCommentId); diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java index 04e889911..b49144e73 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentQueryRepositoryImpl.java @@ -29,7 +29,7 @@ public class CommentQueryRepositoryImpl implements CommentQueryRepository { private final QUserJpaEntity parentCommentCreator = new QUserJpaEntity("parentCommentCreator"); @Override - public List findRootCommentsWithDeletedByCreatedAtDesc(Long postId, LocalDateTime lastCreatedAt, int size) { + public List findRootCommentsWithDeletedByCreatedAtDesc(Long postId, String postTypeStr, LocalDateTime lastCreatedAt, int size) { // 최상위 댓글(size+1) 프로젝션 생성 QCommentQueryDto proj = new QCommentQueryDto( comment.commentId, @@ -46,6 +46,7 @@ public List findRootCommentsWithDeletedByCreatedAtDesc(Long pos // WHERE 절 분리 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) diff --git a/src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java b/src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java index 57b75cb3a..3e860ef0f 100644 --- a/src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java +++ b/src/main/java/konkuk/thip/comment/application/port/in/dto/CommentShowAllQuery.java @@ -1,13 +1,15 @@ package konkuk.thip.comment.application.port.in.dto; +import konkuk.thip.common.post.PostType; + public record CommentShowAllQuery( Long postId, Long userId, - String postType, + PostType postType, String cursorStr ) { - public static CommentShowAllQuery of(Long postId, Long userId, String postType, String cursorStr) { - return new CommentShowAllQuery(postId, userId, postType, cursorStr); + public static CommentShowAllQuery of(Long postId, Long userId, String postTypeStr, String cursorStr) { + return new CommentShowAllQuery(postId, userId, PostType.from(postTypeStr), cursorStr); // 내부에서 PostType string 값 유효성 검증 } } diff --git a/src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java b/src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java index 69fc24818..770e92a32 100644 --- a/src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java +++ b/src/main/java/konkuk/thip/comment/application/port/out/CommentQueryPort.java @@ -10,7 +10,7 @@ public interface CommentQueryPort { - CursorBasedList findLatestRootCommentsWithDeleted(Long postId, Cursor cursor); + CursorBasedList findLatestRootCommentsWithDeleted(Long postId, String postTypeStr, Cursor cursor); List findAllActiveChildCommentsOldestFirst(Long rootCommentId); diff --git a/src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java b/src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java index daad5408f..7c946c167 100644 --- a/src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java +++ b/src/main/java/konkuk/thip/comment/application/service/CommentShowAllService.java @@ -29,7 +29,7 @@ public CommentForSinglePostResponse showAllCommentsOfPost(CommentShowAllQuery qu Cursor cursor = Cursor.from(query.cursorStr(), PAGE_SIZE); // 1. size 크기만큼의 루트 댓글 최신순 조회 -> 삭제된 루트 댓글 포함해서 전부 조회 - CursorBasedList commentQueryDtoCursorBasedList = commentQueryPort.findLatestRootCommentsWithDeleted(query.postId(), cursor); + CursorBasedList commentQueryDtoCursorBasedList = commentQueryPort.findLatestRootCommentsWithDeleted(query.postId(), query.postType().getType(), cursor); List rootsInOrder = commentQueryDtoCursorBasedList.contents(); // 2. 조회한 루트 댓글들의 전체 자식 댓귿들을(깊이 무관) 작성 시간순으로 조회 From a4ec647f2eab80b51ecd4f901bd8085b3f5570db Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 8 Aug 2025 22:50:05 +0900 Subject: [PATCH 21/23] =?UTF-8?q?[refactor]=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20api=20controller=20=EC=97=90=EC=84=9C=20Po?= =?UTF-8?q?stType=20=EC=9D=84=20=ED=95=84=EC=88=98=20request=20param=20?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=A7=80=EC=A0=95=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/comment/adapter/in/web/CommentQueryController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java b/src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java index f5bbb2107..586d113c6 100644 --- a/src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java +++ b/src/main/java/konkuk/thip/comment/adapter/in/web/CommentQueryController.java @@ -31,7 +31,7 @@ public BaseResponse showAllCommentsOfPost( @Parameter(description = "댓글을 조회할 게시글(= FEED, RECORD, VOTE)의 id값") @PathVariable("postId") final Long postId, @Parameter(description = "게시물 타입 (RECORD, VOTE, FEED)", example = "RECORD") - @RequestParam(value = "postType", required = false) final String postType, // 이거 우짤지 ?? + @RequestParam(value = "postType") final String postType, @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)") @RequestParam(value = "cursor", required = false) final String cursor) { return BaseResponse.ok(commentShowAllUseCase.showAllCommentsOfPost( From f0b30577fb600cad2678d18a6529e1d290e82c33 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 8 Aug 2025 22:50:51 +0900 Subject: [PATCH 22/23] =?UTF-8?q?[refactor]=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20api=20=ED=86=B5=ED=95=A9=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api 테스트 호출 시 request param 에 postType 전부 추가 --- .../adapter/in/web/CommentShowAllApiTest.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java index d1acbe0cf..e254d4187 100644 --- a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java +++ b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllApiTest.java @@ -49,6 +49,8 @@ class CommentShowAllApiTest { @Autowired private CommentLikeJpaRepository commentLikeJpaRepository; @Autowired private JdbcTemplate jdbcTemplate; + private static final String FEED_POST_TYPE = PostType.FEED.getType(); + @AfterEach void tearDown() { commentLikeJpaRepository.deleteAllInBatch(); @@ -92,7 +94,8 @@ void comment_show_all_test() throws Exception { //when //then mockMvc.perform(get("/comments/{postId}", f1.getPostId().intValue()) - .requestAttr("userId", me.getUserId())) + .requestAttr("userId", me.getUserId()) + .param("postType", FEED_POST_TYPE)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.commentList", hasSize(1))) /** @@ -160,7 +163,8 @@ void comment_show_all_ordering_test() throws Exception { //when //then mockMvc.perform(get("/comments/{postId}", f1.getPostId().intValue()) - .requestAttr("userId", me.getUserId())) + .requestAttr("userId", me.getUserId()) + .param("postType", FEED_POST_TYPE)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.commentList", hasSize(2))) /** @@ -225,7 +229,8 @@ void comment_show_all_deleted_root_comment_test() throws Exception { //when //then mockMvc.perform(get("/comments/{postId}", f1.getPostId().intValue()) - .requestAttr("userId", me.getUserId())) + .requestAttr("userId", me.getUserId()) + .param("postType", FEED_POST_TYPE)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.commentList", hasSize(1))) // comment1 만 조회된다 /** @@ -317,7 +322,8 @@ void comment_show_all_page_test() throws Exception { //when //then MvcResult firstResult = mockMvc.perform(get("/comments/{postId}", f1.getPostId().intValue()) - .requestAttr("userId", me.getUserId())) + .requestAttr("userId", me.getUserId()) + .param("postType", FEED_POST_TYPE)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.nextCursor", notNullValue())) .andExpect(jsonPath("$.data.isLast", is(false))) @@ -344,6 +350,7 @@ void comment_show_all_page_test() throws Exception { mockMvc.perform(get("/comments/{postId}", f1.getPostId().intValue()) .requestAttr("userId", me.getUserId()) + .param("postType", FEED_POST_TYPE) .param("cursor", nextCursor)) // 2페이지 요청 .andExpect(status().isOk()) From 720b0f16abae516f7b22db1d4bbdf121cb781c13 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 8 Aug 2025 22:51:06 +0900 Subject: [PATCH 23/23] =?UTF-8?q?[test]=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20api=20controller=20=EB=8B=A8=EC=9C=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/CommentShowAllControllerTest.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllControllerTest.java diff --git a/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllControllerTest.java b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllControllerTest.java new file mode 100644 index 000000000..c77c1c4fe --- /dev/null +++ b/src/test/java/konkuk/thip/comment/adapter/in/web/CommentShowAllControllerTest.java @@ -0,0 +1,40 @@ +package konkuk.thip.comment.adapter.in.web; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import static konkuk.thip.common.exception.code.ErrorCode.POST_TYPE_NOT_MATCH; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[단위] 댓글 조회 api controller 단위 테스트") +class CommentShowAllControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + @DisplayName("댓글 조회 api 요청 시 request param의 PostType 값이 유효하지 않는 경우, 400 에러가 발생한다.") + void comment_show_all_post_type_invalid() throws Exception { + //given + String invalidPostType = "invalidPostType"; + + //when //then + mockMvc.perform(get("/comments/{postId}", 1L) + .requestAttr("userId", 1L) + .param("postType", invalidPostType)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(POST_TYPE_NOT_MATCH.getCode())) + .andExpect(jsonPath("$.message", containsString(POST_TYPE_NOT_MATCH.getMessage()))); + } +}