-
Notifications
You must be signed in to change notification settings - Fork 4
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(Comment) : 댓글 기능 추가 #102
Changes from all commits
69e054c
29654fa
7ebc771
4a61d65
45e74b9
a6776ee
8ec380e
30a420e
1f0a6f9
20d1d80
0afd4ed
e6d5c33
20a85d5
188404f
a99498c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package chzzk.grassdiary.domain.comment.controller; | ||
|
||
import chzzk.grassdiary.domain.comment.dto.CommentDeleteResponseDTO; | ||
import chzzk.grassdiary.domain.comment.dto.CommentResponseDTO; | ||
import chzzk.grassdiary.domain.comment.dto.CommentSaveRequestDTO; | ||
import chzzk.grassdiary.domain.comment.dto.CommentUpdateRequestDTO; | ||
import chzzk.grassdiary.domain.comment.service.CommentService; | ||
import chzzk.grassdiary.global.auth.common.AuthenticatedMember; | ||
import chzzk.grassdiary.global.auth.service.dto.AuthMemberPayload; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import java.util.List; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.data.domain.Pageable; | ||
import org.springframework.data.domain.Slice; | ||
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; | ||
|
||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/api/comment") | ||
@Tag(name = "댓글 컨트롤러") | ||
public class CommentController { | ||
private final CommentService commentService; | ||
|
||
@PostMapping("/{diaryId}") | ||
public CommentResponseDTO save( | ||
@PathVariable(name = "diaryId") Long diaryId, | ||
@RequestBody CommentSaveRequestDTO requestDTO, | ||
@AuthenticatedMember AuthMemberPayload payload | ||
) { | ||
return commentService.save(diaryId, requestDTO, payload.id()); | ||
} | ||
|
||
@PatchMapping("/{commentId}") | ||
public CommentResponseDTO update( | ||
@PathVariable(name = "commentId") Long commentId, | ||
@RequestBody CommentUpdateRequestDTO requestDTO, | ||
@AuthenticatedMember AuthMemberPayload payload | ||
) { | ||
return commentService.update(commentId, requestDTO, payload.id()); | ||
} | ||
|
||
@PatchMapping("/{commentId}/delete") | ||
public CommentDeleteResponseDTO delete( | ||
@PathVariable(name = "commentId") Long commentId, | ||
@AuthenticatedMember AuthMemberPayload payload | ||
) { | ||
return commentService.delete(commentId, payload.id()); | ||
} | ||
|
||
// 모든 댓글 검색 | ||
@GetMapping("/{diaryId}") | ||
public List<CommentResponseDTO> findAll( | ||
Pageable pageable, | ||
@PathVariable(name = "diaryId") Long diaryId | ||
) { | ||
return commentService.findAll(pageable, diaryId); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package chzzk.grassdiary.domain.comment.dto; | ||
|
||
import chzzk.grassdiary.domain.comment.entity.Comment; | ||
|
||
public record CommentDeleteResponseDTO( | ||
boolean deleted | ||
) { | ||
public static CommentDeleteResponseDTO from(Comment comment) { | ||
return new CommentDeleteResponseDTO( | ||
comment.isDeleted() | ||
); | ||
} | ||
} | ||
Comment on lines
+5
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코드 리뷰 중 해당 클래스에서 프론트에서 응답을 받았을 때 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 불필요한 데이터 전송을 최소화하고자 따로 만들었는데, 유지 보수 측면을 생각해보면 CommentResponseDTO로 통일 시키는 것도 괜찮아 보이네요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 추가적으로, 예슬님 피드백을 보다보니 CommentResponseDTO에 CommentID값을 같이 보내주도록 추가하는 것이 필수적인 것 같습니다. 감사합니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CommentId가 없었군요? 추가 하는 것 좋습니다 👍👍 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package chzzk.grassdiary.domain.comment.dto; | ||
|
||
import chzzk.grassdiary.domain.comment.entity.Comment; | ||
import java.time.format.DateTimeFormatter; | ||
import java.util.List; | ||
import java.util.stream.Collectors; | ||
|
||
public record CommentResponseDTO( | ||
// 멤버 정보(사진, 아이디), 댓글내용, 작성 시간 | ||
// todo: 사진정보 추가 | ||
Long commentId, | ||
Long memberId, | ||
String content, | ||
boolean deleted, | ||
String createdDate, | ||
String createdAt, | ||
List<CommentResponseDTO> childComments | ||
) { | ||
public static CommentResponseDTO from(Comment comment) { | ||
return new CommentResponseDTO( | ||
comment.getId(), | ||
comment.getMember().getId(), | ||
comment.getContent(), | ||
comment.isDeleted(), | ||
comment.getCreatedAt().format(DateTimeFormatter.ofPattern("yy년 MM월 dd일")), | ||
comment.getCreatedAt().format(DateTimeFormatter.ofPattern("HH:mm")), | ||
comment.getChildComments().stream().map(CommentResponseDTO::from).collect(Collectors.toList()) | ||
); | ||
} | ||
|
||
public static CommentResponseDTO fromDeleted(Comment comment) { | ||
return new CommentResponseDTO( | ||
comment.getId(), | ||
null, | ||
null, | ||
true, | ||
null, | ||
null, | ||
comment.getChildComments().stream().map(CommentResponseDTO::from).collect(Collectors.toList()) | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package chzzk.grassdiary.domain.comment.dto; | ||
|
||
import chzzk.grassdiary.domain.comment.entity.Comment; | ||
import chzzk.grassdiary.domain.diary.entity.Diary; | ||
import chzzk.grassdiary.domain.member.entity.Member; | ||
|
||
public record CommentSaveRequestDTO( | ||
Long memberId, | ||
Long diaryId, | ||
String content, | ||
Long parentCommentId | ||
) { | ||
public Comment toEntity(Member member, Diary diary, Comment parentComment) { | ||
return Comment.builder() | ||
.member(member) | ||
.diary(diary) | ||
.content(content) | ||
.parentComment(parentComment) | ||
.build(); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package chzzk.grassdiary.domain.comment.dto; | ||
|
||
public record CommentUpdateRequestDTO( | ||
String content | ||
) { | ||
} | ||
Comment on lines
+3
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 클래스도 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package chzzk.grassdiary.domain.comment.entity; | ||
|
||
import chzzk.grassdiary.domain.base.BaseTimeEntity; | ||
import chzzk.grassdiary.domain.diary.entity.Diary; | ||
import chzzk.grassdiary.domain.member.entity.Member; | ||
import com.fasterxml.jackson.annotation.JsonIgnore; | ||
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; | ||
|
||
@Getter | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
@Entity | ||
public class Comment extends BaseTimeEntity { | ||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
@Column(name = "comment_id") | ||
private Long id; | ||
|
||
@ManyToOne(fetch = FetchType.LAZY) | ||
@JoinColumn(name = "member_id") | ||
private Member member; | ||
|
||
@ManyToOne(fetch = FetchType.LAZY) | ||
@JoinColumn(name = "diary_id") | ||
private Diary diary; | ||
|
||
@Column(columnDefinition = "TEXT") | ||
private String content; | ||
|
||
@ManyToOne(fetch = FetchType.LAZY) | ||
@JoinColumn(name = "parent_id") | ||
@JsonIgnore | ||
private Comment parentComment; | ||
|
||
@OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL) | ||
private List<Comment> childComments = new ArrayList<>(); | ||
|
||
// @OneToMany(mappedBy = "comment", cascade = CascadeType.ALL) | ||
// private List<CommentLike> commentLikes = new ArrayList<>(); | ||
|
||
private boolean deleted; | ||
|
||
@Builder | ||
protected Comment(Member member, Diary diary, String content, Comment parentComment) { | ||
this.member = member; | ||
this.diary = diary; | ||
this.content = content; | ||
this.parentComment = parentComment; | ||
this.deleted = false; | ||
} | ||
|
||
public void update(String content) { | ||
this.content = content; | ||
} | ||
|
||
public void delete() { | ||
this.deleted = true; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package chzzk.grassdiary.domain.comment.entity; | ||
|
||
import java.util.List; | ||
import org.springframework.data.domain.Pageable; | ||
import org.springframework.data.domain.Slice; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import org.springframework.data.jpa.repository.Query; | ||
|
||
public interface CommentDAO extends JpaRepository<Comment, Long> { | ||
@Query("select c from Comment c join fetch c.member left join fetch c.parentComment where c.diary.id = :diaryId order by c.parentComment.id asc nulls first, c.id asc") | ||
List<Comment> findAllByDiaryId(Long diaryId, Pageable pageable); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package chzzk.grassdiary.domain.comment.entity; | ||
|
||
import chzzk.grassdiary.domain.base.BaseCreatedTimeEntity; | ||
import chzzk.grassdiary.domain.base.BaseTimeEntity; | ||
|
||
public class CommentLike extends BaseTimeEntity { | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
package chzzk.grassdiary.domain.comment.service; | ||
|
||
import chzzk.grassdiary.domain.comment.dto.CommentDeleteResponseDTO; | ||
import chzzk.grassdiary.domain.comment.dto.CommentSaveRequestDTO; | ||
import chzzk.grassdiary.domain.comment.dto.CommentUpdateRequestDTO; | ||
import chzzk.grassdiary.domain.comment.entity.Comment; | ||
import chzzk.grassdiary.domain.comment.entity.CommentDAO; | ||
import chzzk.grassdiary.domain.diary.entity.Diary; | ||
import chzzk.grassdiary.domain.diary.entity.DiaryDAO; | ||
import chzzk.grassdiary.domain.comment.dto.CommentResponseDTO; | ||
import chzzk.grassdiary.domain.member.entity.Member; | ||
import chzzk.grassdiary.domain.member.entity.MemberDAO; | ||
import chzzk.grassdiary.global.common.error.exception.SystemException; | ||
import chzzk.grassdiary.global.common.response.ClientErrorCode; | ||
import java.util.ArrayList; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.stream.Collectors; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.data.domain.Pageable; | ||
import org.springframework.data.domain.Slice; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class CommentService { | ||
private final CommentDAO commentDAO; | ||
private final DiaryDAO diaryDAO; | ||
private final MemberDAO memberDAO; | ||
|
||
@Transactional | ||
public CommentResponseDTO save(Long diaryId, CommentSaveRequestDTO requestDTO, Long logInMemberId) { | ||
Member member = getMemberById(logInMemberId); | ||
Diary diary = getDiaryById(diaryId); | ||
Comment parentComment = getParentCommentById(requestDTO.parentCommentId()); | ||
Comment comment = requestDTO.toEntity(member, diary, parentComment); | ||
commentDAO.save(comment); | ||
|
||
return CommentResponseDTO.from(comment); | ||
} | ||
|
||
@Transactional | ||
public CommentResponseDTO update(Long commentId, CommentUpdateRequestDTO requestDTO, Long logInMemberId) { | ||
Member member = getMemberById(logInMemberId); | ||
Comment comment = getCommentById(commentId); | ||
validateCommentAuthor(member, comment); | ||
comment.update(requestDTO.content()); | ||
|
||
return CommentResponseDTO.from(comment); | ||
} | ||
|
||
@Transactional | ||
public CommentDeleteResponseDTO delete(Long commentId, Long logInMemberId) { | ||
Member member = getMemberById(logInMemberId); | ||
Comment comment = getCommentById(commentId); | ||
validateCommentAuthor(member, comment); | ||
validateNotDeleted(comment); | ||
|
||
comment.delete(); | ||
|
||
return CommentDeleteResponseDTO.from(comment); | ||
} | ||
|
||
@Transactional(readOnly = true) | ||
public List<CommentResponseDTO> findAll(Pageable pageable, Long diaryId) { | ||
Diary diary = getDiaryById(diaryId); | ||
List<Comment> comments = commentDAO.findAllByDiaryId(diaryId, pageable); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 댓글의 pageable과 같은 경우는 그냥 모두 불러오도록 List로 받도록 했는데, 파라미터를 미처 삭제하지 못했습니다 ㅋㅋㅋ ㅠㅠ |
||
|
||
List<Comment> hierarchicalComments = convertHierarchy(comments); | ||
|
||
return hierarchicalComments.stream() | ||
.map(this::mapToDTO) | ||
.collect(Collectors.toList()); | ||
} | ||
Comment on lines
+66
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분에서 |
||
|
||
private List<Comment> convertHierarchy(List<Comment> comments) { | ||
Map<Long, Comment> map = new HashMap<>(); | ||
List<Comment> parentComments = new ArrayList<>(); | ||
|
||
for (Comment comment : comments) { | ||
if (comment.getParentComment() != null) { | ||
// 부모 댓글이 있는 경우 | ||
Comment parentComment = map.get(comment.getParentComment().getId()); | ||
if (parentComment != null) { | ||
parentComment.getChildComments().add(comment); | ||
} | ||
} else { | ||
// 부모 댓글이 없는 경우 | ||
parentComments.add(comment); | ||
} | ||
map.put(comment.getId(), comment); | ||
} | ||
|
||
return parentComments; | ||
} | ||
|
||
private CommentResponseDTO mapToDTO(Comment comment) { | ||
if (comment.isDeleted()) { | ||
return CommentResponseDTO.fromDeleted(comment); | ||
} | ||
return CommentResponseDTO.from(comment); | ||
} | ||
|
||
private Member getMemberById(Long id) { | ||
return memberDAO.findById(id) | ||
.orElseThrow(() -> new SystemException(ClientErrorCode.MEMBER_NOT_FOUND_ERR)); | ||
} | ||
|
||
private Diary getDiaryById(Long id) { | ||
return diaryDAO.findById(id) | ||
.orElseThrow(() -> new SystemException(ClientErrorCode.DIARY_NOT_FOUND_ERR)); | ||
} | ||
|
||
private Comment getParentCommentById(Long id) { | ||
if (id == null) { | ||
return null; | ||
} | ||
return getCommentById(id); | ||
} | ||
|
||
private Comment getCommentById(Long id) { | ||
return commentDAO.findById(id) | ||
.orElseThrow(() -> new SystemException(ClientErrorCode.COMMENT_NOT_FOUND_ERR)); | ||
} | ||
|
||
private void validateCommentAuthor(Member member, Comment comment) { | ||
if (!member.equals(comment.getMember())) { | ||
throw new SystemException(ClientErrorCode.AUTHOR_MISMATCH_ERR); | ||
} | ||
} | ||
|
||
private void validateNotDeleted(Comment comment) { | ||
if (comment.isDeleted()) { | ||
throw new SystemException(ClientErrorCode.COMMENT_ALREADY_DELETED_ERR); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pageable을 사용할 때 default 개수를 설정 해 주는 것은 어떨까요? 몇 개 정도가 적당하다고 생각히시는지 궁금해요. 5개 정도면 괜찮으려나요?