diff --git a/src/main/java/com/renew/sw/mentoring/domain/comment/CommentService.java b/src/main/java/com/renew/sw/mentoring/domain/comment/CommentService.java new file mode 100644 index 0000000..ff3b9b4 --- /dev/null +++ b/src/main/java/com/renew/sw/mentoring/domain/comment/CommentService.java @@ -0,0 +1,105 @@ +package com.renew.sw.mentoring.domain.comment; + +import com.renew.sw.mentoring.domain.comment.exception.CommentNotFoundException; +import com.renew.sw.mentoring.domain.comment.model.CommentStatus; +import com.renew.sw.mentoring.domain.comment.model.dto.response.SummarizedCommentDto; +import com.renew.sw.mentoring.domain.comment.model.dto.response.SummarizedReplyDto; +import com.renew.sw.mentoring.domain.comment.model.entity.Comment; +import com.renew.sw.mentoring.domain.comment.repository.CommentRepository; +import com.renew.sw.mentoring.domain.post.exception.PostNotFoundException; +import com.renew.sw.mentoring.domain.post.model.entity.Post; +import com.renew.sw.mentoring.domain.post.repository.PostRepository; +import com.renew.sw.mentoring.domain.user.exception.UserNotFoundException; +import com.renew.sw.mentoring.domain.user.model.entity.User; +import com.renew.sw.mentoring.domain.user.repository.UserRepository; +import com.renew.sw.mentoring.global.error.exception.NotGrantedException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class CommentService { + + private final PostRepository postRepository; + private final UserRepository userRepository; + private final CommentRepository commentRepository; + + @Transactional(readOnly = true) + public List list(Long postId) { + List comments = commentRepository.findByPostIdExceptReply(postId); + return comments.stream().map(e -> { + List replies = commentRepository.findAllReplies(e.getId()); + return new SummarizedCommentDto(e, replies.stream().map(SummarizedReplyDto::new).toList()); + }).collect(Collectors.toList()); + } + + @Transactional + public Long create(Long postId, Long userId, String content) { + Post post = postRepository.findById(postId).orElseThrow(PostNotFoundException::new); + User user = userRepository.findById(userId).orElseThrow(UserNotFoundException::new); + + Comment comment = Comment.builder() + .user(user) + .post(post) + .content(content) + .commentStatus(CommentStatus.ACTIVE) + .build(); + + comment.changePost(post); + comment = commentRepository.save(comment); + return comment.getId(); + } + + @Transactional + public void edit(Long commentId, Long userId, String content) { + Comment comment = commentRepository.findById(commentId).orElseThrow(CommentNotFoundException::new); + + if(!comment.getUser().getId().equals(userId)) { + throw new NotGrantedException(); + } + if(comment.getCommentStatus() == CommentStatus.ACTIVE + || comment.getCommentStatus() == CommentStatus.EDITED) { + comment.updateContent(content); + } + } + + @Transactional + public void delete(Long commentId, Long userId) { + Comment comment = commentRepository.findById(commentId).orElseThrow(CommentNotFoundException::new); + + if(comment.getUser().getUserRole().isAdmin()) { + comment.markedAsDeleted(true); + } else if(comment.getUser().getId().equals(userId)) { + comment.markedAsDeleted(false); + } else { + throw new NotGrantedException(); + } + } + + @Transactional + public Long createReply(Long commentId, Long userId, String content) { + Comment parentComment = commentRepository.findById(commentId).orElseThrow(CommentNotFoundException::new); + Long postId = parentComment.getPost().getId(); + Post post = postRepository.findById(postId).orElseThrow(PostNotFoundException::new); + User user = userRepository.findById(userId).orElseThrow(UserNotFoundException::new); + + Comment comment = Comment.builder() + .user(user) + .post(post) + .content(content) + .commentStatus(CommentStatus.ACTIVE) + .parentCommentId(parentComment.getId()) + .build(); + + comment.changePost(post); + comment = commentRepository.save(comment); + parentComment.addChildComment(comment); + return comment.getId(); + } +} diff --git a/src/main/java/com/renew/sw/mentoring/domain/comment/exception/CommentNotFoundException.java b/src/main/java/com/renew/sw/mentoring/domain/comment/exception/CommentNotFoundException.java new file mode 100644 index 0000000..4a9f6e9 --- /dev/null +++ b/src/main/java/com/renew/sw/mentoring/domain/comment/exception/CommentNotFoundException.java @@ -0,0 +1,11 @@ +package com.renew.sw.mentoring.domain.comment.exception; + +import com.renew.sw.mentoring.global.error.exception.LocalizedMessageException; +import org.springframework.http.HttpStatus; + +public class CommentNotFoundException extends LocalizedMessageException { + + public CommentNotFoundException() { + super(HttpStatus.NOT_FOUND, "notfound.comment"); + } +} diff --git a/src/main/java/com/renew/sw/mentoring/domain/comment/model/dto/request/RequestCreateCommentDto.java b/src/main/java/com/renew/sw/mentoring/domain/comment/model/dto/request/RequestCreateCommentDto.java new file mode 100644 index 0000000..a599f15 --- /dev/null +++ b/src/main/java/com/renew/sw/mentoring/domain/comment/model/dto/request/RequestCreateCommentDto.java @@ -0,0 +1,19 @@ +package com.renew.sw.mentoring.domain.comment.model.dto.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RequestCreateCommentDto { + + @NotBlank + private String content; + + public RequestCreateCommentDto(String content) { + this.content = content; + } +} diff --git a/src/main/java/com/renew/sw/mentoring/domain/comment/model/dto/response/SummarizedCommentDto.java b/src/main/java/com/renew/sw/mentoring/domain/comment/model/dto/response/SummarizedCommentDto.java new file mode 100644 index 0000000..233ee55 --- /dev/null +++ b/src/main/java/com/renew/sw/mentoring/domain/comment/model/dto/response/SummarizedCommentDto.java @@ -0,0 +1,36 @@ +package com.renew.sw.mentoring.domain.comment.model.dto.response; + +import com.renew.sw.mentoring.domain.comment.model.CommentStatus; +import com.renew.sw.mentoring.domain.comment.model.entity.Comment; +import lombok.Getter; + +import java.util.List; +import java.util.Objects; + +@Getter +public class SummarizedCommentDto { + + private final Long id; + + private final String author; + + private final String content; + + private final List replies; + + public SummarizedCommentDto(Comment comment, List replies) { + this.id = comment.getId(); + if(checkDeletedComment(comment)) { + this.author = null; + this.content = "삭제된 댓글입니다."; + } else { + this.author = comment.getUser().getNickname(); + this.content = comment.getContent(); + } + this.replies = Objects.requireNonNullElseGet(replies, List::of); + } + + private boolean checkDeletedComment(Comment comment) { + return comment.getCommentStatus() == CommentStatus.DELETED || comment.getCommentStatus() == CommentStatus.DELETED_BY_ADMIN; + } +} diff --git a/src/main/java/com/renew/sw/mentoring/domain/comment/model/dto/response/SummarizedReplyDto.java b/src/main/java/com/renew/sw/mentoring/domain/comment/model/dto/response/SummarizedReplyDto.java new file mode 100644 index 0000000..ecc05f8 --- /dev/null +++ b/src/main/java/com/renew/sw/mentoring/domain/comment/model/dto/response/SummarizedReplyDto.java @@ -0,0 +1,30 @@ +package com.renew.sw.mentoring.domain.comment.model.dto.response; + +import com.renew.sw.mentoring.domain.comment.model.CommentStatus; +import com.renew.sw.mentoring.domain.comment.model.entity.Comment; +import lombok.Getter; + +@Getter +public class SummarizedReplyDto { + + private final Long id; + + private final String author; + + private final String content; + + public SummarizedReplyDto(Comment comment) { + this.id = comment.getId(); + if(checkDeletedReply(comment)) { + this.author = null; + this.content = "삭제된 댓글입니다."; + } else { + this.author = comment.getUser().getNickname(); + this.content = comment.getContent(); + } + } + + private boolean checkDeletedReply(Comment comment) { + return comment.getCommentStatus() == CommentStatus.DELETED || comment.getCommentStatus() == CommentStatus.DELETED_BY_ADMIN; + } +} diff --git a/src/main/java/com/renew/sw/mentoring/domain/comment/model/entity/Comment.java b/src/main/java/com/renew/sw/mentoring/domain/comment/model/entity/Comment.java index 3f7c3af..dbb05a1 100644 --- a/src/main/java/com/renew/sw/mentoring/domain/comment/model/entity/Comment.java +++ b/src/main/java/com/renew/sw/mentoring/domain/comment/model/entity/Comment.java @@ -2,15 +2,18 @@ import com.renew.sw.mentoring.domain.comment.model.CommentStatus; import com.renew.sw.mentoring.domain.post.model.entity.Post; +import com.renew.sw.mentoring.domain.post.model.entity.type.MissionBoard; import com.renew.sw.mentoring.domain.user.model.entity.User; import com.renew.sw.mentoring.global.base.BaseEntity; import javax.persistence.*; import javax.validation.constraints.NotNull; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.RequiredArgsConstructor; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +import static javax.persistence.FetchType.LAZY; import static lombok.AccessLevel.PROTECTED; @Entity @@ -30,6 +33,15 @@ public class Comment extends BaseEntity { @JoinColumn(name = "user_id") private User user; + @Setter + @Column(updatable = false) + private Long parentCommentId; + + @ToString.Exclude + @OrderBy("createdAt ASC") + @OneToMany(mappedBy = "parentCommentId", cascade = CascadeType.ALL, fetch = LAZY) + private List childComments = new ArrayList<>(); + @Lob private String content; @@ -40,10 +52,40 @@ public class Comment extends BaseEntity { private Comment(@NotNull Post post, @NotNull User user, String content, - CommentStatus commentStatus) { + CommentStatus commentStatus, + Long parentCommentId) { this.post = post; this.user = user; this.content = content; this.commentStatus = commentStatus; + this.parentCommentId = parentCommentId; + } + + + public void addChildComment(Comment child) { + child.setParentCommentId(this.getId()); + this.getChildComments().add(child); + } + + public void changePost(Post post) { + if (this.post != null) { + this.post.getComments().remove(this); + } + + this.post = post; + this.post.getComments().add(this); + } + + public void updateContent(String content) { + this.content = content; + this.commentStatus = CommentStatus.EDITED; + } + + public void markedAsDeleted(boolean isAdmin) { + if(isAdmin) { + this.commentStatus = CommentStatus.DELETED_BY_ADMIN; + } else { + this.commentStatus = CommentStatus.DELETED; + } } } diff --git a/src/main/java/com/renew/sw/mentoring/domain/comment/repository/CommentRepository.java b/src/main/java/com/renew/sw/mentoring/domain/comment/repository/CommentRepository.java new file mode 100644 index 0000000..86cef93 --- /dev/null +++ b/src/main/java/com/renew/sw/mentoring/domain/comment/repository/CommentRepository.java @@ -0,0 +1,21 @@ +package com.renew.sw.mentoring.domain.comment.repository; + +import com.renew.sw.mentoring.domain.comment.model.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface CommentRepository extends JpaRepository { + @Query("select c from Comment c " + + "join fetch c.post " + + "where c.post.id = :postId and c.parentCommentId is null " + + "order by c.createdAt asc ") + List findByPostIdExceptReply(@Param("postId") Long postId); + + @Query("select c from Comment c " + + "join fetch c.post " + + "where c.parentCommentId=:id order by c.createdAt asc ") + List findAllReplies(Long id); +} diff --git a/src/main/java/com/renew/sw/mentoring/domain/post/controller/MissionBoardController.java b/src/main/java/com/renew/sw/mentoring/domain/post/controller/MissionBoardController.java index 3ee7d45..132683f 100644 --- a/src/main/java/com/renew/sw/mentoring/domain/post/controller/MissionBoardController.java +++ b/src/main/java/com/renew/sw/mentoring/domain/post/controller/MissionBoardController.java @@ -1,5 +1,8 @@ package com.renew.sw.mentoring.domain.post.controller; +import com.renew.sw.mentoring.domain.comment.CommentService; +import com.renew.sw.mentoring.domain.comment.model.dto.request.RequestCreateCommentDto; +import com.renew.sw.mentoring.domain.comment.model.dto.response.SummarizedCommentDto; import com.renew.sw.mentoring.domain.post.model.entity.dto.list.SummarizedMissionBoardDto; import com.renew.sw.mentoring.domain.post.model.entity.dto.request.RequestCreateMissionBoardDto; import com.renew.sw.mentoring.domain.post.model.entity.dto.request.RequestUpdateMissionBoardDto; @@ -19,6 +22,7 @@ import org.springframework.web.bind.annotation.*; import javax.validation.Valid; +import java.util.List; @Tag(name = "미션 인증 게시판", description = "미션 인증 관련 api") @RestController @@ -27,6 +31,7 @@ public class MissionBoardController { private final MissionBoardService missionBoardService; + private final CommentService commentService; /** * 미션 인증 글 등록 @@ -113,4 +118,61 @@ public void delete(AppAuthentication auth, @PathVariable Long id) { missionBoardService.delete(auth.getUserId(), id, auth.getUserRole()); } + + /** + * 모든 댓글을 조회합니다. + */ + @GetMapping("/comments/{postId}") + public List listComments(@PathVariable Long postId) { + return commentService.list(postId); + } + + /** + * 게시글에 댓글을 생성합니다. + **/ + @PostMapping("/comment/{postId}") + @UserAuth + public ResponseIdDto createComment(AppAuthentication auth, + @PathVariable Long postId, + @Valid @RequestBody RequestCreateCommentDto dto) { + Long result = commentService.create(postId, auth.getUserId(), dto.getContent()); + return new ResponseIdDto(result); + } + + /** + * 댓글을 수정합니다. + *

대댓글도 수정할 수 있습니다.

+ */ + @PatchMapping("/comment/{commentId}") + @UserAuth + public void editComment(AppAuthentication auth, + @PathVariable Long commentId, + @Valid @RequestBody RequestCreateCommentDto dto) { + commentService.edit(commentId, auth.getUserId(), dto.getContent()); + } + + /** + * 댓글을 삭제합니다. + *

대댓글도 삭제할 수 있습니다.

+ */ + @DeleteMapping("/comment/{commentId}") + @UserAuth + public void deleteComment(AppAuthentication auth, + @PathVariable Long commentId) { + commentService.delete(commentId, auth.getUserId()); + } + + /** + * 대댓글을 생성합니다. + * + * @param commentId 댓글 ID + */ + @PostMapping("/reply/{commentId}") + @UserAuth + public ResponseIdDto createReply(AppAuthentication auth, + @PathVariable Long commentId, + @Valid @RequestBody RequestCreateCommentDto dto) { + Long result = commentService.createReply(commentId, auth.getUserId(), dto.getContent()); + return new ResponseIdDto(result); + } } diff --git a/src/main/java/com/renew/sw/mentoring/domain/post/controller/NoticeController.java b/src/main/java/com/renew/sw/mentoring/domain/post/controller/NoticeController.java index 0b02f00..28f8844 100644 --- a/src/main/java/com/renew/sw/mentoring/domain/post/controller/NoticeController.java +++ b/src/main/java/com/renew/sw/mentoring/domain/post/controller/NoticeController.java @@ -1,5 +1,8 @@ package com.renew.sw.mentoring.domain.post.controller; +import com.renew.sw.mentoring.domain.comment.CommentService; +import com.renew.sw.mentoring.domain.comment.model.dto.request.RequestCreateCommentDto; +import com.renew.sw.mentoring.domain.comment.model.dto.response.SummarizedCommentDto; import com.renew.sw.mentoring.domain.post.model.entity.dto.list.SummarizedGenericPostDto; import com.renew.sw.mentoring.domain.post.model.entity.dto.request.RequestCreateNoticeDto; import com.renew.sw.mentoring.domain.post.model.entity.dto.request.RequestUpdateNoticeDto; @@ -19,6 +22,9 @@ import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; +import javax.validation.Valid; +import java.util.List; + @RestController @Slf4j @RequiredArgsConstructor @@ -27,6 +33,7 @@ public class NoticeController { private final NoticeService noticeService; + private final CommentService commentService; /** * 공지사항 목록 조회 @@ -92,4 +99,60 @@ public void delete(@PathVariable Long id, AppAuthentication auth) { noticeService.delete(auth.getUserId(), id, auth.getUserRole()); } + /** + * 모든 댓글을 조회합니다. + */ + @GetMapping("/comment/{postId}") + public List listComments(@PathVariable Long postId) { + return commentService.list(postId); + } + + /** + * 게시글에 댓글을 생성합니다. + **/ + @PostMapping("/comment/{postId}") + @UserAuth + public ResponseIdDto createComment(AppAuthentication auth, + @PathVariable Long postId, + @Valid @RequestBody RequestCreateCommentDto dto) { + Long result = commentService.create(postId, auth.getUserId(), dto.getContent()); + return new ResponseIdDto(result); + } + + /** + * 댓글을 수정합니다. + *

대댓글도 수정할 수 있습니다.

+ */ + @PatchMapping("/comment/{commentId}") + @UserAuth + public void editComment(AppAuthentication auth, + @PathVariable Long commentId, + @Valid @RequestBody RequestCreateCommentDto dto) { + commentService.edit(commentId, auth.getUserId(), dto.getContent()); + } + + /** + * 댓글을 삭제합니다. + *

대댓글도 삭제할 수 있습니다.

+ */ + @DeleteMapping("/comment/{commentId}") + @UserAuth + public void deleteComment(AppAuthentication auth, + @PathVariable Long commentId) { + commentService.delete(commentId, auth.getUserId()); + } + + /** + * 대댓글을 생성합니다. + * + * @param commentId 댓글 ID + */ + @PostMapping("/reply/{commentId}") + @UserAuth + public ResponseIdDto createReply(AppAuthentication auth, + @PathVariable Long commentId, + @Valid @RequestBody RequestCreateCommentDto dto) { + Long result = commentService.createReply(commentId, auth.getUserId(), dto.getContent()); + return new ResponseIdDto(result); + } } diff --git a/src/main/java/com/renew/sw/mentoring/domain/post/repository/PostRepository.java b/src/main/java/com/renew/sw/mentoring/domain/post/repository/PostRepository.java new file mode 100644 index 0000000..0ea463d --- /dev/null +++ b/src/main/java/com/renew/sw/mentoring/domain/post/repository/PostRepository.java @@ -0,0 +1,7 @@ +package com.renew.sw.mentoring.domain.post.repository; + +import com.renew.sw.mentoring.domain.post.model.entity.Post; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostRepository extends JpaRepository { +} diff --git a/src/main/resources/errors.properties b/src/main/resources/errors.properties index 37ead8c..1c2bbd7 100644 --- a/src/main/resources/errors.properties +++ b/src/main/resources/errors.properties @@ -22,6 +22,7 @@ notfound.user=\uD574\uB2F9 \uC720\uC800\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\u notfound.team=\uD574\uB2F9 \uD300\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. notfound.post=\uD574\uB2F9 \uAC8C\uC2DC\uAE00\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. notfound.mission=\uD574\uB2F9 \uBBF8\uC158\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. +notfound.comment=\uD574\uB2F9 \uB313\uAE00\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. faild.mission-board.accepted=\uC2B9\uC778\uC774 \uC644\uB8CC\uB41C \uBBF8\uC158 \uC778\uC99D \uAE00\uC774 \uC544\uB2D9\uB2C8\uB2E4. faild.mission-board.in-progress=\uC2B9\uC778 \uB300\uAE30 \uC911\uC778 \uBBF8\uC158 \uC778\uC99D \uAE00\uC774 \uC544\uB2D9\uB2C8\uB2E4. \ No newline at end of file diff --git a/src/main/resources/errors_en_US.properties b/src/main/resources/errors_en_US.properties index 5360d60..05fecd1 100644 --- a/src/main/resources/errors_en_US.properties +++ b/src/main/resources/errors_en_US.properties @@ -22,6 +22,7 @@ notfound.user=Cannot find that user. notfound.team=Cannot find that team. notfound.post=Cannot find that post. notfound.mission=Cannot find that mission. +notfound.comment=Cannot find that comment. failed.mission-board.accepted=This is not an accepted mission board. failed.mission-board.in-progress=This post is not pending approval.