Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: 게시물 댓글 CRUD 구현 #257

Merged
merged 13 commits into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.codiary.backend.domain.comment.controller;

import com.codiary.backend.domain.comment.converter.CommentConverter;
import com.codiary.backend.domain.comment.dto.request.CommentRequestDTO.CommentDTO;
import com.codiary.backend.domain.comment.dto.response.CommentResponseDTO;
import com.codiary.backend.domain.comment.entity.Comment;
import com.codiary.backend.domain.comment.service.CommentService;
import com.codiary.backend.domain.member.security.CustomMemberDetails;
import com.codiary.backend.global.apiPayload.ApiResponse;
import com.codiary.backend.global.apiPayload.code.status.SuccessStatus;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RestController;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v2/posts")
@Tag(name = "댓글 API", description = "댓글 관련 API 입니다.")
public class CommentController {

private final CommentService commentService;

@Operation(summary = "댓글 달기")
@PostMapping("/posts/{post_id}/comments")
public ApiResponse<CommentResponseDTO.CommentDTO> commentOnPost(
@PathVariable("post_id") Long postId,
@RequestBody CommentDTO request,
@AuthenticationPrincipal CustomMemberDetails memberDetails
) {
Long commenterId = memberDetails.getId();
Comment newComment = commentService.commentOnPost(postId, commenterId, request);
return ApiResponse.onSuccess(SuccessStatus.COMMENT_OK, CommentConverter.toCommentResponseDto(newComment));
}

@Operation(summary = "댓글 삭제")
@DeleteMapping("comments/{comment_id}")
public ApiResponse<String> deleteComment(
@PathVariable("comment_id") Long commentId,
@AuthenticationPrincipal CustomMemberDetails memberDetails
) {
Long memberId = memberDetails.getId();
String response = commentService.deleteComment(commentId, memberId);
return ApiResponse.onSuccess(SuccessStatus.COMMENT_OK, response);
}

@Operation(summary = "댓글 수정")
@PatchMapping("comments/{comment_id}")
public ApiResponse<CommentResponseDTO.CommentDTO> updateComment(
@PathVariable("comment_id") Long commentId,
@RequestBody CommentDTO request,
@AuthenticationPrincipal CustomMemberDetails memberDetails
) {
Long memberId = memberDetails.getId();
Comment updatedComment = commentService.updateComment(commentId, memberId, request);
return ApiResponse.onSuccess(SuccessStatus.COMMENT_OK, CommentConverter.toCommentResponseDto(updatedComment));
}

@Operation(summary = "댓글 조회")
@GetMapping("/posts/{post_id}/comments")
public ApiResponse<List<CommentResponseDTO.CommentDTO>> getComments(
@PathVariable("post_id") Long postId,
@AuthenticationPrincipal CustomMemberDetails memberDetails
) {
Long memberId = memberDetails.getId();
List<Comment> comments = commentService.getComments(postId, memberId);
return ApiResponse.onSuccess(SuccessStatus.COMMENT_OK, CommentConverter.toCommentResponseListDto(comments));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.codiary.backend.domain.comment.converter;

import com.codiary.backend.domain.comment.dto.response.CommentResponseDTO;
import com.codiary.backend.domain.comment.entity.Comment;
import java.util.List;
import java.util.stream.Collectors;

public class CommentConverter {

public static CommentResponseDTO.CommentDTO toCommentResponseDto(Comment comment) {
return CommentResponseDTO.CommentDTO.builder()
.commentId(comment.getCommentId())
.commentBody(comment.getCommentBody())
.postId(comment.getPost().getPostId())
.commenterId(comment.getMember().getMemberId())
.commenterProfileImageUrl(
(comment.getMember().getImage() != null) ? (comment.getMember().getImage().getImageUrl()) : "")
.commenterNickname(comment.getMember().getNickname())
.createdAt(comment.getCreatedAt())
.updatedAt(comment.getUpdatedAt())
.build();
}

public static List<CommentResponseDTO.CommentDTO> toCommentResponseListDto(List<Comment> comments) {
return comments.stream()
.map(CommentConverter::toCommentResponseDto)
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.codiary.backend.domain.comment.dto.request;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Builder;

public class CommentRequestDTO {

// Comment 생성 DTO
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@Builder
public record CommentDTO(
String commentBody
) {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.codiary.backend.domain.comment.dto.response;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import java.time.LocalDateTime;
import lombok.Builder;

public class CommentResponseDTO {

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@Builder
public record CommentDTO(
Long commentId,
String commentBody,
Long postId,
Long commenterId,
String commenterProfileImageUrl,
String commenterNickname,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
package com.codiary.backend.domain.comment.entity;

import com.codiary.backend.domain.member.entity.Member;
import com.codiary.backend.global.common.BaseEntity;
import com.codiary.backend.domain.post.entity.Post;
import jakarta.persistence.*;
import lombok.*;

import com.codiary.backend.global.common.BaseEntity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import java.util.ArrayList;
import java.util.List;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
Expand Down Expand Up @@ -37,6 +48,13 @@ public class Comment extends BaseEntity {
@OneToMany(mappedBy = "parentId", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> childComments = new ArrayList<>();

@Builder
public Comment(String commentBody, Member member, Post post) {
this.commentBody = commentBody;
this.member = member;
this.post = post;
}

public void setMember(Member member) {
if (this.member != null) {
member.getCommentList().remove(this);
Expand All @@ -56,4 +74,8 @@ public void setPost(Post post) {

post.getCommentList().add(this);
}

public void setCommentBody(String commentBody) {
this.commentBody = commentBody;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.codiary.backend.domain.comment.repository;

import com.codiary.backend.domain.comment.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CommentRepository extends JpaRepository<Comment, Long>, CommentRepositoryCustom {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.codiary.backend.domain.comment.repository;

import com.codiary.backend.domain.comment.entity.Comment;
import java.util.List;

public interface CommentRepositoryCustom {

List<Comment> findByPostWithMemberInfoOrderByCreatedAtAsc(Long postId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.codiary.backend.domain.comment.repository;

import static com.codiary.backend.domain.comment.entity.QComment.comment;
import static com.codiary.backend.domain.member.entity.QMember.member;
import static com.codiary.backend.domain.post.entity.QPost.post;

import com.codiary.backend.domain.comment.entity.Comment;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class CommentRepositoryImpl implements CommentRepositoryCustom {

private final JPAQueryFactory queryFactory;

@Override
public List<Comment> findByPostWithMemberInfoOrderByCreatedAtAsc(Long postId) {
List<Comment> comments = queryFactory
.selectFrom(comment)
.leftJoin(comment.member, member)
.leftJoin(comment.post, post)
.where(comment.post.postId.eq(postId))
// .offset(pageable.getOffset())
// .limit(pageable.getPageSize())
.orderBy(comment.createdAt.asc())
.fetch();

return comments;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.codiary.backend.domain.comment.service;

import com.codiary.backend.domain.comment.dto.request.CommentRequestDTO;
import com.codiary.backend.domain.comment.dto.request.CommentRequestDTO.CommentDTO;
import com.codiary.backend.domain.comment.entity.Comment;
import com.codiary.backend.domain.comment.repository.CommentRepository;
import com.codiary.backend.domain.member.entity.Member;
import com.codiary.backend.domain.member.repository.MemberRepository;
import com.codiary.backend.domain.post.entity.Post;
import com.codiary.backend.domain.post.enumerate.PostAccess;
import com.codiary.backend.domain.post.repository.PostRepository;
import com.codiary.backend.domain.team.entity.Team;
import com.codiary.backend.domain.team.repository.TeamRepository;
import com.codiary.backend.global.apiPayload.code.status.ErrorStatus;
import com.codiary.backend.global.apiPayload.exception.GeneralException;
import com.codiary.backend.global.apiPayload.exception.handler.MemberHandler;
import com.codiary.backend.global.apiPayload.exception.handler.PostHandler;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class CommentService {

private final CommentRepository commentRepository;
private final MemberRepository memberRepository;
private final PostRepository postRepository;
private final TeamRepository teamRepository;

public Comment commentOnPost(Long postId, Long commenterId, CommentDTO request) {
// validation: 사용자, post 유무 확인
Member commenter = memberRepository.findById(commenterId)
.orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND));
Post post = postRepository.findById(postId).orElseThrow(() -> new PostHandler(ErrorStatus.POST_NOT_FOUND));

// validation: 사용자가 해당 게시물에 대한 권한 있는지
if (post.getPostAccess().equals(PostAccess.MEMBER) && post.getMember() != commenter) {
throw new GeneralException(ErrorStatus.COMMENT_CREATE_UNAUTHORIZED);
} else if (post.getPostStatus().equals(PostAccess.TEAM)) {
Team teamOfPost = post.getTeam();
if (!teamRepository.isTeamMember(teamOfPost, commenter)) {
throw new GeneralException((ErrorStatus.COMMENT_CREATE_UNAUTHORIZED));
}
}

// business logic: 댓글 생성
Comment comment = Comment.builder()
.commentBody(request.commentBody())
.member(commenter)
.post(post)
.build();

// response: 댓글 반환
return commentRepository.save(comment);
}

public String deleteComment(Long commentId, Long memberId) {
// validation: 사용자, comment 유무 확인
// + 사용자가 해당 댓글에 대한 권한 있는지
Member requester = memberRepository.findById(memberId)
.orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND));
Comment comment = commentRepository.findById(commentId)
.orElseThrow(() -> new GeneralException(ErrorStatus.COMMENT_NOT_FOUND));
if (comment.getMember() != requester) {
throw new GeneralException(ErrorStatus.COMMENT_DELETE_UNAUTHORIZED);
}

// business logic: 댓글 삭제
commentRepository.delete(comment);

// response: 삭제 성공 반환
return "성공적으로 삭제되었습니다!";
}

@Transactional
public Comment updateComment(Long commentId, Long memberId, CommentRequestDTO.CommentDTO request) {
// validation: 사용자, comment 유무 확인
// + 사용자가 해당 댓글에 대한 권한 있는지
Member requester = memberRepository.findById(memberId)
.orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND));
Comment comment = commentRepository.findById(commentId)
.orElseThrow(() -> new GeneralException(ErrorStatus.COMMENT_NOT_FOUND));
if (comment.getMember() != requester) {
throw new GeneralException(ErrorStatus.COMMENT_UPDATE_UNAUTHORIZED);
}

// business logic: 댓글 수정
comment.setCommentBody(request.commentBody());

// response
return commentRepository.save(comment);
}

@Transactional(readOnly = true)
public List<Comment> getComments(Long postId, Long memberId) {
// validation: 사용자, post 유무 확인
Member requester = memberRepository.findById(memberId)
.orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND));
Post post = postRepository.findById(postId).orElseThrow(() -> new PostHandler(ErrorStatus.POST_NOT_FOUND));

// validation: 사용자가 해당 게시물에 대한 권한 있는지
if (post.getPostAccess().equals(PostAccess.MEMBER) && post.getMember() != requester) {
throw new GeneralException(ErrorStatus.COMMENT_CREATE_UNAUTHORIZED);
} else if (post.getPostStatus().equals(PostAccess.TEAM)) {
Team teamOfPost = post.getTeam();
if (!teamRepository.isTeamMember(teamOfPost, requester)) {
throw new GeneralException((ErrorStatus.COMMENT_CREATE_UNAUTHORIZED));
}
}

// business logic: 댓글 조회
List<Comment> comments = commentRepository.findByPostWithMemberInfoOrderByCreatedAtAsc(postId);

// response: comment list 반환
return comments;
}
}
Loading