From e69419ccf2c6a055390d7f19479980152d3bc9fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=9A=B0=EC=B0=BD?= Date: Mon, 31 Jul 2023 16:16:46 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: (#130) 댓글 수정 기능 구현 * feat: (#130) 댓글 수정 로직 구현 * feat: (#130) 댓글 수정 API 구현 * chore: (#130) 코드 컨벤션 정리 * refactor: (#130) 테스트명 오타 수정 --- .../controller/PostCommentController.java | 32 +++++ .../dto/request/CommentUpdateRequest.java | 12 ++ .../domain/post/entity/comment/Comment.java | 8 ++ .../post/service/PostCommentService.java | 20 ++- .../controller/PostCommentControllerTest.java | 76 ++++++++++ .../post/entity/comment/CommentTest.java | 25 ++++ .../post/service/PostCommentServiceTest.java | 132 +++++++++++++++++- 7 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 backend/src/main/java/com/votogether/domain/post/dto/request/CommentUpdateRequest.java diff --git a/backend/src/main/java/com/votogether/domain/post/controller/PostCommentController.java b/backend/src/main/java/com/votogether/domain/post/controller/PostCommentController.java index e788a1d05..45201f392 100644 --- a/backend/src/main/java/com/votogether/domain/post/controller/PostCommentController.java +++ b/backend/src/main/java/com/votogether/domain/post/controller/PostCommentController.java @@ -2,6 +2,7 @@ import com.votogether.domain.member.entity.Member; import com.votogether.domain.post.dto.request.CommentRegisterRequest; +import com.votogether.domain.post.dto.request.CommentUpdateRequest; import com.votogether.domain.post.service.PostCommentService; import com.votogether.exception.ExceptionResponse; import com.votogether.global.jwt.Auth; @@ -19,6 +20,7 @@ import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -50,9 +52,39 @@ public ResponseEntity createComment( return ResponseEntity.status(HttpStatus.CREATED).build(); } + @Operation(summary = "게시글 댓글 수정", description = "게시글 댓글을 수정한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "게시글 댓글 수정 성공"), + @ApiResponse( + responseCode = "400", + description = "게시글에 속하지 않은 댓글, 올바르지 않은 댓글 작성자", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "존재하지 않는 게시글, 존재하지 않는 댓글", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + @PutMapping("/{postId}/comments/{commentId}") + public ResponseEntity updateComment( + @PathVariable @Parameter(description = "게시글 ID") final Long postId, + @PathVariable @Parameter(description = "댓글 ID") final Long commentId, + @RequestBody @Valid final CommentUpdateRequest commentUpdateRequest, + @Auth final Member member + ) { + postCommentService.updateComment(postId, commentId, commentUpdateRequest, member); + return ResponseEntity.ok().build(); + } + @Operation(summary = "게시글 댓글 삭제", description = "게시글 댓글을 삭제한다.") @ApiResponses({ @ApiResponse(responseCode = "204", description = "게시글 댓글 삭제 성공"), + @ApiResponse( + responseCode = "400", + description = "게시글에 속하지 않은 댓글, 올바르지 않은 댓글 작성자", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), @ApiResponse( responseCode = "404", description = "존재하지 않는 게시글, 존재하지 않는 댓글", diff --git a/backend/src/main/java/com/votogether/domain/post/dto/request/CommentUpdateRequest.java b/backend/src/main/java/com/votogether/domain/post/dto/request/CommentUpdateRequest.java new file mode 100644 index 000000000..c201653ab --- /dev/null +++ b/backend/src/main/java/com/votogether/domain/post/dto/request/CommentUpdateRequest.java @@ -0,0 +1,12 @@ +package com.votogether.domain.post.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "게시글 수정 요청") +public record CommentUpdateRequest( + @Schema(description = "댓글 수정 내용", example = "content") + @NotBlank(message = "댓글 내용은 존재해야 합니다.") + String content +) { +} diff --git a/backend/src/main/java/com/votogether/domain/post/entity/comment/Comment.java b/backend/src/main/java/com/votogether/domain/post/entity/comment/Comment.java index 94fe03637..8af40c0ab 100644 --- a/backend/src/main/java/com/votogether/domain/post/entity/comment/Comment.java +++ b/backend/src/main/java/com/votogether/domain/post/entity/comment/Comment.java @@ -60,4 +60,12 @@ public void validateBelong(final Post post) { } } + public void updateContent(final String content) { + this.content = new Content(content); + } + + public String getContent() { + return content.getValue(); + } + } diff --git a/backend/src/main/java/com/votogether/domain/post/service/PostCommentService.java b/backend/src/main/java/com/votogether/domain/post/service/PostCommentService.java index a03dd0153..d991d9644 100644 --- a/backend/src/main/java/com/votogether/domain/post/service/PostCommentService.java +++ b/backend/src/main/java/com/votogether/domain/post/service/PostCommentService.java @@ -2,6 +2,7 @@ import com.votogether.domain.member.entity.Member; import com.votogether.domain.post.dto.request.CommentRegisterRequest; +import com.votogether.domain.post.dto.request.CommentUpdateRequest; import com.votogether.domain.post.entity.Post; import com.votogether.domain.post.entity.comment.Comment; import com.votogether.domain.post.exception.CommentExceptionType; @@ -37,6 +38,24 @@ public void createComment( post.addComment(comment); } + @Transactional + public void updateComment( + final Long postId, + final Long commentId, + final CommentUpdateRequest commentUpdateRequest, + final Member member + ) { + final Post post = postRepository.findById(postId) + .orElseThrow(() -> new NotFoundException(PostExceptionType.POST_NOT_FOUND)); + final Comment comment = commentRepository.findById(commentId) + .orElseThrow(() -> new NotFoundException(CommentExceptionType.COMMENT_NOT_FOUND)); + + comment.validateBelong(post); + comment.validateWriter(member); + + comment.updateContent(commentUpdateRequest.content()); + } + @Transactional public void deleteComment(final Long postId, final Long commentId, final Member member) { final Post post = postRepository.findById(postId) @@ -49,5 +68,4 @@ public void deleteComment(final Long postId, final Long commentId, final Member commentRepository.delete(comment); } - } diff --git a/backend/src/test/java/com/votogether/domain/post/controller/PostCommentControllerTest.java b/backend/src/test/java/com/votogether/domain/post/controller/PostCommentControllerTest.java index 9b75fa295..d5295df4d 100644 --- a/backend/src/test/java/com/votogether/domain/post/controller/PostCommentControllerTest.java +++ b/backend/src/test/java/com/votogether/domain/post/controller/PostCommentControllerTest.java @@ -11,6 +11,7 @@ import com.votogether.domain.member.entity.Member; import com.votogether.domain.member.service.MemberService; import com.votogether.domain.post.dto.request.CommentRegisterRequest; +import com.votogether.domain.post.dto.request.CommentUpdateRequest; import com.votogether.domain.post.service.PostCommentService; import com.votogether.fixtures.MemberFixtures; import com.votogether.global.jwt.TokenPayload; @@ -121,6 +122,81 @@ void createComment() throws Exception { } + @Nested + @DisplayName("게시글 댓글 수정") + class UpdateComment { + + @ParameterizedTest + @ValueSource(strings = {"@", "a", "가"}) + @DisplayName("게시글 ID가 Long 타입으로 변환할 수 없는 값이라면 400을 응답한다.") + void invalidPostIDType(String postId) throws Exception { + // given + TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); + given(tokenProcessor.resolveToken(anyString())).willReturn("token"); + given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); + given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get()); + CommentUpdateRequest request = new CommentUpdateRequest("hello"); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .contentType(MediaType.APPLICATION_JSON) + .body(request) + .when().put("/posts/{postId}/comments/{commentId}", postId, 1L) + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(-9998)) + .body("message", containsString("postId는 Long 타입이 필요합니다.")); + } + + @ParameterizedTest + @ValueSource(strings = {"@", "a", "가"}) + @DisplayName("댓글 ID가 Long 타입으로 변환할 수 없는 값이라면 400을 응답한다.") + void invalidCommentIDType(String commentId) throws Exception { + // given + TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); + given(tokenProcessor.resolveToken(anyString())).willReturn("token"); + given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); + given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get()); + CommentUpdateRequest request = new CommentUpdateRequest("hello"); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .contentType(MediaType.APPLICATION_JSON) + .body(request) + .when().put("/posts/{postId}/comments/{commentId}", 1L, commentId) + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(-9998)) + .body("message", containsString("commentId는 Long 타입이 필요합니다.")); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("수정할 댓글 내용이 비어있거나 존재하지 않으면 400을 응답한다.") + void nullAndEmptyCommentContent(String content) throws Exception { + // given + TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); + given(tokenProcessor.resolveToken(anyString())).willReturn("token"); + given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); + given(memberService.findById(anyLong())).willReturn(MemberFixtures.MALE_20.get()); + CommentUpdateRequest request = new CommentUpdateRequest(content); + + // when, then + RestAssuredMockMvc.given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .contentType(MediaType.APPLICATION_JSON) + .body(request) + .when().put("/posts/{postId}/comments/{commentId}", 1L, 1L) + .then().log().all() + .status(HttpStatus.BAD_REQUEST) + .body("code", equalTo(-9997)) + .body("message", containsString("댓글 내용은 존재해야 합니다.")); + } + + } + @Nested @DisplayName("게시글 댓글 삭제") class DeleteComment { diff --git a/backend/src/test/java/com/votogether/domain/post/entity/comment/CommentTest.java b/backend/src/test/java/com/votogether/domain/post/entity/comment/CommentTest.java index 8ebbe2aa3..1ec98ff44 100644 --- a/backend/src/test/java/com/votogether/domain/post/entity/comment/CommentTest.java +++ b/backend/src/test/java/com/votogether/domain/post/entity/comment/CommentTest.java @@ -106,4 +106,29 @@ void invalidPost() { .hasMessage("댓글의 게시글 정보와 일치하지 않습니다."); } + @Test + @DisplayName("댓글 수정 시 최대 글자를 초과하면 예외를 던진다.") + void updateContentWithInvalidContentLength() { + // given + PostBody body = PostBody.builder() + .title("title") + .content("content") + .build(); + Post post = Post.builder() + .writer(MemberFixtures.FEMALE_20.get()) + .postBody(body) + .deadline(LocalDateTime.now()) + .build(); + Comment comment = Comment.builder() + .member(MemberFixtures.MALE_20.get()) + .post(post) + .content("hello") + .build(); + + // when, then + assertThatThrownBy(() -> comment.updateContent("a".repeat(501))) + .isInstanceOf(BadRequestException.class) + .hasMessage("유효하지 않은 댓글 길이입니다."); + } + } diff --git a/backend/src/test/java/com/votogether/domain/post/service/PostCommentServiceTest.java b/backend/src/test/java/com/votogether/domain/post/service/PostCommentServiceTest.java index f6580a61a..9465d8cf5 100644 --- a/backend/src/test/java/com/votogether/domain/post/service/PostCommentServiceTest.java +++ b/backend/src/test/java/com/votogether/domain/post/service/PostCommentServiceTest.java @@ -7,6 +7,7 @@ import com.votogether.domain.member.entity.Member; import com.votogether.domain.member.repository.MemberRepository; import com.votogether.domain.post.dto.request.CommentRegisterRequest; +import com.votogether.domain.post.dto.request.CommentUpdateRequest; import com.votogether.domain.post.entity.Post; import com.votogether.domain.post.entity.PostBody; import com.votogether.domain.post.entity.comment.Comment; @@ -76,6 +77,135 @@ void createComment() { } + @Nested + @DisplayName("게시글 댓글 수정") + class UpdateComment { + + @Test + @DisplayName("존재하지 않는 게시글이라면 예외를 던진다.") + void emptyPost() { + // given + Member member = memberRepository.save(MemberFixtures.MALE_20.get()); + CommentUpdateRequest request = new CommentUpdateRequest("hello"); + + // when, then + assertThatThrownBy(() -> postCommentService.updateComment(-1L, 1L, request, member)) + .isInstanceOf(NotFoundException.class) + .hasMessage("해당 게시글이 존재하지 않습니다."); + } + + @Test + @DisplayName("존재하지 않는 댓글이라면 예외를 던진다.") + void emptyComment() { + // given + Member member = memberRepository.save(MemberFixtures.MALE_20.get()); + Post post = postRepository.save( + Post.builder() + .writer(member) + .postBody(PostBody.builder().title("title").content("content").build()) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) + .build() + ); + CommentUpdateRequest request = new CommentUpdateRequest("hello"); + + // when, then + assertThatThrownBy(() -> postCommentService.updateComment(post.getId(), -1L, request, member)) + .isInstanceOf(NotFoundException.class) + .hasMessage("해당 댓글이 존재하지 않습니다."); + } + + @Test + @DisplayName("댓글의 게시글과 일치하지 않으면 예외를 던진다.") + void invalidBelongPost() { + // given + Member member = memberRepository.save(MemberFixtures.MALE_20.get()); + Post postA = postRepository.save( + Post.builder() + .writer(member) + .postBody(PostBody.builder().title("titleA").content("contentA").build()) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) + .build() + ); + Post postB = postRepository.save( + Post.builder() + .writer(member) + .postBody(PostBody.builder().title("titleB").content("contentB").build()) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) + .build() + ); + Comment comment = commentRepository.save( + Comment.builder() + .member(member) + .post(postA) + .content("comment") + .build() + ); + CommentUpdateRequest request = new CommentUpdateRequest("hello"); + + // when, then + assertThatThrownBy(() -> postCommentService.updateComment(postB.getId(), comment.getId(), request, member)) + .isInstanceOf(BadRequestException.class) + .hasMessage("댓글의 게시글 정보와 일치하지 않습니다."); + } + + @Test + @DisplayName("댓글의 작성자가 아니라면 예외를 던진다.") + void invalidWriter() { + // given + Member memberA = memberRepository.save(MemberFixtures.MALE_20.get()); + Member memberB = memberRepository.save(MemberFixtures.FEMALE_20.get()); + Post post = postRepository.save( + Post.builder() + .writer(memberA) + .postBody(PostBody.builder().title("titleA").content("contentA").build()) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) + .build() + ); + Comment comment = commentRepository.save( + Comment.builder() + .member(memberB) + .post(post) + .content("comment") + .build() + ); + CommentUpdateRequest request = new CommentUpdateRequest("hello"); + + // when, then + assertThatThrownBy(() -> postCommentService.updateComment(post.getId(), comment.getId(), request, memberA)) + .isInstanceOf(BadRequestException.class) + .hasMessage("댓글 작성자가 아닙니다."); + } + + @Test + @DisplayName("게시글의 댓글을 수정한다.") + void deleteComment() { + // given + Member member = memberRepository.save(MemberFixtures.MALE_20.get()); + Post post = postRepository.save( + Post.builder() + .writer(member) + .postBody(PostBody.builder().title("titleA").content("contentA").build()) + .deadline(LocalDateTime.of(2100, 7, 12, 0, 0)) + .build() + ); + Comment comment = commentRepository.save( + Comment.builder() + .member(member) + .post(post) + .content("comment") + .build() + ); + CommentUpdateRequest request = new CommentUpdateRequest("hello"); + + // when + postCommentService.updateComment(post.getId(), comment.getId(), request, member); + + // then + assertThat(comment.getContent()).isEqualTo("hello"); + } + + } + @Nested @DisplayName("게시글 댓글 삭제") class DeleteComment { @@ -145,7 +275,7 @@ void invalidBelongPost() { } @Test - @DisplayName("댓글의 작성자가 아니라면 예외르 던진다.") + @DisplayName("댓글의 작성자가 아니라면 예외를 던진다.") void invalidWriter() { // given Member memberA = memberRepository.save(MemberFixtures.MALE_20.get());