본문 바로가기
프로젝트/Recipository

[Dev] 22.12.17. 댓글 삭제

by 규글 2022. 12. 17.

 구상 초기에는 댓글 삭제의 경우 댓글을 삭제할 경우 그에 대한 대댓글까지 한 번에 삭제하려고 했다. 대댓글까지 한 번에 지워주지 않으면 그 대댓글이 위쪽에 위치한 댓글의 대댓글로 탈바꿈되기 때문이다. 그런데 개발에 앞서 고민해보니 댓글을 삭제할 때 타인이 작성한 댓글까지 삭제하는 것이 옳은가에 대한 의문이 생겼다. 그래서 댓글을 삭제하지 않고 comment만을 삭제된 내용이라는 것으로 출력하도록 수정하는 방식을 택하기로 했다.

 

 

작업

comment.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<div th:fragment="normal(comment)"
     th:class="|row reply groupId${comment.groupId}|"
     th:id="|normalComment${comment.commentId}|">
    <div class="flex-row">
        <span th:text="${comment.writer}"></span>
        <span th:text="|/ ${comment.regDate}|"></span>
    </div>
    <input type="text" th:id="|input${comment.commentId}|" class="form-control col"
           style="background-color: white;" th:value="${comment.comment}" disabled>
    <button th:if="${comment.beDeleted} == false"
            th:data-num="${comment.groupId}"
            th:data-text="${comment.writer}"
            th:id = "|reply${comment.commentId}|"
            sec:authorize="isAuthenticated()"
            class="btn btn-light float-end col-1 replyBtn" type="submit"
            style="width: 70px;">답글</button>
    <a th:if="${comment.writer} == ${#authentication.name} and ${comment.beDeleted} == false"
            th:data-num="${comment.commentId}"
            th:id = "|delete${comment.commentId}|"
            th:href="@{/user/del-comment/{commentId}(commentId=${comment.commentId})}"
            class="btn btn-light float-end col-1 commentDeleteBtn" type="submit"
            style="width: 70px;">삭제</a>
</div>
<div th:fragment="reply(comment)"
     th:class="|row reply groupId${comment.groupId}|"
     th:id="|replyComment${comment.commentId}|">
    <svg xmlns="http://www.w3.org/2000/svg" width="29" height="29"
         fill="currentColor" class="bi bi-arrow-return-right col-1" viewBox="0 0 16 16"
         style="margin-top: 25px;">
        <path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5
                    2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5
                    0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z"/>
    </svg>
    <div class="row col" style="padding: 0px;">
        <div class="flex-row">
            <span th:text="${comment.writer}"></span>
            <span th:text="|/ ${comment.regDate}|"></span>
        </div>
        <input type="text" th:id="|input${comment.commentId}|" name="" class="form-control col"
               style="background-color: white;" th:value="${comment.comment}" disabled>
        <button th:if="${comment.beDeleted} == false"
                th:data-num="${comment.groupId}"
                th:data-text="${comment.writer}"
                th:id = "|reply${comment.commentId}|"
                sec:authorize="isAuthenticated()"
                class="btn btn-light float-end col-1 replyBtn" type="submit"
                style="width: 70px;">답글</button>
        <a th:if="${comment.writer} == ${#authentication.name} and ${comment.beDeleted} == false"
                th:data-num="${comment.commentId}"
                th:id = "|delete${comment.commentId}|"
                th:href="@{/user/del-comment/{commentId}(commentId=${comment.commentId})}"
                class="btn btn-light float-end col-1 commentDeleteBtn" type="submit"
                style="width: 70px;">삭제</a>
    </div>
</div>
</html>

 comment.html 의 댓글과 대댓글에 대한 fragment에 대해서 약간의 수정을 했다. 게시글을 삭제했을 때 화면에서 두 버튼을 지워주기 위해서 각각에 id를 부여했다. 그리고 아예 anchor의 href 속성에 지워야 할 댓글의 coment id도 부여해주었다.

 

comment.js

    // 댓글 삭제 시 동작
    document.querySelectorAll(".commentDeleteBtn").forEach(tmp => {
        tmp.addEventListener("click", function(e){
            e.preventDefault();

            // 삭제 의도를 파악한 후
            var commentDelete = confirm("댓글을 삭제하시겠습니까?");

            if(commentDelete){
                var url = this.href;

                var token = document.querySelector("meta[name=_csrf]").content;
                var header = document.querySelector("meta[name=_csrf_header]").content;

                var promise = fetch(url, {
                    method: "PUT",
                    headers: {
                        "header": header,
                        "X-Requested-With": "XMLHttpRequest",
                        "X-CSRF-Token": token
                    }
                });

                promise.then(function(response){
                    return response.json();
                }).then(function(data){
                    // 삭제 성공 여부에 따라 다른 동작을 하도록
                    if(data.beDeleted){
                        alert("댓글을 삭제하였습니다.");
                        document.querySelector("#input" + data.commentId).value = "삭제된 댓글입니다.";
                        document.querySelector("#reply" + data.commentId).remove();
                        document.querySelector("#delete" + data.commentId).remove();
                    } else {
                        alert("댓글 삭제에 실패하였습니다. 문제가 반복된다면 문의 바랍니다.");
                    }
                });
            }
        });
    });

 Fragment에서 작성한 anchor를 눌렀을 때 동작을 javascript로 작성한 것이다. 삭제 여부를 다시 파악 후에 fetch로 작성한 url에 대한 PUT request를 보내고, 그 결과를 받아서 성공 시 input의 value를 바꾸면서 댓글의 두 버튼을 지워주는 동작을 작성해주었다.

 

CommentController.java

    // 댓글 삭제 (사실상 내용만 수정)
    @PutMapping("/user/del-comment/{commentId}")
    @PreAuthorize("hasAuthority('ROLE_USER')")
    public ResponseEntity<Object> delComment(@PathVariable Long commentId){

        // Map에 삭제한 댓글 id와 삭제 성공 여부를 담아 return
        Map<String, Object> map = new HashMap<>();
        map.put("commentId", commentId);
        map.put("beDeleted", commentService.delComment(commentId));

        return ResponseEntity.ok().body(map);
    }

 PUT request를 보낸 것이니 controller의 method에 @PutMapping annotation을 붙였고, @PathVariable annotation과 함께 url로 path variable을 받았다. Path variable 로 전달받은 댓글의 id를 service로 넘겨 삭제 logic을 거친 뒤 받은 결과를 Map에 담아 return 하도록 했다. 이렇게 return된 data가 client에서 fetch 후에 response로 사용된다.

 

CommentServiceImpl.java

    // 댓글 삭제 (사실상 수정)
    @Override
    public boolean delComment(Long commentId) {
        try{
            // 기존 댓글의 data를 불러와서 comment 내용을 바꿔서 다시 save(update)
            Comment comment = commentRepository.findById(commentId).get();
            comment.updateComment(true);
            commentRepository.save(comment);

            return true;
        } catch(Exception e){
            return false;
        }
    }

 댓글 삭제라고 했지만, 사실상 수정하는 작업이다. 기존의 댓글 정보는 그대로 남겨두고 'beDeleted' column만 false에서 true로 수정하도록 했다. 단순히 save만 수행해도 select query문 후에 insert나 update query가 수행되지만, 작성한대로 따로 findById를 수행하도록 해도 select가 두 번 수행되는 것은 아니다. 오류 없이 logic이 수행되면 true를 return 하고, 오류가 발생하면 false를 return 하도록 하여 controller에서 성공 여부를 넘길 수 있도록 했다.

 

Comment.java

    private boolean beDeleted;

    public CommentDto.CommentResponseDto toDto(){
        // 댓글을 return 할 String을 만들어두고
        String comment = "";
        // 삭제되었다면 삭제 정보를 넣고
        if(this.beDeleted){
            comment = "삭제된 댓글입니다.";
        // 아니라면 그대로 return
        } else {
            comment = this.comment;
        }

        return CommentDto.CommentResponseDto.builder()
                .commentId(this.commentId)
                .writer(this.writer)
                .comment(comment)
                .groupId(this.groupId)
                .regDate(super.getRegDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
                .beDeleted(this.beDeleted)
                .build();
    }

    public void updateComment(boolean beDeleted){
        this.beDeleted = beDeleted;
    }

 이때 client에서는 해당 댓글이 삭제되었는지 여부를 체크하기 위해 'beDeleted' 라는 column을 추가했다. 또한 그 true/false 여부에 따라 댓글에 존재하던 답글 및 삭제 버튼이 보이지 않도록 하는 데에 활용되며, 댓글의 내용도 삭제되었음을 출력하도록 할 수 있다. 새롭게 column을 추가하였으니, 그 data를 response 하는 ResponseDto에도 field를 추가해주었다.

 이때 기존 comment의 data를 그대로 return 하지 않도록 Comment Entity에서 Dto로 변환힐 때, beDeleted column의 true / false 여부에 따라 다른 data를 출력하도록 구분했다. 이렇게 하지 않고 모든 data를 return 하고, beDeleted column의 값에 따라 client에서 구분하려고 한다면 기존의 data를 노출하는 것이라고 생각하여 애초에 server 측에서 data를 custom 한 것이다.

 

 

 이렇게 댓글을 삭제하는 작업을 마무리했다. 댓글을 수정하는 작업은 따로 구현하지 않으려고 한다. SNS의 경우도 댓글을 수정할 수 있는 경우와 없는 경우로 구분할 수 있는데, 필자는 그 중에서 인스타그램처럼 댓글을 수정할 수 없는 경우를 택한 것이다.

 

 

 

댓글