점프 투 스프링부트 추가 기능 구현 여덟번째, 질문 및 답변 댓글 기능 구현
구현한 기능
2, 6번의 경우 타임리프 조건으로 구현하였습니다.
위 기능 중 4번 삭제 관련해서 자세히 다루고, 나머지 기능 및 코드들은 GitHub PR에 설명을 남겨두었으니 참고하시고 코드 보시면 이해가 가지 않을까 싶습니다!
누르면 아래와 같습니다. 로그인을 해야 댓글 작성이 가능합니다.
댓글 삭제 관련한 로직은 아래 이미지와 같습니다.
JPA에서 영속성 컨텍스트를 활용하기 때문에 하나의 URI 요청내에서 데이터를 변경하고 다시 조회할 때 영속성 컨텍스트를 활용하여 데이터를 가져옵니다.
대댓글만 삭제
한다고 했을 때, 댓글을 조회하는 새로운 URI요청(새로고침 등)이 있지 않으면 댓글에 연결된 대댓글 리스트에서 제거되지 않은 댓글 목록 조회됨
따라서 댓글을 삭제하고 조회하여 댓글 목록을 반환하는 두 기능을 하나의 메서드에서 처리하기 위해 부모 댓글의 자식 댓글 List에서 remove를 수행하여 리스트에서 제거된 목록을 반환하였습니다.
case 4, 5의 경우만 연관관계를 고려하면 됩니다!
CommentController.java
// 댓글 삭제 메서드
@PreAuthorize("isAuthenticated()")
@PostMapping("/delete/{type}")
public String delete(Model model, CommentForm commentForm, Principal principal, @PathVariable String type) {
Question question = questionService.getQuestion(commentForm.getQuestionId());
SiteUser user = userService.getUser(principal.getName());
// 관리자가 아니거나 현재 로그인한 사용자가 작성한 댓글이 아니면 삭제 불가
if (!(user.isAdmin()) && (user.getId() != commentForm.getCommentWriter())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "삭제 권한이 없습니다");
}
Comment comment = commentService.getComment(commentForm.getId());
// 댓글이 속한 페이지 번호
int page = 0;
Answer answer = null;
if (type.equals("question")) {
page = commentService.getPageNumberByQuestion(question, comment, PAGESIZE);
}
else {
answer = answerService.getAnswer(commentForm.getAnswerId());
page = commentService.getPageNumberByAnswer(answer, comment, PAGESIZE);
}
// 부모(댓글)이 있을 경우 연관관계 끊어주기 -> 삭제되더라도 GET 등으로 새로 요청을 보내는 것이 아니기에
// 이 작업은 꼭 해줘야 대댓글 리스트도 수정된다!
// 부모댓글이 삭제 되지 않았다면 연관관계 끊어주기만 하면 됨
// => Ajax 비동기 리스트화를 위해 리스트에서 명시적 삭제
if (comment.getParent() != null && !comment.getParent().isDeleted()) {
comment.getParent().getChildren().remove(comment);
}
// 부모댓글이 삭제 상태이고 부모의 자식 댓글이 본인 포함 2개 이상이라면
// 자식 댓글의 삭제가 부모 댓글 객체 삭제에 영향을 주지 않으니 연관관계만 끊어주기
// => Ajax 비동기 리스트화를 위해 리스트에서 명시적 삭제
else if (comment.getParent() != null && comment.getParent().isDeleted()
&& comment.getParent().getChildren().size() > 1) {
comment.getParent().getChildren().remove(comment);
}
commentService.delete(comment);
model.addAttribute("question", question);
Page<Comment> paging;
if (type.equals("question")) {
paging = commentService.getCommentPageByQuestion(page, question);
// 만일 삭제 전이 6개 -> 삭제하면 5개 -> 0패이지가 보여져야 하는데, 삭제 전에 page를 계산하여 1페이지가 보여짐.
// 여기서 조건을 검사해줘야 함, 현재 페이지 개수가 0개면 이전 페이지 이동, 단 현재 페이지가 0인데도 개수가 0개?
// 그러면 댓글이 아에 없으니 그냥 0페이지 표기!
// 삭제하기 전에 page를 구한 이유는 댓글이 삭제되면 삭제한 댓글이 원래 어디 페이지에 있는지 검사가 안되기 때문
if(page !=0 && paging.getNumberOfElements() == 0)
paging = commentService.getCommentPageByQuestion(page-1, question);
model.addAttribute("questionCommentPaging", paging);
// 전체 댓글 수 갱신
model.addAttribute("totalCount", paging.getTotalElements());
return "comment/question_comment :: #question-comment-list";
} else {
paging = commentService.getCommentPageByAnswer(page, answer);
if((page !=0 && paging.getNumberOfElements() == 0))
paging = commentService.getCommentPageByAnswer(page-1, answer);
model.addAttribute("answer", answer);
model.addAttribute("answerCommentPaging", paging);
// 전체 댓글 수 갱신
model.addAttribute("totalCount", paging.getTotalElements());
return "comment/answer_comment :: #answer-comment-list";
}
}
@Transactional
public void delete(Comment comment) {
if (comment == null) {
throw new IllegalArgumentException("Comment cannot be null");
}
if (comment.getChildren().size() != 0) {
// 자식이 있으면 삭제 상태만 변경
comment.deleteParent();
} else { // 자식이 없다 -> 대댓글이 없다 -> 객체 그냥 삭제해도 된다.
// 삭제 가능한 조상 댓글을 구해서 삭제
// ex) 할아버지 - 아버지 - 대댓글, 3자라 했을 때 대댓글 입장에서 자식이 없으니 삭제 가능
// => 삭제하면 아버지도 삭제 가능 => 할아버지도 삭제 가능하니 이런식으로 조상 찾기 메서드
Comment tmp = getDeletableAncestorComment(comment);
commentRepository.delete(tmp);
}
}
@Transactional
public Comment getDeletableAncestorComment(Comment comment) {
Comment parent = comment.getParent(); // 현재 댓글의 부모를 구함
if (parent != null && parent.getChildren().size() == 1 && parent.isDeleted() == true) {
// 부모가 있고, 부모의 자식이 1개(지금 삭제하는 댓글)이고, 부모의 삭제 상태가 TRUE인 댓글이라면 재귀
// 삭제가능 댓글 -> 만일 댓글의 조상(대댓글의 입장에서 할아버지 댓글)도 해당 댓글 삭제 시 삭제 가능한지 확인
// Ajax로 비동기로 리스트 가져오기에, 대댓글 1개인거 삭제할 때 연관관계 삭제하고 부모 댓글 삭제하기 필요
// 컨트롤러가 아닌 서비스의 삭제에서 처리해주는 이유는 연관관계를 삭제해주면 parent를 구할 수 없기에 여기서 끊어줘야 함
parent.getChildren().remove(comment);
return getDeletableAncestorComment(parent);
}
return comment;
}
getDeletableAncestorComment 메서드를 통해 삭제하려는 댓글이 대댓글이며, 부모 댓글이 삭제상태인 댓글을 찾습니다.
부모 객체를 삭제하면 자식 객체는 고아 객체가 되어 자동 삭제 됩니다.
Comment.java
// OrphanRemoval로 부모 댓글이 삭제되면 자식 댓글 모두 삭제되도록 구현
@OneToMany(mappedBy = "parent", orphanRemoval = true)
@ToString.Exclude
@Builder.Default // 빌더패턴 리스트시 초기화
private List<Comment> children = new ArrayList<>();
상세 코드를 확인하고 싶은 분들은 Git-PR를 참조해주세요.
참고자료 1 : [Spring Boot] 대댓글 기능 만들기
참고자료 2 : Thymeleaf - ajax를 이용해 비동기식 화면 수정