From cf502b0173c9d30232bd60674466e6807aa721f6 Mon Sep 17 00:00:00 2001 From: kkw610 Date: Fri, 29 May 2026 22:14:32 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=EB=B0=8F=20=EC=82=AD=EC=A0=9C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/QuestionController.java | 23 ++++++++++ .../domain/question/dto/QuestionReqDTO.java | 7 +++ .../domain/question/dto/QuestionResDTO.java | 9 ++++ .../question/entity/QuestionComment.java | 12 +++++ .../exception/code/QuestionSuccessCode.java | 2 + .../question/service/QuestionService.java | 46 +++++++++++++++++++ 6 files changed, 99 insertions(+) diff --git a/backend/src/main/java/com/example/Piroin/project/domain/question/controller/QuestionController.java b/backend/src/main/java/com/example/Piroin/project/domain/question/controller/QuestionController.java index ad60114..543f41d 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/question/controller/QuestionController.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/question/controller/QuestionController.java @@ -119,6 +119,29 @@ public ResponseEntity> updateQuestio questionService.updateQuestionStatus(questionId, userId)); } + // 댓글 수정 + // PATCH /api/comments/{commentId} + @PatchMapping("/api/comments/{commentId}") + public ResponseEntity> updateComment( + @PathVariable Long commentId, + @RequestBody QuestionReqDTO.CommentUpdateReq request, + @AuthenticationPrincipal Long userId + ) { + return ResponseUtil.success(QuestionSuccessCode.COMMENT_UPDATED, + questionService.updateComment(commentId, request, userId)); + } + + // 댓글 삭제 + // DELETE /api/comments/{commentId} + @DeleteMapping("/api/comments/{commentId}") + public ResponseEntity> deleteComment( + @PathVariable Long commentId, + @AuthenticationPrincipal Long userId + ) { + return ResponseUtil.success(QuestionSuccessCode.COMMENT_DELETED, + questionService.deleteComment(commentId, userId)); + } + // 이해도 체크 생성 // POST /api/sessions/{sessionId}/understanding-checks @PostMapping("/api/sessions/{sessionId}/understanding-checks") diff --git a/backend/src/main/java/com/example/Piroin/project/domain/question/dto/QuestionReqDTO.java b/backend/src/main/java/com/example/Piroin/project/domain/question/dto/QuestionReqDTO.java index 5cedbae..c49ab0c 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/question/dto/QuestionReqDTO.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/question/dto/QuestionReqDTO.java @@ -31,6 +31,13 @@ public static class CommentReq { private Long parentCommentId; // 대댓글일 때만 값이 있음, 일반 댓글이면 null } + // 댓글 수정 요청 + @Getter + @NoArgsConstructor + public static class CommentUpdateReq { + private String content; + } + // 이해도 체크 응답 요청 @Getter @NoArgsConstructor diff --git a/backend/src/main/java/com/example/Piroin/project/domain/question/dto/QuestionResDTO.java b/backend/src/main/java/com/example/Piroin/project/domain/question/dto/QuestionResDTO.java index bbd774b..f0ef336 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/question/dto/QuestionResDTO.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/question/dto/QuestionResDTO.java @@ -41,6 +41,15 @@ public record CommentCreateRes( ) { } + // 댓글 수정/삭제 응답 + public record CommentUpdateDeleteRes( + Long commentId, + String content, + LocalDateTime updatedAt, + LocalDateTime deletedAt + ) { + } + // 좋아요 토글 응답 // isLiked: true면 좋아요 추가된 상태, false면 취소된 상태 public record LikeRes( diff --git a/backend/src/main/java/com/example/Piroin/project/domain/question/entity/QuestionComment.java b/backend/src/main/java/com/example/Piroin/project/domain/question/entity/QuestionComment.java index 0cde7c0..c173dd9 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/question/entity/QuestionComment.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/question/entity/QuestionComment.java @@ -50,4 +50,16 @@ public class QuestionComment { @Column(name = "deleted_at") private LocalDateTime deletedAt; + + // 댓글 내용 수정 + public void updateContent(String content) { + this.content = content; + this.updatedAt = LocalDateTime.now(); + } + + // 댓글 소프트 삭제 + public void softDelete() { + this.deletedAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/example/Piroin/project/domain/question/exception/code/QuestionSuccessCode.java b/backend/src/main/java/com/example/Piroin/project/domain/question/exception/code/QuestionSuccessCode.java index b58a2ca..cf90b2d 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/question/exception/code/QuestionSuccessCode.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/question/exception/code/QuestionSuccessCode.java @@ -18,6 +18,8 @@ public enum QuestionSuccessCode implements BaseCode { QUESTION_STATUS_UPDATED(HttpStatus.OK, "QUESTION200_7", "질문 상태가 변경되었습니다."), QUESTION_CREATED(HttpStatus.CREATED, "QUESTION201_1", "질문이 등록되었습니다."), COMMENT_CREATED(HttpStatus.CREATED, "QUESTION201_2", "댓글이 등록되었습니다."), + COMMENT_UPDATED(HttpStatus.OK, "QUESTION200_8", "댓글이 수정되었습니다."), + COMMENT_DELETED(HttpStatus.OK, "QUESTION200_9", "댓글이 삭제되었습니다."), UNDERSTANDING_CHECK_CREATED(HttpStatus.CREATED, "QUESTION201_3", "이해도 체크가 생성되었습니다."); private final HttpStatus status; diff --git a/backend/src/main/java/com/example/Piroin/project/domain/question/service/QuestionService.java b/backend/src/main/java/com/example/Piroin/project/domain/question/service/QuestionService.java index 3a4188f..5770e77 100644 --- a/backend/src/main/java/com/example/Piroin/project/domain/question/service/QuestionService.java +++ b/backend/src/main/java/com/example/Piroin/project/domain/question/service/QuestionService.java @@ -298,6 +298,40 @@ public QuestionResDTO.UpdateDeleteRes deleteQuestion(Long questionId, Long userI ); } + // 댓글 수정 + @Transactional + public QuestionResDTO.CommentUpdateDeleteRes updateComment( + Long commentId, + QuestionReqDTO.CommentUpdateReq request, + Long userId + ) { + User loginUser = findLoginUser(userId); + QuestionComment comment = findComment(commentId); + validateCommentOwner(comment, loginUser); + + comment.updateContent(request.getContent()); + + return new QuestionResDTO.CommentUpdateDeleteRes( + comment.getId(), comment.getContent(), + comment.getUpdatedAt(), comment.getDeletedAt() + ); + } + + // 댓글 삭제 + @Transactional + public QuestionResDTO.CommentUpdateDeleteRes deleteComment(Long commentId, Long userId) { + User loginUser = findLoginUser(userId); + QuestionComment comment = findComment(commentId); + validateCommentOwner(comment, loginUser); + + comment.softDelete(); + + return new QuestionResDTO.CommentUpdateDeleteRes( + comment.getId(), comment.getContent(), + comment.getUpdatedAt(), comment.getDeletedAt() + ); + } + // 질문 상태 완료 전환 // PATCH /api/questions/{questionId}/status @Transactional @@ -404,6 +438,18 @@ private void validateQuestionOwner(Question question, User loginUser) { } } + private void validateCommentOwner(QuestionComment comment, User loginUser) { + if (!comment.getUser().getId().equals(loginUser.getId())) { + throw new QuestionException(HttpStatus.FORBIDDEN, "본인의 댓글만 수정/삭제할 수 있습니다."); + } + } + + private QuestionComment findComment(Long commentId) { + return questionCommentRepository.findById(commentId) + .filter(c -> c.getDeletedAt() == null) + .orElseThrow(() -> new QuestionException(HttpStatus.NOT_FOUND, "댓글을 찾을 수 없습니다.")); + } + private UnderstandResChoice applyUnderstandingResponse( UnderstandingCheck check, User loginUser, UnderstandResChoice requestedChoice ) {