From 799ff0fb2ec7f4ac747c1c3d841d1be5b520b021 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Sat, 4 Oct 2025 16:34:14 +0900 Subject: [PATCH 1/4] =?UTF-8?q?Feat:=20=EB=8C=80=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/CommentController.java | 18 ++++++++ .../board/comment/dto/ReplyResponse.java | 39 ++++++++++++++++++ .../board/comment/service/CommentService.java | 41 +++++++++++++++++++ .../com/back/global/exception/ErrorCode.java | 2 + 4 files changed, 100 insertions(+) create mode 100644 src/main/java/com/back/domain/board/comment/dto/ReplyResponse.java diff --git a/src/main/java/com/back/domain/board/comment/controller/CommentController.java b/src/main/java/com/back/domain/board/comment/controller/CommentController.java index 44a1811d..aa873c85 100644 --- a/src/main/java/com/back/domain/board/comment/controller/CommentController.java +++ b/src/main/java/com/back/domain/board/comment/controller/CommentController.java @@ -3,6 +3,7 @@ import com.back.domain.board.comment.dto.CommentListResponse; import com.back.domain.board.comment.dto.CommentRequest; import com.back.domain.board.comment.dto.CommentResponse; +import com.back.domain.board.comment.dto.ReplyResponse; import com.back.domain.board.common.dto.PageResponse; import com.back.domain.board.comment.service.CommentService; import com.back.global.common.dto.RsData; @@ -86,4 +87,21 @@ public ResponseEntity> deleteComment( null )); } + + // 대댓글 생성 + @PostMapping("/{commentId}/replies") + public ResponseEntity> createReply( + @PathVariable Long postId, + @PathVariable Long commentId, + @RequestBody @Valid CommentRequest request, + @AuthenticationPrincipal CustomUserDetails user + ) { + ReplyResponse response = commentService.createReply(postId, commentId, request, user.getUserId()); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(RsData.success( + "대댓글이 생성되었습니다.", + response + )); + } } diff --git a/src/main/java/com/back/domain/board/comment/dto/ReplyResponse.java b/src/main/java/com/back/domain/board/comment/dto/ReplyResponse.java new file mode 100644 index 00000000..93e720a1 --- /dev/null +++ b/src/main/java/com/back/domain/board/comment/dto/ReplyResponse.java @@ -0,0 +1,39 @@ +package com.back.domain.board.comment.dto; + +import com.back.domain.board.comment.entity.Comment; +import com.back.domain.board.common.dto.AuthorResponse; + +import java.time.LocalDateTime; + +/** + * 대댓글 응답 DTO + * + * @param commentId 댓글 Id + * @param postId 게시글 Id + * @param parentId 부모 댓글 Id + * @param author 작성자 정보 + * @param content 댓글 내용 + * @param createdAt 댓글 생성 일시 + * @param updatedAt 댓글 수정 일시 + */ +public record ReplyResponse( + Long commentId, + Long postId, + Long parentId, + AuthorResponse author, + String content, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { + public static ReplyResponse from(Comment comment) { + return new ReplyResponse( + comment.getId(), + comment.getPost().getId(), + comment.getParent().getId(), + AuthorResponse.from(comment.getUser()), + comment.getContent(), + comment.getCreatedAt(), + comment.getUpdatedAt() + ); + } +} diff --git a/src/main/java/com/back/domain/board/comment/service/CommentService.java b/src/main/java/com/back/domain/board/comment/service/CommentService.java index 129862ab..5bf185f2 100644 --- a/src/main/java/com/back/domain/board/comment/service/CommentService.java +++ b/src/main/java/com/back/domain/board/comment/service/CommentService.java @@ -3,6 +3,7 @@ import com.back.domain.board.comment.dto.CommentListResponse; import com.back.domain.board.comment.dto.CommentRequest; import com.back.domain.board.comment.dto.CommentResponse; +import com.back.domain.board.comment.dto.ReplyResponse; import com.back.domain.board.common.dto.PageResponse; import com.back.domain.board.comment.entity.Comment; import com.back.domain.board.post.entity.Post; @@ -120,4 +121,44 @@ public void deleteComment(Long postId, Long commentId, Long userId) { commentRepository.delete(comment); } + + /** + * 대댓글 생성 서비스 + * 1. User 조회 + * 2. Post 조회 + * 3. 부모 Comment 조회 + * 4. 부모 및 depth 검증 + * 5. 자식 Comment 생성 + * 6. Comment 저장 및 ReplyResponse 반환 + */ + public ReplyResponse createReply(Long postId, Long parentCommentId, CommentRequest request, Long userId) { + // User 조회 + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + // Post 조회 + Post post = postRepository.findById(postId) + .orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND)); + + // 부모 Comment 조회 + Comment parent = commentRepository.findById(parentCommentId) + .orElseThrow(() -> new CustomException(ErrorCode.COMMENT_NOT_FOUND)); + + // 부모의 게시글 일치 검증 + if (!parent.getPost().getId().equals(postId)) { + throw new CustomException(ErrorCode.COMMENT_PARENT_MISMATCH); + } + + // depth 검증: 부모가 이미 대댓글이면 예외 + if (parent.getParent() != null) { + throw new CustomException(ErrorCode.COMMENT_DEPTH_EXCEEDED); + } + + // 자식 Comment 생성 + Comment reply = new Comment(post, user, request.content(), parent); + + // 저장 및 응답 반환 + commentRepository.save(reply); + return ReplyResponse.from(reply); + } } diff --git a/src/main/java/com/back/global/exception/ErrorCode.java b/src/main/java/com/back/global/exception/ErrorCode.java index 67266f4a..36e22fe8 100644 --- a/src/main/java/com/back/global/exception/ErrorCode.java +++ b/src/main/java/com/back/global/exception/ErrorCode.java @@ -88,6 +88,8 @@ public enum ErrorCode { CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "POST_003", "존재하지 않는 카테고리입니다."), COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMENT_001", "존재하지 않는 댓글입니다."), COMMENT_NO_PERMISSION(HttpStatus.FORBIDDEN, "COMMENT_002", "댓글 작성자만 수정/삭제할 수 있습니다."), + COMMENT_PARENT_MISMATCH(HttpStatus.BAD_REQUEST, "COMMENT_003", "부모 댓글이 해당 게시글에 속하지 않습니다."), + COMMENT_DEPTH_EXCEEDED(HttpStatus.BAD_REQUEST, "COMMENT_004", "대댓글은 한 단계까지만 작성할 수 있습니다."), // ======================== 공통 에러 ======================== BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON_400", "잘못된 요청입니다."), From 9ac875b66428fb3a1d941be6ca432ee9903bfea9 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Sat, 4 Oct 2025 16:41:08 +0900 Subject: [PATCH 2/4] =?UTF-8?q?Test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CommentControllerTest.java | 244 ++++++++++++++++++ .../board/service/CommentServiceTest.java | 101 ++++++++ 2 files changed, 345 insertions(+) diff --git a/src/test/java/com/back/domain/board/controller/CommentControllerTest.java b/src/test/java/com/back/domain/board/controller/CommentControllerTest.java index b4b29518..05a1ed4c 100644 --- a/src/test/java/com/back/domain/board/controller/CommentControllerTest.java +++ b/src/test/java/com/back/domain/board/controller/CommentControllerTest.java @@ -24,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; +import java.util.List; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; @@ -581,4 +582,247 @@ void deleteComment_noToken() throws Exception { .andExpect(jsonPath("$.code").value("AUTH_001")) .andExpect(jsonPath("$.message").value("인증이 필요합니다.")); } + + // ====================== 대댓글 생성 테스트 ====================== + + @Test + @DisplayName("대댓글 생성 성공 → 201 Created") + void createReply_success() throws Exception { + // given: 유저 + 게시글 + 부모 댓글 + User user = User.createUser("writer", "writer@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "이몽룡", null, null, LocalDate.of(2000, 1, 1), 1000)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + Post post = new Post(user, "첫 글", "내용"); + postRepository.save(post); + + Comment parent = new Comment(post, user, "부모 댓글", null); + commentRepository.save(parent); + + String accessToken = generateAccessToken(user); + + CommentRequest request = new CommentRequest("저도 동의합니다!"); + + // when + ResultActions resultActions = mvc.perform( + post("/api/posts/{postId}/comments/{commentId}/replies", post.getId(), parent.getId()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ).andDo(print()); + + // then + resultActions + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value("SUCCESS_200")) + .andExpect(jsonPath("$.data.content").value("저도 동의합니다!")) + .andExpect(jsonPath("$.data.author.nickname").value("이몽룡")) + .andExpect(jsonPath("$.data.postId").value(post.getId())) + .andExpect(jsonPath("$.data.parentId").value(parent.getId())); + } + + @Test + @DisplayName("대댓글 생성 실패 - 존재하지 않는 사용자 → 404 Not Found") + void createReply_userNotFound() throws Exception { + // given: 게시글 + 부모 댓글 + User user = User.createUser("writer", "writer@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "이몽룡", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + Post post = new Post(user, "제목", "내용"); + postRepository.save(post); + + Comment parent = new Comment(post, user, "부모 댓글", null); + commentRepository.save(parent); + + // DB에 없는 userId로 JWT 발급 + String fakeToken = testJwtTokenProvider.createAccessToken(999L, "ghost@example.com", "USER"); + + CommentRequest request = new CommentRequest("대댓글 내용"); + + // when & then + mvc.perform(post("/api/posts/{postId}/comments/{commentId}/replies", post.getId(), parent.getId()) + .header("Authorization", "Bearer " + fakeToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value("USER_001")) + .andExpect(jsonPath("$.message").value("존재하지 않는 사용자입니다.")); + } + + @Test + @DisplayName("대댓글 생성 실패 - 존재하지 않는 게시글 → 404 Not Found") + void createReply_postNotFound() throws Exception { + // given + User user = User.createUser("writer", "writer@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "이몽룡", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + Post post = new Post(user, "제목", "내용"); + postRepository.save(post); + + Comment parent = new Comment(post, user, "부모 댓글", null); + commentRepository.save(parent); + + String accessToken = generateAccessToken(user); + CommentRequest request = new CommentRequest("대댓글 내용"); + + // when & then + mvc.perform(post("/api/posts/{postId}/comments/{commentId}/replies", 999L, parent.getId()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value("POST_001")) + .andExpect(jsonPath("$.message").value("존재하지 않는 게시글입니다.")); + } + + @Test + @DisplayName("대댓글 생성 실패 - 존재하지 않는 부모 댓글 → 404 Not Found") + void createReply_commentNotFound() throws Exception { + // given + User user = User.createUser("writer", "writer@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "이몽룡", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + Post post = new Post(user, "제목", "내용"); + postRepository.save(post); + + String accessToken = generateAccessToken(user); + CommentRequest request = new CommentRequest("대댓글 내용"); + + // when & then + mvc.perform(post("/api/posts/{postId}/comments/{commentId}/replies", post.getId(), 999L) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value("COMMENT_001")) + .andExpect(jsonPath("$.message").value("존재하지 않는 댓글입니다.")); + } + + @Test + @DisplayName("대댓글 생성 실패 - 부모 댓글이 다른 게시글에 속함 → 400 Bad Request") + void createReply_parentMismatch() throws Exception { + // given + User user = User.createUser("writer", "writer@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "이몽룡", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + Post post1 = new Post(user, "게시글1", "내용1"); + Post post2 = new Post(user, "게시글2", "내용2"); + postRepository.saveAll(List.of(post1, post2)); + + Comment parent = new Comment(post1, user, "부모 댓글", null); + commentRepository.save(parent); + + String accessToken = generateAccessToken(user); + CommentRequest request = new CommentRequest("대댓글 내용"); + + // when & then + mvc.perform(post("/api/posts/{postId}/comments/{commentId}/replies", post2.getId(), parent.getId()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("COMMENT_003")) + .andExpect(jsonPath("$.message").value("부모 댓글이 해당 게시글에 속하지 않습니다.")); + } + + @Test + @DisplayName("대댓글 생성 실패 - depth 제한 초과(부모가 이미 대댓글) → 400 Bad Request") + void createReply_depthExceeded() throws Exception { + // given + User user = User.createUser("writer", "writer@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "이몽룡", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + Post post = new Post(user, "제목", "내용"); + postRepository.save(post); + + Comment parent = new Comment(post, user, "부모 댓글", null); + Comment child = new Comment(post, user, "대댓글1", parent); + commentRepository.saveAll(List.of(parent, child)); + + String accessToken = generateAccessToken(user); + CommentRequest request = new CommentRequest("대댓글2"); + + // when & then + mvc.perform(post("/api/posts/{postId}/comments/{commentId}/replies", post.getId(), child.getId()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("COMMENT_004")) + .andExpect(jsonPath("$.message").value("대댓글은 한 단계까지만 작성할 수 있습니다.")); + } + + @Test + @DisplayName("대댓글 생성 실패 - 필드 누락(content 없음) → 400 Bad Request") + void createReply_badRequest() throws Exception { + // given + User user = User.createUser("writer", "writer@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "이몽룡", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + Post post = new Post(user, "제목", "내용"); + postRepository.save(post); + + Comment parent = new Comment(post, user, "부모 댓글", null); + commentRepository.save(parent); + + String accessToken = generateAccessToken(user); + + String invalidJson = "{}"; + + // when & then + mvc.perform(post("/api/posts/{postId}/comments/{commentId}/replies", post.getId(), parent.getId()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(invalidJson)) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("COMMON_400")) + .andExpect(jsonPath("$.message").value("잘못된 요청입니다.")); + } + + @Test + @DisplayName("대댓글 생성 실패 - 토큰 없음 → 401 Unauthorized") + void createReply_noToken() throws Exception { + // given + User user = User.createUser("writer", "writer@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "이몽룡", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + Post post = new Post(user, "제목", "내용"); + postRepository.save(post); + + Comment parent = new Comment(post, user, "부모 댓글", null); + commentRepository.save(parent); + + CommentRequest request = new CommentRequest("대댓글 내용"); + + // when & then + mvc.perform(post("/api/posts/{postId}/comments/{commentId}/replies", post.getId(), parent.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value("AUTH_001")) + .andExpect(jsonPath("$.message").value("인증이 필요합니다.")); + } } diff --git a/src/test/java/com/back/domain/board/service/CommentServiceTest.java b/src/test/java/com/back/domain/board/service/CommentServiceTest.java index c4dadec1..94a2c674 100644 --- a/src/test/java/com/back/domain/board/service/CommentServiceTest.java +++ b/src/test/java/com/back/domain/board/service/CommentServiceTest.java @@ -3,6 +3,7 @@ import com.back.domain.board.comment.dto.CommentListResponse; import com.back.domain.board.comment.dto.CommentRequest; import com.back.domain.board.comment.dto.CommentResponse; +import com.back.domain.board.comment.dto.ReplyResponse; import com.back.domain.board.comment.service.CommentService; import com.back.domain.board.common.dto.PageResponse; import com.back.domain.board.comment.entity.Comment; @@ -25,6 +26,8 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + import static org.assertj.core.api.Assertions.*; @SpringBootTest @@ -344,4 +347,102 @@ void deleteComment_fail_noPermission() { ).isInstanceOf(CustomException.class) .hasMessage(ErrorCode.COMMENT_NO_PERMISSION.getMessage()); } + + // ====================== 대댓글 생성 테스트 ====================== + + @Test + @DisplayName("대댓글 생성 성공") + void createReply_success() { + // given: 유저 + 게시글 + 부모 댓글 저장 + User user = User.createUser("writer", "writer@example.com", "encodedPwd"); + user.setUserProfile(new UserProfile(user, "작성자", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + Post post = new Post(user, "제목", "내용"); + postRepository.save(post); + + Comment parent = new Comment(post, user, "부모 댓글", null); + commentRepository.save(parent); + + CommentRequest request = new CommentRequest("대댓글 내용"); + + // when + ReplyResponse response = commentService.createReply(post.getId(), parent.getId(), request, user.getId()); + + // then + assertThat(response.content()).isEqualTo("대댓글 내용"); + assertThat(response.author().nickname()).isEqualTo("작성자"); + assertThat(response.parentId()).isEqualTo(parent.getId()); + assertThat(response.postId()).isEqualTo(post.getId()); + } + + @Test + @DisplayName("대댓글 생성 실패 - 부모 댓글이 다른 게시글에 속함") + void createReply_fail_parentMismatch() { + // given + User user = User.createUser("writer", "writer@example.com", "encodedPwd"); + user.setUserProfile(new UserProfile(user, "작성자", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + Post post1 = new Post(user, "게시글1", "내용1"); + Post post2 = new Post(user, "게시글2", "내용2"); + postRepository.saveAll(List.of(post1, post2)); + + Comment parent = new Comment(post1, user, "부모 댓글", null); + commentRepository.save(parent); + + CommentRequest request = new CommentRequest("대댓글 내용"); + + // when & then + assertThatThrownBy(() -> commentService.createReply(post2.getId(), parent.getId(), request, user.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.COMMENT_PARENT_MISMATCH.getMessage()); + } + + @Test + @DisplayName("대댓글 생성 실패 - 부모 댓글이 이미 대댓글(depth 초과)") + void createReply_fail_depthExceeded() { + // given + User user = User.createUser("writer", "writer@example.com", "encodedPwd"); + user.setUserProfile(new UserProfile(user, "작성자", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + Post post = new Post(user, "제목", "내용"); + postRepository.save(post); + + // 부모 댓글 + 그 부모의 대댓글까지 생성 + Comment parent = new Comment(post, user, "부모 댓글", null); + Comment child = new Comment(post, user, "대댓글1", parent); + commentRepository.saveAll(List.of(parent, child)); + + CommentRequest request = new CommentRequest("대댓글2 내용"); + + // when & then + assertThatThrownBy(() -> commentService.createReply(post.getId(), child.getId(), request, user.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.COMMENT_DEPTH_EXCEEDED.getMessage()); + } + + @Test + @DisplayName("대댓글 생성 실패 - 존재하지 않는 부모 댓글") + void createReply_fail_commentNotFound() { + // given + User user = User.createUser("writer", "writer@example.com", "encodedPwd"); + user.setUserProfile(new UserProfile(user, "작성자", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + Post post = new Post(user, "제목", "내용"); + postRepository.save(post); + + CommentRequest request = new CommentRequest("대댓글 내용"); + + // when & then + assertThatThrownBy(() -> commentService.createReply(post.getId(), 999L, request, user.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.COMMENT_NOT_FOUND.getMessage()); + } } From dfbdef284c5cc9e44d9cdb41ed8ed66222f0fd6f Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Sat, 4 Oct 2025 16:42:34 +0900 Subject: [PATCH 3/4] =?UTF-8?q?Docs:=20Swagger=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CommentControllerDocs.java | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/src/main/java/com/back/domain/board/comment/controller/CommentControllerDocs.java b/src/main/java/com/back/domain/board/comment/controller/CommentControllerDocs.java index 3adb752d..4f50bbf9 100644 --- a/src/main/java/com/back/domain/board/comment/controller/CommentControllerDocs.java +++ b/src/main/java/com/back/domain/board/comment/controller/CommentControllerDocs.java @@ -3,6 +3,7 @@ import com.back.domain.board.comment.dto.CommentListResponse; import com.back.domain.board.comment.dto.CommentRequest; import com.back.domain.board.comment.dto.CommentResponse; +import com.back.domain.board.comment.dto.ReplyResponse; import com.back.domain.board.common.dto.PageResponse; import com.back.global.common.dto.RsData; import com.back.global.security.user.CustomUserDetails; @@ -509,4 +510,157 @@ ResponseEntity> deleteComment( @PathVariable Long commentId, @AuthenticationPrincipal CustomUserDetails user ); + + @Operation( + summary = "대댓글 생성", + description = "로그인한 사용자가 특정 게시글의 댓글에 대댓글을 작성합니다. (대댓글은 1단계까지만 허용됩니다.)" + ) + @ApiResponses({ + @ApiResponse( + responseCode = "201", + description = "대댓글 생성 성공", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": true, + "code": "SUCCESS_200", + "message": "대댓글이 생성되었습니다.", + "data": { + "replyId": 45, + "postId": 101, + "parentId": 25, + "author": { + "id": 7, + "nickname": "이몽룡" + }, + "content": "저도 동의합니다!", + "createdAt": "2025-09-22T13:30:00", + "updatedAt": "2025-09-22T13:30:00" + } + } + """) + ) + ), + @ApiResponse( + responseCode = "400", + description = "잘못된 요청 또는 depth 제한 초과", + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject(name = "필드 누락", value = """ + { + "success": false, + "code": "COMMON_400", + "message": "잘못된 요청입니다.", + "data": null + } + """), + @ExampleObject(name = "부모 댓글 불일치", value = """ + { + "success": false, + "code": "COMMENT_003", + "message": "부모 댓글이 해당 게시글에 속하지 않습니다.", + "data": null + } + """), + @ExampleObject(name = "depth 초과", value = """ + { + "success": false, + "code": "COMMENT_004", + "message": "대댓글은 한 단계까지만 작성할 수 있습니다.", + "data": null + } + """) + } + ) + ), + @ApiResponse( + responseCode = "401", + description = "인증 실패 (토큰 없음/잘못됨/만료)", + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject(name = "토큰 없음", value = """ + { + "success": false, + "code": "AUTH_001", + "message": "인증이 필요합니다.", + "data": null + } + """), + @ExampleObject(name = "잘못된 토큰", value = """ + { + "success": false, + "code": "AUTH_002", + "message": "유효하지 않은 액세스 토큰입니다.", + "data": null + } + """), + @ExampleObject(name = "만료된 토큰", value = """ + { + "success": false, + "code": "AUTH_004", + "message": "만료된 액세스 토큰입니다.", + "data": null + } + """) + } + ) + ), + @ApiResponse( + responseCode = "404", + description = "존재하지 않는 사용자 / 게시글 / 댓글", + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject(name = "존재하지 않는 사용자", value = """ + { + "success": false, + "code": "USER_001", + "message": "존재하지 않는 사용자입니다.", + "data": null + } + """), + @ExampleObject(name = "존재하지 않는 게시글", value = """ + { + "success": false, + "code": "POST_001", + "message": "존재하지 않는 게시글입니다.", + "data": null + } + """), + @ExampleObject(name = "존재하지 않는 댓글", value = """ + { + "success": false, + "code": "COMMENT_001", + "message": "존재하지 않는 댓글입니다.", + "data": null + } + """) + } + ) + ), + @ApiResponse( + responseCode = "500", + description = "서버 내부 오류", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": false, + "code": "COMMON_500", + "message": "서버 오류가 발생했습니다.", + "data": null + } + """) + ) + ) + }) + ResponseEntity> createReply( + @PathVariable Long postId, + @PathVariable Long commentId, + @RequestBody CommentRequest request, + @AuthenticationPrincipal CustomUserDetails user + ); } \ No newline at end of file From e4eea2272516e9614adb4d99293724a8eed45f18 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Sat, 4 Oct 2025 16:57:08 +0900 Subject: [PATCH 4/4] =?UTF-8?q?Test:=20=EB=8C=80=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95/=EC=82=AD=EC=A0=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CommentControllerTest.java | 58 ++++++++++++++++++- .../board/service/CommentServiceTest.java | 53 ++++++++++++++++- 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/back/domain/board/controller/CommentControllerTest.java b/src/test/java/com/back/domain/board/controller/CommentControllerTest.java index 05a1ed4c..1a260a45 100644 --- a/src/test/java/com/back/domain/board/controller/CommentControllerTest.java +++ b/src/test/java/com/back/domain/board/controller/CommentControllerTest.java @@ -583,7 +583,7 @@ void deleteComment_noToken() throws Exception { .andExpect(jsonPath("$.message").value("인증이 필요합니다.")); } - // ====================== 대댓글 생성 테스트 ====================== + // ====================== 대댓글 테스트 ====================== @Test @DisplayName("대댓글 생성 성공 → 201 Created") @@ -825,4 +825,60 @@ void createReply_noToken() throws Exception { .andExpect(jsonPath("$.code").value("AUTH_001")) .andExpect(jsonPath("$.message").value("인증이 필요합니다.")); } + + @Test + @DisplayName("대댓글 수정 성공 → 200 OK") + void updateReply_success() throws Exception { + // given + User user = User.createUser("writer", "writer@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "이몽룡", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + Post post = new Post(user, "제목", "내용"); + postRepository.save(post); + + Comment parent = new Comment(post, user, "부모 댓글", null); + Comment reply = new Comment(post, user, "대댓글", parent); + commentRepository.saveAll(List.of(parent, reply)); + + String accessToken = generateAccessToken(user); + CommentRequest request = new CommentRequest("수정된 대댓글 내용"); + + // when & then + mvc.perform(put("/api/posts/{postId}/comments/{commentId}", post.getId(), reply.getId()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.content").value("수정된 대댓글 내용")); + } + + @Test + @DisplayName("대댓글 삭제 성공 → 200 OK") + void deleteReply_success() throws Exception { + // given + User user = User.createUser("writer", "writer@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "이몽룡", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + Post post = new Post(user, "제목", "내용"); + postRepository.save(post); + + Comment parent = new Comment(post, user, "부모 댓글", null); + Comment reply = new Comment(post, user, "대댓글", parent); + commentRepository.saveAll(List.of(parent, reply)); + + String accessToken = generateAccessToken(user); + + // when & then + mvc.perform(delete("/api/posts/{postId}/comments/{commentId}", post.getId(), reply.getId()) + .header("Authorization", "Bearer " + accessToken)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("댓글이 삭제되었습니다.")); + } } diff --git a/src/test/java/com/back/domain/board/service/CommentServiceTest.java b/src/test/java/com/back/domain/board/service/CommentServiceTest.java index 94a2c674..37a942eb 100644 --- a/src/test/java/com/back/domain/board/service/CommentServiceTest.java +++ b/src/test/java/com/back/domain/board/service/CommentServiceTest.java @@ -348,7 +348,7 @@ void deleteComment_fail_noPermission() { .hasMessage(ErrorCode.COMMENT_NO_PERMISSION.getMessage()); } - // ====================== 대댓글 생성 테스트 ====================== + // ====================== 대댓글 테스트 ====================== @Test @DisplayName("대댓글 생성 성공") @@ -445,4 +445,55 @@ void createReply_fail_commentNotFound() { .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.COMMENT_NOT_FOUND.getMessage()); } + + @Test + @DisplayName("대댓글 수정 성공") + void updateReply_success() { + // given + User user = User.createUser("writer", "writer@example.com", "encodedPwd"); + user.setUserProfile(new UserProfile(user, "작성자", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + Post post = new Post(user, "제목", "내용"); + postRepository.save(post); + + Comment parent = new Comment(post, user, "부모 댓글", null); + Comment reply = new Comment(post, user, "대댓글", parent); + commentRepository.saveAll(List.of(parent, reply)); + + CommentRequest updateRequest = new CommentRequest("수정된 대댓글 내용"); + + // when + CommentResponse updated = commentService.updateComment(post.getId(), reply.getId(), updateRequest, user.getId()); + + // then + assertThat(updated.content()).isEqualTo("수정된 대댓글 내용"); + assertThat(updated.commentId()).isEqualTo(reply.getId()); + assertThat(updated.author().nickname()).isEqualTo("작성자"); + } + + @Test + @DisplayName("대댓글 삭제 성공") + void deleteReply_success() { + // given + User user = User.createUser("writer", "writer@example.com", "encodedPwd"); + user.setUserProfile(new UserProfile(user, "작성자", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + Post post = new Post(user, "제목", "내용"); + postRepository.save(post); + + Comment parent = new Comment(post, user, "부모 댓글", null); + Comment reply = new Comment(post, user, "삭제할 대댓글", parent); + commentRepository.saveAll(List.of(parent, reply)); + + // when + commentService.deleteComment(post.getId(), reply.getId(), user.getId()); + + // then + boolean exists = commentRepository.findById(reply.getId()).isPresent(); + assertThat(exists).isFalse(); + } }