From c50f282dbbdd0e92f59fa37965a5deb666fb82a0 Mon Sep 17 00:00:00 2001 From: yeseul106 <68415644+yeseul106@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:41:36 +0900 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=EB=AA=A8=EC=9E=84=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20=EB=8C=93=EA=B8=80=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?API=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EB=B0=8F=20=EC=95=8C=EB=A6=BC=20=EC=9E=91=EC=97=85=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FEAT] 모임 게시글 댓글 작성 API 마이그레이션 및 푸시 알림 구현 #118 * [INFRA] 게시글 댓글 작성 API v2 라우팅 규칙 변경 #118 * [INFRA] 댓글 작성 API와 게시글 작성 API 라우팅 규칙 수정 #118 --- docker-compose.yml | 4 +- .../main/comment/v2/CommentV2Controller.java | 42 +++ .../CommentV2CreateCommentBodyDto.java | 22 ++ .../CommentV2CreateCommentResponseDto.java | 14 + .../comment/v2/service/CommentV2Service.java | 10 + .../v2/service/CommentV2ServiceImpl.java | 72 +++++ .../main/common/response/ErrorStatus.java | 3 +- .../crew/main/entity/comment/Comment.java | 254 +++++++++--------- .../entity/comment/CommentRepository.java | 7 + .../makers/crew/main/entity/post/Post.java | 1 + .../crew/main/entity/post/PostRepository.java | 10 + .../notification/PushNotificationEnums.java | 4 +- .../src/comment/v1/comment-v1.controller.ts | 1 + 13 files changed, 312 insertions(+), 132 deletions(-) create mode 100644 main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2CreateCommentBodyDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2CreateCommentResponseDto.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java diff --git a/docker-compose.yml b/docker-compose.yml index 9925f636..7f723aeb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -74,8 +74,10 @@ services: caddy.route_3.reverse_proxy: "{{ upstreams 4000 }}" caddy.route_4: /meeting/v2/* caddy.route_4.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_5: /post/v2/* + caddy.route_5: /post/v2 caddy.route_5.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_6: /comment/v2 + caddy.route_6.reverse_proxy: "{{ upstreams 4000 }}" nestjs: build: diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java new file mode 100644 index 00000000..e40c5b27 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java @@ -0,0 +1,42 @@ +package org.sopt.makers.crew.main.comment.v2; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.security.Principal; +import lombok.RequiredArgsConstructor; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.service.CommentV2Service; +import org.sopt.makers.crew.main.common.util.UserUtil; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/comment/v2") +@RequiredArgsConstructor +@Tag(name = "댓글/대댓글") +public class CommentV2Controller { + + private final CommentV2Service commentV2Service; + + @Operation(summary = "모임 게시글 댓글 작성") + @PostMapping() + @ResponseStatus(HttpStatus.CREATED) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "성공"), + }) + public ResponseEntity createComment( + @Valid @RequestBody CommentV2CreateCommentBodyDto requestBody, Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok(commentV2Service.createComment(requestBody, userId)); + } + +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2CreateCommentBodyDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2CreateCommentBodyDto.java new file mode 100644 index 00000000..c7df385f --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2CreateCommentBodyDto.java @@ -0,0 +1,22 @@ +package org.sopt.makers.crew.main.comment.v2.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Schema(description = "댓글 생성 request body dto") +public class CommentV2CreateCommentBodyDto { + + @Schema(example = "1", required = true, description = "게시글 ID") + @NotNull + private Integer postId; + + @Schema(example = "알고보면 쓸데있는 개발 프로세스", required = true, description = "댓글 내용") + @NotEmpty + private String contents; + +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2CreateCommentResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2CreateCommentResponseDto.java new file mode 100644 index 00000000..d40b12f8 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2CreateCommentResponseDto.java @@ -0,0 +1,14 @@ +package org.sopt.makers.crew.main.comment.v2.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(staticName = "of") +public class CommentV2CreateCommentResponseDto { + + /** + * 생성된 댓글 id + */ + private Integer commentId; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java new file mode 100644 index 00000000..c2a7e78a --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java @@ -0,0 +1,10 @@ +package org.sopt.makers.crew.main.comment.v2.service; + +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; + +public interface CommentV2Service { + + CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, + Integer userId); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java new file mode 100644 index 00000000..dbbb5d7a --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java @@ -0,0 +1,72 @@ +package org.sopt.makers.crew.main.comment.v2.service; + +import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.NEW_COMMENT_PUSH_NOTIFICATION_TITLE; +import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.PUSH_NOTIFICATION_CATEGORY; + +import lombok.RequiredArgsConstructor; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; +import org.sopt.makers.crew.main.entity.comment.Comment; +import org.sopt.makers.crew.main.entity.comment.CommentRepository; +import org.sopt.makers.crew.main.entity.post.Post; +import org.sopt.makers.crew.main.entity.post.PostRepository; +import org.sopt.makers.crew.main.entity.user.User; +import org.sopt.makers.crew.main.entity.user.UserRepository; +import org.sopt.makers.crew.main.internal.notification.PushNotificationService; +import org.sopt.makers.crew.main.internal.notification.dto.PushNotificationRequestDto; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CommentV2ServiceImpl implements CommentV2Service { + + private final PostRepository postRepository; + private final UserRepository userRepository; + private final CommentRepository commentRepository; + private final PushNotificationService pushNotificationService; + + @Value("${push-notification.web-url}") + private String pushWebUrl; + + /** + * 모임 게시글 댓글 작성 + * + * @throws 400 존재하지 않는 게시글일 떄 + * @apiNote 모임에 속한 유저만 작성 가능 + */ + @Override + @Transactional + + public CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, + Integer userId) { + Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); + User user = userRepository.findByIdOrThrow(userId); + + Comment comment = Comment.builder() + .contents(requestBody.getContents()) + .user(user) + .post(post) + .build(); + + Comment savedComment = commentRepository.save(comment); + + User PostWriter = post.getUser(); + String[] userIds = {String.valueOf(PostWriter.getOrgId())}; + + String pushNotificationContent = String.format("[%s의 댓글] : \"%s\"", + user.getName(), requestBody.getContents()); + String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); + + PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, + NEW_COMMENT_PUSH_NOTIFICATION_TITLE.getValue(), + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); + + pushNotificationService.sendPushNotification(pushRequestDto); + + return CommentV2CreateCommentResponseDto.of(savedComment.getId()); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java b/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java index b7311d2f..22890132 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java +++ b/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java @@ -11,13 +11,14 @@ public enum ErrorStatus { * 204 NO_CONTENT */ NO_CONTENT_EXCEPTION("참여한 모임이 없습니다."), - + /** * 400 BAD_REQUEST */ VALIDATION_EXCEPTION("CR-001"), // errorCode는 예시, 추후 변경 예정 -> 잘못된 요청입니다. VALIDATION_REQUEST_MISSING_EXCEPTION("요청값이 입력되지 않았습니다."), NOT_FOUND_MEETING("모임이 없습니다."), + NOT_FOUND_POST("존재하지 않는 게시글입니다."), /** * 401 UNAUTHORIZED diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java index 03cc2067..48832145 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java @@ -34,133 +34,129 @@ @Table(name = "comment") public class Comment { - /** - * 댓글의 고유 식별자 - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private int id; - - /** - * 댓글 내용 - */ - @Column(nullable = false) - private String contents; - - /** - * 댓글 깊이 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int depth; - - /** - * 댓글 순서 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int order; - - /** - * 작성일 - */ - @Column(name = "createdDate", nullable = false, columnDefinition = "TIMESTAMP") - @CreatedDate - private LocalDateTime createdDate; - - /** - * 수정일 - */ - @Column(name = "updatedDate", nullable = false, columnDefinition = "TIMESTAMP") - @LastModifiedDate - private LocalDateTime updatedDate; - - /** - * 작성자 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "userId", nullable = false) - private User user; - - /** - * 작성자의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private int userId; - - /** - * 댓글이 속한 게시글 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "postId", nullable = false) - private Post post; - - /** - * 댓글이 속한 게시글의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private int postId; - - /** - * 댓글에 대한 좋아요 목록 - */ - @OneToMany(mappedBy = "comment", cascade = CascadeType.REMOVE) - private List likes = new ArrayList<>(); - - /** - * 댓글에 대한 좋아요 수 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int likeCount; - - /** - * 부모 댓글 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "parentId") - private Comment parent; - - /** - * 부모 댓글의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private int parentId; - - /** - * 자식 댓글 목록 - */ - @OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE) - private List children; - - /** - * 댓글에 대한 신고 목록 - */ - @OneToMany(mappedBy = "comment", cascade = CascadeType.REMOVE) - private List reports; - - @Builder - public Comment(String contents, int depth, int order, - User user, int userId, Post post, int postId, int likeCount, Comment parent, - int parentId, List children, List reports) { - this.contents = contents; - this.depth = depth; - this.order = order; - this.user = user; - this.userId = userId; - this.post = post; - this.postId = postId; - this.likeCount = 0; - this.parent = parent; - this.parentId = parentId; - } - - public void addLike(Like like) { - this.likes.add(like); - } - - public void addChildrenComment(Comment comment) { - this.children.add(comment); - } - - public void addReport(Report report) { - this.reports.add(report); - } + /** + * 댓글의 고유 식별자 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + /** + * 댓글 내용 + */ + @Column(nullable = false) + private String contents; + + /** + * 댓글 깊이 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int depth; + + /** + * 댓글 순서 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int order; + + /** + * 작성일 + */ + @Column(name = "createdDate", nullable = false, columnDefinition = "TIMESTAMP") + @CreatedDate + private LocalDateTime createdDate; + + /** + * 수정일 + */ + @Column(name = "updatedDate", nullable = false, columnDefinition = "TIMESTAMP") + @LastModifiedDate + private LocalDateTime updatedDate; + + /** + * 작성자 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", nullable = false) + private User user; + + /** + * 작성자의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private int userId; + + /** + * 댓글이 속한 게시글 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "postId", nullable = false) + private Post post; + + /** + * 댓글이 속한 게시글의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private int postId; + + /** + * 댓글에 대한 좋아요 목록 + */ + @OneToMany(mappedBy = "comment", cascade = CascadeType.REMOVE) + private List likes = new ArrayList<>(); + + /** + * 댓글에 대한 좋아요 수 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int likeCount; + + /** + * 부모 댓글 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parentId") + private Comment parent; + + /** + * 부모 댓글의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private int parentId; + + /** + * 자식 댓글 목록 + */ + @OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE) + private List children; + + /** + * 댓글에 대한 신고 목록 + */ + @OneToMany(mappedBy = "comment", cascade = CascadeType.REMOVE) + private List reports; + + @Builder + public Comment(String contents, User user, Post post, Comment parent) { + this.contents = contents; + this.user = user; + this.post = post; + this.parent = parent; + this.depth = 0; + this.order = 0; + this.likeCount = 0; + this.post.addComment(this); + } + + public void addLike(Like like) { + this.likes.add(like); + } + + public void addChildrenComment(Comment comment) { + this.children.add(comment); + } + + public void addReport(Report report) { + this.reports.add(report); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java new file mode 100644 index 00000000..45e789e3 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java @@ -0,0 +1,7 @@ +package org.sopt.makers.crew.main.entity.comment; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository { + +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java b/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java index 95481e86..ef8bc778 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java @@ -157,6 +157,7 @@ public void addLike(Like like) { public void addComment(Comment comment) { this.comments.add(comment); + this.commentCount++; } public void addReport(Report report) { diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostRepository.java index 8358b300..816719c7 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostRepository.java @@ -1,7 +1,17 @@ package org.sopt.makers.crew.main.entity.post; +import static org.sopt.makers.crew.main.common.response.ErrorStatus.NOT_FOUND_POST; + +import java.util.Optional; +import org.sopt.makers.crew.main.common.exception.BadRequestException; import org.springframework.data.jpa.repository.JpaRepository; public interface PostRepository extends JpaRepository { + Optional findById(Integer postId); + + default Post findByIdOrThrow(Integer postId) { + return findById(postId) + .orElseThrow(() -> new BadRequestException(NOT_FOUND_POST.getErrorCode())); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java b/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java index 46c6d533..83096c9c 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java +++ b/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java @@ -11,7 +11,9 @@ public enum PushNotificationEnums { PUSH_NOTIFICATION_CATEGORY("NEWS"), - NEW_POST_PUSH_NOTIFICATION_TITLE("✏️내 모임에 새로운 글이 업로드됐어요."); + NEW_POST_PUSH_NOTIFICATION_TITLE("✏️내 모임에 새로운 글이 업로드됐어요."), + NEW_COMMENT_PUSH_NOTIFICATION_TITLE("📢내가 작성한 모임 피드에 새로운 댓글이 달렸어요."), + ; private final String value; } diff --git a/server/src/comment/v1/comment-v1.controller.ts b/server/src/comment/v1/comment-v1.controller.ts index d0f0b0c5..38a1e3ab 100644 --- a/server/src/comment/v1/comment-v1.controller.ts +++ b/server/src/comment/v1/comment-v1.controller.ts @@ -95,6 +95,7 @@ export class CommentV1Controller { @ApiOperation({ summary: '모임 게시글 댓글 작성', + deprecated: true, }) @ApiOkResponseCommon(CommentV1CreateCommentResponseDto) @ApiResponse({