diff --git a/documents/Undabang SQL/Undabang Database DDL.sql b/documents/Undabang SQL/Undabang Database DDL.sql index bd71439d..65809d3e 100644 --- a/documents/Undabang SQL/Undabang Database DDL.sql +++ b/documents/Undabang SQL/Undabang Database DDL.sql @@ -25,6 +25,7 @@ DROP TABLE IF EXISTS notification_types; DROP TABLE IF EXISTS fcm_tokens; DROP TABLE IF EXISTS comment_likes; +DROP TABLE IF EXISTS comment_tags; DROP TABLE IF EXISTS comments; DROP TABLE IF EXISTS feed_likes; DROP TABLE IF EXISTS feed_pictures; @@ -660,6 +661,17 @@ CREATE TABLE comments CONSTRAINT FK_comments_feed FOREIGN KEY (feed_id) REFERENCES feeds (feed_id) ); +CREATE TABLE comment_tags +( + comment_tag_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '댓글 태그 식별자', + comment_id BIGINT NOT NULL, + tagged_member_id CHAR(36) NOT NULL COMMENT '태그된 회원 UUID', + comment_tag_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT FK_comment_tags_comment FOREIGN KEY (comment_id) REFERENCES comments (comment_id), + CONSTRAINT FK_comment_tags_member FOREIGN KEY (tagged_member_id) REFERENCES members (member_id) +); + CREATE TABLE comment_likes ( comment_like_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '피드 댓글 좋아요 식별자', diff --git a/src/main/java/com/project200/undabang/comment/dto/record/TaggedMemberRecord.java b/src/main/java/com/project200/undabang/comment/dto/record/TaggedMemberRecord.java new file mode 100644 index 00000000..f47091db --- /dev/null +++ b/src/main/java/com/project200/undabang/comment/dto/record/TaggedMemberRecord.java @@ -0,0 +1,8 @@ +package com.project200.undabang.comment.dto.record; + +import java.util.UUID; + +public record TaggedMemberRecord( + UUID memberId, + String memberNickname) { +} diff --git a/src/main/java/com/project200/undabang/comment/dto/request/CreateCommentRequest.java b/src/main/java/com/project200/undabang/comment/dto/request/CreateCommentRequest.java index c09654ae..8372edfb 100644 --- a/src/main/java/com/project200/undabang/comment/dto/request/CreateCommentRequest.java +++ b/src/main/java/com/project200/undabang/comment/dto/request/CreateCommentRequest.java @@ -2,7 +2,10 @@ import jakarta.validation.constraints.NotBlank; +import java.util.UUID; + public record CreateCommentRequest( @NotBlank(message = "댓글 내용은 필수입니다.") String content, - Long parentCommentId) { + Long parentCommentId, + UUID taggedMemberId) { } diff --git a/src/main/java/com/project200/undabang/comment/dto/response/CommentResponse.java b/src/main/java/com/project200/undabang/comment/dto/response/CommentResponse.java index 6e6e5247..2ae599d9 100644 --- a/src/main/java/com/project200/undabang/comment/dto/response/CommentResponse.java +++ b/src/main/java/com/project200/undabang/comment/dto/response/CommentResponse.java @@ -1,5 +1,7 @@ package com.project200.undabang.comment.dto.response; +import com.project200.undabang.comment.dto.record.TaggedMemberRecord; + import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -14,5 +16,6 @@ public record CommentResponse( Integer likesCount, Boolean commentIsLiked, LocalDateTime createdAt, + TaggedMemberRecord taggedMember, List children) { } diff --git a/src/main/java/com/project200/undabang/comment/entity/CommentTag.java b/src/main/java/com/project200/undabang/comment/entity/CommentTag.java new file mode 100644 index 00000000..6a9317c1 --- /dev/null +++ b/src/main/java/com/project200/undabang/comment/entity/CommentTag.java @@ -0,0 +1,42 @@ +package com.project200.undabang.comment.entity; + +import com.project200.undabang.member.entity.Member; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.ColumnDefault; + +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Entity +@Table(name = "comment_tags") +public class CommentTag { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "comment_tag_id", nullable = false, updatable = false) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "comment_id", nullable = false, updatable = false) + private Comment comment; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "tagged_member_id", nullable = false, updatable = false) + private Member taggedMember; + + @Builder.Default + @ColumnDefault("CURRENT_TIMESTAMP") + @Column(name = "comment_tag_created_at", nullable = false, updatable = false) + private LocalDateTime createdAt = LocalDateTime.now(); + + public static CommentTag of(Comment comment, Member taggedMember) { + return CommentTag.builder() + .comment(comment) + .taggedMember(taggedMember) + .build(); + } +} diff --git a/src/main/java/com/project200/undabang/comment/repository/CommentTagRepository.java b/src/main/java/com/project200/undabang/comment/repository/CommentTagRepository.java new file mode 100644 index 00000000..f04d43f0 --- /dev/null +++ b/src/main/java/com/project200/undabang/comment/repository/CommentTagRepository.java @@ -0,0 +1,7 @@ +package com.project200.undabang.comment.repository; + +import com.project200.undabang.comment.entity.CommentTag; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentTagRepository extends JpaRepository { +} diff --git a/src/main/java/com/project200/undabang/comment/repository/impl/CommentRepositoryImpl.java b/src/main/java/com/project200/undabang/comment/repository/impl/CommentRepositoryImpl.java index 83ff6b50..4f530127 100644 --- a/src/main/java/com/project200/undabang/comment/repository/impl/CommentRepositoryImpl.java +++ b/src/main/java/com/project200/undabang/comment/repository/impl/CommentRepositoryImpl.java @@ -1,7 +1,9 @@ package com.project200.undabang.comment.repository.impl; +import com.project200.undabang.comment.dto.record.TaggedMemberRecord; import com.project200.undabang.comment.dto.response.CommentResponse; import com.project200.undabang.comment.entity.QComment; +import com.project200.undabang.comment.entity.QCommentTag; import com.project200.undabang.comment.repository.CommentRepositoryCustom; import com.project200.undabang.common.entity.QPicture; import com.project200.undabang.like.entity.QCommentLike; @@ -35,6 +37,8 @@ public class CommentRepositoryImpl implements CommentRepositoryCustom { private final QPicture picture = QPicture.picture; private final QCommentLike commentLike = QCommentLike.commentLike; private final QMemberBlock memberBlock = QMemberBlock.memberBlock; + private final QCommentTag commentTag = QCommentTag.commentTag; + private final QMember taggedMember = new QMember("taggedMember"); @Override public List findCommentsWithChildrenByFeedId(Long feedId, Member currentMember) { @@ -50,11 +54,16 @@ public List findCommentsWithChildrenByFeedId(Long feedId, Membe comment.likesCount, isCommentLikedExpression(currentMember), comment.createdAt, + Projections.constructor(TaggedMemberRecord.class, + taggedMember.memberId, + taggedMember.memberNickname), Expressions.constant(Collections.emptyList()))) .from(comment) .leftJoin(comment.member, member) .leftJoin(member.memberPicture, memberPicture) .leftJoin(memberPicture.picture, picture) + .leftJoin(commentTag).on(commentTag.comment.eq(comment)) + .leftJoin(commentTag.taggedMember, taggedMember) .where( comment.feed.id.eq(feedId), comment.parent.isNull(), @@ -84,11 +93,16 @@ public List findCommentsWithChildrenByFeedId(Long feedId, Membe comment.likesCount, isCommentLikedExpression(currentMember), comment.createdAt, + Projections.constructor(TaggedMemberRecord.class, + taggedMember.memberId, + taggedMember.memberNickname), Expressions.constant(Collections.emptyList()))) .from(comment) .leftJoin(comment.member, member) .leftJoin(member.memberPicture, memberPicture) .leftJoin(memberPicture.picture, picture) + .leftJoin(commentTag).on(commentTag.comment.eq(comment)) + .leftJoin(commentTag.taggedMember, taggedMember) .where( comment.parent.id.in(parentIds), comment.deletedAt.isNull(), @@ -111,6 +125,7 @@ public List findCommentsWithChildrenByFeedId(Long feedId, Membe parent.likesCount(), parent.commentIsLiked(), parent.createdAt(), + parent.taggedMember(), childrenMap.getOrDefault(parent.commentId(), new ArrayList<>()))) .collect(Collectors.toList()); } diff --git a/src/main/java/com/project200/undabang/comment/service/impl/CommentCommandServiceImpl.java b/src/main/java/com/project200/undabang/comment/service/impl/CommentCommandServiceImpl.java index f18ccd08..f6380333 100644 --- a/src/main/java/com/project200/undabang/comment/service/impl/CommentCommandServiceImpl.java +++ b/src/main/java/com/project200/undabang/comment/service/impl/CommentCommandServiceImpl.java @@ -3,7 +3,9 @@ import com.project200.undabang.comment.dto.request.CreateCommentRequest; import com.project200.undabang.comment.dto.response.CreateCommentResponse; import com.project200.undabang.comment.entity.Comment; +import com.project200.undabang.comment.entity.CommentTag; import com.project200.undabang.comment.repository.CommentRepository; +import com.project200.undabang.comment.repository.CommentTagRepository; import com.project200.undabang.comment.service.CommentCommandService; import com.project200.undabang.common.context.UserContextHolder; import com.project200.undabang.common.web.exception.CustomException; @@ -24,6 +26,7 @@ public class CommentCommandServiceImpl implements CommentCommandService { private final CommentRepository commentRepository; + private final CommentTagRepository commentTagRepository; private final FeedRepository feedRepository; private final MemberRepository memberRepository; @@ -49,6 +52,14 @@ public CreateCommentResponse createComment(Long feedId, CreateCommentRequest req Comment comment = Comment.create(member, feed, parentComment, request); Comment savedComment = commentRepository.save(comment); + // 태그된 멤버가 있으면 CommentTag 생성 + if (request.taggedMemberId() != null) { + Member taggedMember = memberRepository + .findByMemberIdAndMemberDeletedAtNull(request.taggedMemberId()) + .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND)); + commentTagRepository.save(CommentTag.of(savedComment, taggedMember)); + } + // 피드 댓글 수 증가 feed.incrementCommentsCount(); diff --git a/src/main/java/com/project200/undabang/common/web/exception/ErrorCode.java b/src/main/java/com/project200/undabang/common/web/exception/ErrorCode.java index 5f89a75f..7cd2ed71 100644 --- a/src/main/java/com/project200/undabang/common/web/exception/ErrorCode.java +++ b/src/main/java/com/project200/undabang/common/web/exception/ErrorCode.java @@ -103,6 +103,9 @@ public enum ErrorCode { COMMENT_DELETE_FORBIDDEN(403, "COMMENT_DELETE_FORBIDDEN", "댓글 삭제 권한이 없습니다."), COMMENT_PARENT_NOT_FOUND(404, "COMMENT_PARENT_NOT_FOUND", "부모 댓글을 찾을 수 없습니다."), + // 댓글 태그 관련 에러 + COMMENT_TAG_NOT_ALLOWED(400, "COMMENT_TAG_NOT_ALLOWED", "대댓글에서만 태그 기능을 사용할 수 있습니다."), + // 피드 관련 에러 FEED_NOT_FOUND(404, "FEED_NOT_FOUND", "존재하지 않는 피드입니다."), diff --git a/src/test/java/com/project200/undabang/comment/controller/CommentCommandControllerTest.java b/src/test/java/com/project200/undabang/comment/controller/CommentCommandControllerTest.java index 2c4c7f42..ed70be2b 100644 --- a/src/test/java/com/project200/undabang/comment/controller/CommentCommandControllerTest.java +++ b/src/test/java/com/project200/undabang/comment/controller/CommentCommandControllerTest.java @@ -47,7 +47,7 @@ void createComment_success() throws Exception { // given UUID testMemberId = UUID.randomUUID(); Long feedId = 1L; - CreateCommentRequest request = new CreateCommentRequest("댓글 내용입니다.", null); + CreateCommentRequest request = new CreateCommentRequest("댓글 내용입니다.", null, null); CreateCommentResponse responseDto = new CreateCommentResponse(1L); BDDMockito @@ -69,7 +69,8 @@ void createComment_success() throws Exception { parameterWithName("feedId").attributes(getTypeFormat(JsonFieldType.NUMBER)).description("댓글을 작성할 피드의 고유 식별자(ID) 입니다.")), requestFields( fieldWithPath("content").type(JsonFieldType.STRING).description("작성할 댓글의 내용입니다."), - fieldWithPath("parentCommentId").type(JsonFieldType.NUMBER).description("해당 댓글이 대댓글인 경우, 부모 댓글의 고유 식별자(ID)입니다.").optional()), + fieldWithPath("parentCommentId").type(JsonFieldType.NUMBER).description("해당 댓글이 대댓글인 경우, 부모 댓글의 고유 식별자(ID)입니다.").optional(), + fieldWithPath("taggedMemberId").type(JsonFieldType.STRING).description("태그할 멤버의 고유 식별자(ID)입니다.").optional()), responseFields(commonResponseFields(fieldWithPath("data.commentId").type(JsonFieldType.NUMBER).description("작성된 댓글의 고유 식별자(ID) 입니다."))))) .andReturn().getResponse().getContentAsString(); @@ -86,7 +87,7 @@ void createReply_success() throws Exception { UUID testMemberId = UUID.randomUUID(); Long feedId = 1L; Long parentCommentId = 1L; - CreateCommentRequest request = new CreateCommentRequest("대댓글 내용입니다.", parentCommentId); + CreateCommentRequest request = new CreateCommentRequest("대댓글 내용입니다.", parentCommentId, null); CreateCommentResponse responseDto = new CreateCommentResponse(2L); BDDMockito @@ -114,7 +115,7 @@ void createComment_Failed_FeedNotFound() throws Exception { // given UUID testMemberId = UUID.randomUUID(); Long feedId = 999L; - CreateCommentRequest request = new CreateCommentRequest("댓글 내용입니다.", null); + CreateCommentRequest request = new CreateCommentRequest("댓글 내용입니다.", null, null); BDDMockito .given(commentCommandService.createComment(BDDMockito.eq(feedId), @@ -141,7 +142,7 @@ void createReply_Failed_ParentNotFound() throws Exception { // given UUID testMemberId = UUID.randomUUID(); Long feedId = 1L; - CreateCommentRequest request = new CreateCommentRequest("대댓글 내용입니다.", 999L); + CreateCommentRequest request = new CreateCommentRequest("대댓글 내용입니다.", 999L, null); BDDMockito .given(commentCommandService.createComment(BDDMockito.eq(feedId), @@ -167,7 +168,7 @@ void createReply_Failed_ParentNotFound() throws Exception { void createComment_Failed_Not_Having_Token() throws Exception { // given Long feedId = 1L; - CreateCommentRequest request = new CreateCommentRequest("댓글 내용입니다.", null); + CreateCommentRequest request = new CreateCommentRequest("댓글 내용입니다.", null, null); // when & then mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/feeds/{feedId}/comments", feedId) @@ -201,7 +202,8 @@ void deleteComment_success() throws Exception { .andDo(document.document( requestHeaders(HEADER_ACCESS_TOKEN), pathParameters( - parameterWithName("commentId").attributes(getTypeFormat(JsonFieldType.NUMBER)) + parameterWithName("commentId").attributes( + getTypeFormat(JsonFieldType.NUMBER)) .description("삭제할 댓글 ID")), responseFields(commonResponseFieldsOnly()))); diff --git a/src/test/java/com/project200/undabang/comment/controller/CommentQueryControllerTest.java b/src/test/java/com/project200/undabang/comment/controller/CommentQueryControllerTest.java index 84495b64..7c4bf956 100644 --- a/src/test/java/com/project200/undabang/comment/controller/CommentQueryControllerTest.java +++ b/src/test/java/com/project200/undabang/comment/controller/CommentQueryControllerTest.java @@ -1,5 +1,6 @@ package com.project200.undabang.comment.controller; +import com.project200.undabang.comment.dto.record.TaggedMemberRecord; import com.project200.undabang.comment.dto.response.CommentResponse; import com.project200.undabang.comment.service.CommentQueryService; import com.project200.undabang.common.web.exception.CustomException; @@ -37,124 +38,197 @@ @DisplayName("CommentQueryController 테스트") class CommentQueryControllerTest extends AbstractRestDocSupport { - @MockitoBean - private CommentQueryService commentQueryService; - - private List createSampleComments() { - CommentResponse reply = new CommentResponse( - 2L, - UUID.randomUUID(), - "대댓글유저", - "http://example.com/reply_profile.jpg", - null, - "대댓글 내용입니다.", - 3, - false, - LocalDateTime.now().minusMinutes(30), - new ArrayList<>()); - - CommentResponse parent = new CommentResponse( - 1L, - UUID.randomUUID(), - "댓글유저", - "http://example.com/profile.jpg", - null, - "부모 댓글 내용입니다.", - 5, - true, - LocalDateTime.now().minusHours(1), - List.of(reply)); - - return List.of(parent); - } - - @Nested - @DisplayName("getComments 메소드는") - class GetComments { - - @Test - @DisplayName("피드의 댓글 목록 조회를 성공한다") - void getComments_success() throws Exception { - // given - UUID testMemberId = UUID.randomUUID(); - Long feedId = 1L; - List comments = createSampleComments(); - - BDDMockito.given(commentQueryService.getComments(feedId)).willReturn(comments); - - // when - String response = mockMvc - .perform(MockMvcRequestBuilders.get("/api/v1/feeds/{feedId}/comments", feedId) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .headers(getCommonApiHeaders(testMemberId))) - .andExpect(status().isOk()) - .andDo(document.document( - requestHeaders(HEADER_ACCESS_TOKEN), - pathParameters( - parameterWithName("feedId").attributes(getTypeFormat(JsonFieldType.NUMBER)).description("댓글을 조회할 피드 ID")), - responseFields(commonResponseFieldsForList( - fieldWithPath("data[].commentId").type(JsonFieldType.NUMBER).description("댓글 ID"), - fieldWithPath("data[].memberId").type(JsonFieldType.STRING).description("댓글 작성자의 회원 고유 식별자(ID) 입니다."), - fieldWithPath("data[].memberNickname").type(JsonFieldType.STRING).description("댓글 작성자의 닉네임입니다."), - fieldWithPath("data[].memberProfileImageUrl").type(JsonFieldType.STRING).description("댓글 작성자의 프로필 이미지 URL입니다.").optional(), - fieldWithPath("data[].memberThumbnailUrl").type(JsonFieldType.STRING).description("댓글 작성자 프로필 썸네일 URL입니다.").optional(), - fieldWithPath("data[].content").type(JsonFieldType.STRING).description("댓글 내용입니다."), - fieldWithPath("data[].likesCount").type(JsonFieldType.NUMBER).description("댓글 좋아요 수 입니다."), - fieldWithPath("data[].commentIsLiked").type(JsonFieldType.BOOLEAN).description("현재 사용자의 댓글 좋아요 여부입니다."), - fieldWithPath("data[].createdAt").type(JsonFieldType.STRING).description("댓글이 작성된 시간입니다."), - fieldWithPath("data[].children").type(JsonFieldType.ARRAY).description("대댓글 목록입니다."), - fieldWithPath("data[].children[].commentId").type(JsonFieldType.NUMBER).description("대댓글 고유 식별자(ID) 입니다."), - fieldWithPath("data[].children[].memberId").type(JsonFieldType.STRING).description("대댓글 작성자 회원 고유 식별자(ID) 입니다."), - fieldWithPath("data[].children[].memberNickname").type(JsonFieldType.STRING).description("대댓글 작성자 닉네임 입니다."), - fieldWithPath("data[].children[].memberProfileImageUrl").type(JsonFieldType.STRING).description("대댓글 작성자 프로필 이미지 URL").optional(), - fieldWithPath("data[].children[].memberThumbnailUrl").type(JsonFieldType.STRING).description("대댓글 작성자 프로필 썸네일 URL").optional(), - fieldWithPath("data[].children[].content").type(JsonFieldType.STRING).description("대댓글 내용"), - fieldWithPath("data[].children[].likesCount").type(JsonFieldType.NUMBER).description("대댓글 좋아요 수"), - fieldWithPath("data[].children[].commentIsLiked").type(JsonFieldType.BOOLEAN).description("현재 사용자의 대댓글 좋아요 여부"), - fieldWithPath("data[].children[].createdAt").type(JsonFieldType.STRING).description("대댓글 작성 시간"), - fieldWithPath("data[].children[].children").type(JsonFieldType.ARRAY).description("대댓글의 대댓글 (현재 미지원)"))))) - .andReturn().getResponse().getContentAsString(); - - // then - CommonResponse> expectedData = CommonResponse.success(comments); - String expected = objectMapper.writeValueAsString(expectedData); - Assertions.assertThat(response).as("응답 본문 검증").isEqualTo(expected); + @MockitoBean + private CommentQueryService commentQueryService; + + private List createSampleComments() { + UUID taggedId = UUID.randomUUID(); + CommentResponse reply = new CommentResponse( + 2L, + UUID.randomUUID(), + "대댓글유저", + "http://example.com/reply_profile.jpg", + null, + "대댓글 내용입니다.", + 3, + false, + LocalDateTime.now().minusMinutes(30), + new TaggedMemberRecord(taggedId, "태그유저"), + new ArrayList<>()); + + CommentResponse parent = new CommentResponse( + 1L, + UUID.randomUUID(), + "댓글유저", + "http://example.com/profile.jpg", + null, + "부모 댓글 내용입니다.", + 5, + true, + LocalDateTime.now().minusHours(1), + null, + List.of(reply)); + + return List.of(parent); } - @Test - @DisplayName("존재하지 않는 피드 ID로 조회 시 실패한다") - void getComments_Failed_FeedNotFound() throws Exception { - // given - UUID testMemberId = UUID.randomUUID(); - Long feedId = 999L; - - BDDMockito.given(commentQueryService.getComments(feedId)) - .willThrow(new CustomException(ErrorCode.FEED_NOT_FOUND)); - - // when - mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/feeds/{feedId}/comments", feedId) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .headers(getCommonApiHeaders(testMemberId))) - .andExpect(status().isNotFound()); - - // then - BDDMockito.then(commentQueryService).should(BDDMockito.times(1)).getComments(feedId); + @Nested + @DisplayName("getComments 메소드는") + class GetComments { + + @Test + @DisplayName("피드의 댓글 목록 조회를 성공한다") + void getComments_success() throws Exception { + // given + UUID testMemberId = UUID.randomUUID(); + Long feedId = 1L; + List comments = createSampleComments(); + + BDDMockito.given(commentQueryService.getComments(feedId)).willReturn(comments); + + // when + String response = mockMvc + .perform(MockMvcRequestBuilders.get("/api/v1/feeds/{feedId}/comments", feedId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .headers(getCommonApiHeaders(testMemberId))) + .andExpect(status().isOk()) + .andDo(document.document( + requestHeaders(HEADER_ACCESS_TOKEN), + pathParameters( + parameterWithName("feedId").attributes( + getTypeFormat(JsonFieldType.NUMBER)) + .description("댓글을 조회할 피드 ID")), + responseFields(commonResponseFieldsForList( + fieldWithPath("data[].commentId") + .type(JsonFieldType.NUMBER) + .description("댓글 ID"), + fieldWithPath("data[].memberId") + .type(JsonFieldType.STRING) + .description("댓글 작성자의 회원 고유 식별자(ID) 입니다."), + fieldWithPath("data[].memberNickname") + .type(JsonFieldType.STRING) + .description("댓글 작성자의 닉네임입니다."), + fieldWithPath("data[].memberProfileImageUrl") + .type(JsonFieldType.STRING) + .description("댓글 작성자의 프로필 이미지 URL입니다.") + .optional(), + fieldWithPath("data[].memberThumbnailUrl") + .type(JsonFieldType.STRING) + .description("댓글 작성자 프로필 썸네일 URL입니다.") + .optional(), + fieldWithPath("data[].content") + .type(JsonFieldType.STRING) + .description("댓글 내용입니다."), + fieldWithPath("data[].likesCount") + .type(JsonFieldType.NUMBER) + .description("댓글 좋아요 수 입니다."), + fieldWithPath("data[].commentIsLiked") + .type(JsonFieldType.BOOLEAN) + .description("현재 사용자의 댓글 좋아요 여부입니다."), + fieldWithPath("data[].createdAt") + .type(JsonFieldType.STRING) + .description("댓글이 작성된 시간입니다."), + fieldWithPath("data[].taggedMember") + .type(JsonFieldType.OBJECT) + .description("태그된 멤버 정보입니다.") + .optional(), + fieldWithPath("data[].taggedMember.memberId") + .type(JsonFieldType.STRING) + .description("태그된 멤버의 고유 식별자(ID) 입니다.") + .optional(), + fieldWithPath("data[].taggedMember.memberNickname") + .type(JsonFieldType.STRING) + .description("태그된 멤버의 닉네임 입니다.") + .optional(), + fieldWithPath("data[].children") + .type(JsonFieldType.ARRAY) + .description("대댓글 목록입니다."), + fieldWithPath("data[].children[].commentId") + .type(JsonFieldType.NUMBER) + .description("대댓글 고유 식별자(ID) 입니다."), + fieldWithPath("data[].children[].memberId") + .type(JsonFieldType.STRING) + .description("대댓글 작성자 회원 고유 식별자(ID) 입니다."), + fieldWithPath("data[].children[].memberNickname") + .type(JsonFieldType.STRING) + .description("대댓글 작성자 닉네임 입니다."), + fieldWithPath("data[].children[].memberProfileImageUrl") + .type(JsonFieldType.STRING) + .description("대댓글 작성자 프로필 이미지 URL") + .optional(), + fieldWithPath("data[].children[].memberThumbnailUrl") + .type(JsonFieldType.STRING) + .description("대댓글 작성자 프로필 썸네일 URL") + .optional(), + fieldWithPath("data[].children[].content") + .type(JsonFieldType.STRING) + .description("대댓글 내용"), + fieldWithPath("data[].children[].likesCount") + .type(JsonFieldType.NUMBER) + .description("대댓글 좋아요 수"), + fieldWithPath("data[].children[].commentIsLiked") + .type(JsonFieldType.BOOLEAN) + .description("현재 사용자의 대댓글 좋아요 여부"), + fieldWithPath("data[].children[].createdAt") + .type(JsonFieldType.STRING) + .description("대댓글 작성 시간"), + fieldWithPath("data[].children[].taggedMember") + .type(JsonFieldType.OBJECT) + .description("대댓글에 태그된 멤버 정보") + .optional(), + fieldWithPath("data[].children[].taggedMember.memberId") + .type(JsonFieldType.STRING) + .description("대댓글에 태그된 멤버 ID") + .optional(), + fieldWithPath("data[].children[].taggedMember.memberNickname") + .type(JsonFieldType.STRING) + .description("대댓글에 태그된 멤버 닉네임") + .optional(), + fieldWithPath("data[].children[].children") + .type(JsonFieldType.ARRAY) + .description("대댓글의 대댓글 (현재 미지원)"))))) + .andReturn().getResponse().getContentAsString(); + + // then + CommonResponse> expectedData = CommonResponse.success(comments); + String expected = objectMapper.writeValueAsString(expectedData); + Assertions.assertThat(response).as("응답 본문 검증").isEqualTo(expected); + } + + @Test + @DisplayName("존재하지 않는 피드 ID로 조회 시 실패한다") + void getComments_Failed_FeedNotFound() throws Exception { + // given + UUID testMemberId = UUID.randomUUID(); + Long feedId = 999L; + + BDDMockito.given(commentQueryService.getComments(feedId)) + .willThrow(new CustomException(ErrorCode.FEED_NOT_FOUND)); + + // when + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/feeds/{feedId}/comments", feedId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .headers(getCommonApiHeaders(testMemberId))) + .andExpect(status().isNotFound()); + + // then + BDDMockito.then(commentQueryService).should(BDDMockito.times(1)).getComments(feedId); + } + + @Test + @DisplayName("Access 토큰이 없는 경우 401 Unauthorized 오류를 반환한다") + void getComments_Failed_Not_Having_Token() throws Exception { + // given + Long feedId = 1L; + + // when & then + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/feeds/{feedId}/comments", feedId) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnauthorized()) + .andDo(print()); + } } - - @Test - @DisplayName("Access 토큰이 없는 경우 401 Unauthorized 오류를 반환한다") - void getComments_Failed_Not_Having_Token() throws Exception { - // given - Long feedId = 1L; - - // when & then - mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/feeds/{feedId}/comments", feedId) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isUnauthorized()) - .andDo(print()); - } - } } diff --git a/src/test/java/com/project200/undabang/comment/service/impl/CommentCommandServiceImplTest.java b/src/test/java/com/project200/undabang/comment/service/impl/CommentCommandServiceImplTest.java index 1fc36994..bf63f112 100644 --- a/src/test/java/com/project200/undabang/comment/service/impl/CommentCommandServiceImplTest.java +++ b/src/test/java/com/project200/undabang/comment/service/impl/CommentCommandServiceImplTest.java @@ -4,6 +4,7 @@ import com.project200.undabang.comment.dto.response.CreateCommentResponse; import com.project200.undabang.comment.entity.Comment; import com.project200.undabang.comment.repository.CommentRepository; +import com.project200.undabang.comment.repository.CommentTagRepository; import com.project200.undabang.common.context.UserContextHolder; import com.project200.undabang.common.web.exception.CustomException; import com.project200.undabang.common.web.exception.ErrorCode; @@ -42,6 +43,9 @@ class CommentCommandServiceImplTest { @Mock private CommentRepository commentRepository; + @Mock + private CommentTagRepository commentTagRepository; + @Mock private FeedRepository feedRepository; @@ -99,7 +103,7 @@ void createsComment_Success() { // given UUID userId = UUID.randomUUID(); Long feedId = 1L; - CreateCommentRequest request = new CreateCommentRequest("새 댓글입니다.", null); + CreateCommentRequest request = new CreateCommentRequest("새 댓글입니다.", null, null); Member member = createMember(userId); Feed feed = createFeed(feedId, member); @@ -127,7 +131,7 @@ void createsReply_Success() { UUID userId = UUID.randomUUID(); Long feedId = 1L; Long parentCommentId = 1L; - CreateCommentRequest request = new CreateCommentRequest("대댓글입니다.", parentCommentId); + CreateCommentRequest request = new CreateCommentRequest("대댓글입니다.", parentCommentId, null); Member member = createMember(userId); Feed feed = createFeed(feedId, member); @@ -157,7 +161,7 @@ void throwsException_WhenFeedNotFound() { // given UUID userId = UUID.randomUUID(); Long feedId = 999L; - CreateCommentRequest request = new CreateCommentRequest("새 댓글입니다.", null); + CreateCommentRequest request = new CreateCommentRequest("새 댓글입니다.", null, null); try (MockedStatic ignored = mockStatic(UserContextHolder.class)) { given(UserContextHolder.getUserId()).willReturn(userId); @@ -177,7 +181,7 @@ void throwsException_WhenParentNotFound() { UUID userId = UUID.randomUUID(); Long feedId = 1L; Long parentCommentId = 999L; - CreateCommentRequest request = new CreateCommentRequest("대댓글입니다.", parentCommentId); + CreateCommentRequest request = new CreateCommentRequest("대댓글입니다.", parentCommentId, null); Member member = createMember(userId); Feed feed = createFeed(feedId, member); diff --git a/src/test/java/com/project200/undabang/comment/service/impl/CommentQueryServiceImplTest.java b/src/test/java/com/project200/undabang/comment/service/impl/CommentQueryServiceImplTest.java index 32768fb4..3d70883d 100644 --- a/src/test/java/com/project200/undabang/comment/service/impl/CommentQueryServiceImplTest.java +++ b/src/test/java/com/project200/undabang/comment/service/impl/CommentQueryServiceImplTest.java @@ -74,6 +74,7 @@ private List createSampleComments() { 3, false, LocalDateTime.now().minusMinutes(30), + null, // taggedMember new ArrayList<>()); CommentResponse parent = new CommentResponse( @@ -86,6 +87,7 @@ private List createSampleComments() { 5, true, LocalDateTime.now().minusHours(1), + null, // taggedMember List.of(reply)); return List.of(parent);