[ ※ 조건 ]
1. 대댓글은 부모 댓글의 하위에 표시되어야 한다.
2. 부모 댓글 리스트는 "생성 일자 기준 내림 차순으로 정렬"되어야 한다.
3. 자식 댓글 (대댓글) 리스트는 "생성 일자 기준 오름 차순으로 정렬"되어야 한다.
4. 자식 댓글은 자식 댓글을 가질 수 없다.
5. 부모 댓글과 자식 댓글 구분 없이 '수정' , '삭제' 기능을 보장해야 한다.
6. 부모 댓글이 자식 댓글을 가지고 있는 경우,
- 부모 댓글 삭제 -> 댓글 내용을 "삭제된 댓글 입니다." 로 변경한다.
- 자식 댓글 삭제 -> 그대로 삭제
[참고 자료 - 유튜브 댓글 기능]
[ Entity ) Comment.class ]
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment extends BaseEntity {
@Id
@GeneratedValue
@Column(name = "comment_id")
private Long id;
private String content;
private Integer likes = 0; //좋아요
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member; //작성자
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private Post post; //소속 post
/*부모 댓글 존재 여부
* 존재 - comment 객체
* 존재 X - null */
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Comment parent;
/*자식 댓글
* "orphanRemoval = true" : 부모 댓글이 삭제될때 자식 댓글도 삭제됨.*/
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Comment> children = new ArrayList<>();
/*삭제 여부*/
private Boolean isDeleted = false;
//생성자 생략
//comment content update
public void update(CommentRequestDto dto) {
if (dto.getContent() != null) {
this.content = dto.getContent();
}
}
//parent update
public void parentUpdate(Comment parent) {
this.parent = parent;
}
public void changeIsDeleted(Boolean isDeleted) {
this.isDeleted = isDeleted;
}
public void changeContent(String content) {
this.content = content;
}
//likes increment
public void incrementLikes() {
this.likes += 1;
}
//likes decrement
public void decrementLikes() {
if (this.likes > 0) {
this.likes -= 1;
}
}
}
[ Repository ) CommentRepository.interface ]
public interface CommentRepository extends JpaRepository<Comment, Long> {
/*특성 게시글(Post)에 소속된 댓글 목록*/
Page<Comment> findByPost(Post post, Pageable pageable);
/*특정 게시글에 소속된 부모 댓글 목록 (부모 댓글)*/
Page<Comment> findByPostAndParentIsNull(Post post, Pageable pageable);
/*특정 게시글 + 부모의 자식 댓글 목록 (자식 댓글)*/
Page<Comment> findByPostAndParent(Post post, Comment parent, Pageable pageable);
}
※ 샘플로 저장된 댓글 DB
- findByPostAndParentIsNull(Post post, Pageable pageable)
: 부모를 가지고 있지 않은 댓글 페이지
즉, '181' , '185' , '187' 을 포함한다.
- findByPostAndParent(Post post, Comment parent, Pageable pageable)
: 특정 부모 댓글의 자식 댓글 페이지
매개 변수 'parent' 가 '181' Comment 일 경우,
즉, '182' , '183', '184' 를 포함한다.
[ Service ) CommentService.class ]
/** Create Comment */
@Transactional
public Long createComment(CommentRequestDto dto) {
/*1. create entity*/
Comment comment = dto.toEntity();
/*if (부모 댓글이 있다면) - comment.parent 업데이트
* else - comment.parent = null 유지 */
if (dto.getParent() != null) {
comment.parentUpdate(dto.getParent());
}
/*2. save entity*/
Comment savedComment = commentRepository.save(comment);
return savedComment.getId();
}
저장시, comment.parent 필드에 따라 부모 댓글인지, 자식 댓글인지를 판단한다.
/** Read Comment */
/* id 기반 단건 조회 */
public Comment findCommentById(Long id) {
return commentRepository.findById(id)
.orElseThrow(() -> new DataNotFoundException("존재하지 않는 댓글입니다."));
}
/* 특정 Post's id 기반 리스트 조회 */
public Page<Comment> findCommentList(Long postId, Pageable pageable) {
/* 1. 특정 Post 찾기 */
Post post = postRepository.findById(postId)
.orElseThrow(() -> new DataNotFoundException("존재하지 않는 게시글입니다."));
/* 2. comment list 조회 */
return commentRepository.findByPost(post, pageable);
}
/* 부모 Comment List 조회
* Post ID + Parent Comment == null */
public Page<Comment> findParentList(Long postId, Pageable pageable) {
/* 1. 특정 Post 찾기 */
Post post = postRepository.findById(postId)
.orElseThrow(() -> new DataNotFoundException("존재하지 않는 게시글입니다."));
/* 2. comment list 조회 */
return commentRepository.findByPostAndParentIsNull(post, pageable);
}
/* 자식 Comment List 조회
* Post ID + Parent Comment ID */
public Page<Comment> findCommentListByParent(Long postId, Long parentId, Pageable pageable) {
/* 1. 특정 Post 찾기 */
Post post = postRepository.findById(postId)
.orElseThrow(() -> new DataNotFoundException("존재하지 않는 게시글입니다."));
/* 2. 부모 Comment 찾기 */
Comment parentComment = commentRepository.findById(parentId)
.orElseThrow(() -> new DataNotFoundException("존재하지 않는 댓글입니다."));
/* 3. comment list 조회 */
return commentRepository.findByPostAndParent(post, parentComment, pageable);
}
/** Delete Comment */
@Transactional
public Long deleteComment(Long id) {
/*1. delete 대상 comment 찾기*/
Comment comment = commentRepository.findById(id)
.orElseThrow(() -> new DataNotFoundException("존재하지 않는 댓글입니다."));
/*'작성자 == 로그인 회원' 확인 절차*/
if (!isAuthorValidation(comment.getId())) {
throw new UnauthorizedAccessException("잘못된 접근 - 현재 로그인 회원이 작성자와 일치하지 않습니다.");
}
/*삭제 주체
* 1) 자식이 있는 부모 댓글 - isDeleted = true 로 변경 -> content -> "삭제된 댓글 입니다."
* 2) (Leaf Node) 자식 댓글, 자식이 없는 부모댓글 - 즉시 삭제*/
if (!comment.getChildren().isEmpty()) {
comment.changeIsDeleted(true); //isDeleted = true 로 변경
comment.changeContent("삭제된 댓글 입니다.");
} else {
/*2. comment delete*/
commentRepository.delete(comment);
}
/*3. comment id return*/
return id;
}
삭제 시, 자식을 하나라도 가지고 있는다면 삭제 상태를 의미하는 'isDeleted' 필드를 'true' 로 변경하고, 'content' 를 "삭제된 댓글 입니다." 로 변경한다.
[ Controller ) PostController.class 中 ... ]
/**
* 게시글(Post) 상세 정보 - "/posts/{postId}"
* 해당 게시글의 댓글 목록 + 작성 폼
*/
@GetMapping("/{postId}")
public String postInfo(@PathVariable Long postId,
@Qualifier("mainPageable") @PageableDefault(sort = "id", size = 5, direction = Sort.Direction.DESC) Pageable pageable,
@Qualifier("childrenPageable") @PageableDefault(sort = "id", size = 5, direction = Sort.Direction.ASC) Pageable childrenPageable,
Model model) {
/* Header.html - 현재 로그인된 회원 */
Member currentMember = memberService.getCurrentMember();
model.addAttribute("memberId", currentMember.getId());
model.addAttribute("signedMember", currentMember.getUsername());
/* 게시글 상세정보 */
PostResponseDto postInfo = postService.getPostInfo(postId);
model.addAttribute("postInfo", postInfo);
/* 좋아요 여부 */
boolean isLiked = likesService.isLikedByMember(currentMember.getId(), postId);
model.addAttribute("isLiked", isLiked);
/**Comment List**/
/* 댓글 작성 폼 */
model.addAttribute("createCommentForm", new CommentRequestDto());
/* 댓글 목록 */
Page<Comment> parentList = commentService.findParentList(postId, pageable);
Page<CommentResponseDto> list = parentList.map(comment -> {
CommentResponseDto commentResponseDto = new CommentResponseDto(comment);
//로그인 회원이 댓글에 좋아요를 눌렀는가?
commentResponseDto.setLiked(commentLikesService.isLikedByMember(currentMember.getId(), comment.getId()));
//댓글 -> 내림차순 정렬 / 대댓글 -> 오름차순 정렬
Page<Comment> childrenComment = commentService.findCommentListByParent(postId, commentResponseDto.getId(), childrenPageable);
Page<ChildCommentDto> children = childrenComment.map(child -> {
ChildCommentDto childCommentDto = new ChildCommentDto(child);
//로그인 회원이 댓글에 좋아요를 눌렀는가?
childCommentDto.setLiked(commentLikesService.isLikedByMember(currentMember.getId(), child.getId()));
return childCommentDto;
});
commentResponseDto.setChildren(children);
return commentResponseDto;
});
model.addAttribute("comments", list); //comment dto list
model.addAttribute("previous", pageable.previousOrFirst().getPageNumber()); //이전 페이지 정보
model.addAttribute("next", pageable.next().getPageNumber()); //다음 페이지 정보
model.addAttribute("hasPrevious", list.hasPrevious()); //이전 페이지 존재 여부
model.addAttribute("hasNext", list.hasNext()); //다음 페이지 존재 여부
/* 페이지 번호 */
int currentPage = pageable.getPageNumber() + 1; //현재 페이지 정보(User side)
model.addAttribute("current", currentPage);
int blockSize = 5;
int startPage = ((currentPage - 1) / blockSize) * blockSize + 1; //블럭 시작 페이지
int endPage = Math.min(startPage + blockSize - 1, list.getTotalPages()); //블럭 마지막 페이지
model.addAttribute("startPage", startPage);
model.addAttribute("endPage", endPage);
return "posts/post-info";
}
댓글과 대댓글에 서로 다른 페이징 정렬 조건을 설정하여 적용한다.
※ @Qualifier() 를 사용하여 페이징 조건을 특정한다.
[완성 화면]
부모 댓글 Comment ID : 184
'184' 의 자식 댓글
- 자식 댓글 Comment ID : 185
- 자식 댓글 Comment ID : 186
대댓글이 생성일자의 오름차순으로 정렬된 것을 확인 할 수 있다.
'PROJECT > The Board' 카테고리의 다른 글
[The Board] 게시글 댓글 좋아요 기능 구현하기 (1) | 2024.07.08 |
---|---|
[TheBoard] The Board 프로젝트 환경 설정 + 프로젝트 선정 이유 (0) | 2024.07.01 |