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

[Dev] 23.01.29. 게시글 일괄 삭제

by 규글 2023. 1. 29.

 게시글을 삭제하는 기능은 사실 이미 구현되어 있다. 하지만 필자가 계획했던 것은 사용자의 마이 페이지에서 삭제하고 싶은 게시글을 일괄적으로 선택하여 삭제할 수 있도록 하는 기능도 있었다. 따라서 우선 해당 페이지를 먼저 구축해야 한다. 이후에 동작할 기능은 사실 동일한 것이기 때문이다. 게시글을 일괄적으로 삭제하는 기능에 대한 작업이지만, 사실 두 작업은 동일한 logic 으로 통일하게 될 것이다.

 

작업

menucheckbox.html

<!DOCTYPE html>
<html lang="en"
      xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<div class="menucheckbox" th:fragment="menucheckbox">
    <form action="/user/contents" id="contentsDeleteForm">
        <div class="row">
            <h2 class="col">내 글 삭제하기</h2>
            <button id="contentsDeleteBtn" class="btn btn-light float-end col-1">삭제</button>
        </div>
        <ul>
            <li class="menubox" th:each="list : ${recipeList}">
                <div class="stackBox">
                    <img src="/lib/image/emptydummy.png" alt="" class="deleteBox" style="width:200px;">
                    <input type="checkbox" class="deleteCheckBox"
                           name="ids" th:value="${list.contentId}"
                           style="width:200px; height: 200px;">
                </div>
                <h4 th:text="${list.title}">메뉴 이름</h4>
                <h4 th:text="${list.writer}"></h4>
            </li>
        </ul>
    </form>
</div>
</html>

 게시글을 일괄적으로 삭제하기 위한 template을 작성했다. 이는 기존에 게시글 목록을 불러오는 template에서 약간 수정된 것이다.

 

my-page.css

    .stackBox{
        position: relative;
    }
    .deleteBox{
        position: absolute;
        z-index: -1;
    }
    .deleteCheckBox{
        opacity: 0.5;
        z-index: 0;
    }

 위 template을 위한 css를 추가했다. 삭제할 게시글의 썸네일과 check box를 동일한 위치에서 보기 위한 것이다.

 

delete-form.html

<!DOCTYPE html>
<html lang="en"
      xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">

<div id="deleteForm" th:fragment="deleteForm" class="col templateDiv">
  <div th:replace="fragments/menucheckbox :: menucheckbox"></div>
</div>
</html>

 게시글을 삭제하기 위한 html template을 넣은 template이다. Template 안에 또다른 template이 있는 것이라고 할 수 있다. 이는 '글 삭제' 버튼을 눌렀을 때, 사용자가 작성한 게시글을 목록 자리를 대신할 부분이다.

 

my-page.js

    document.querySelector("#deleteBtn").addEventListener("click", function(e){
        var url = "/user/delete-form";

        var promise = fetch(url);

        promise.then(function(response){
            return response.text();
        }).then(function(data){
            var dom = new DOMParser();
            dom = dom.parseFromString(data, "text/html");
            console.log(dom);
            var newDiv = dom.getElementsByClassName("templateDiv")[0];
            console.log(newDiv);
            document.querySelector(".templateDiv").replaceWith(newDiv);

            addContentsDeleteBtnEvent();
        });
    });
    
    // 게시글 일괄 삭제 event 추가 function
    function addContentsDeleteBtnEvent(){
        document.querySelector("#contentsDeleteForm").addEventListener("submit", function(e){
            e.preventDefault();

            var url = this.action;
            var formData = new FormData(this);

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

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

            promise.then(function(response){
                return response.json();
            }).then(function(data){
                if(data.beDeleted){
                    alert("게시글을 삭제했습니다.");
                    location.href = "/user/my-page";
                } else {
                    alert("게시글을 삭제하지 못했습니다. 문제가 반복된다면 문의 바랍니다.");
                }
            });
        });
    }

 이전 프로필 변경에서와 동일한 방식으로 template을 불러오고, 해당 template에 대한 event 를 추가해주는 function을 작성해주었다.[각주:1]

 

PageController.java

    @GetMapping("/user/delete-form")
    @PreAuthorize("hasAuthority('ROLE_USER')")
    public String goDeleteForm(Model model,
                               @AuthenticationPrincipal SpUser spUser){

        Member member = spUser.toMember();
        model.addAttribute("recipeList", recipeService.getMyRecipeList(member));

        return "fragments/delete-form :: deleteForm";
    }

 요청에 대한 template을 불러오는 PageController의 method를 작성해주었다.

 

RecipeController.java

    @DeleteMapping("/user/contents")
    public ResponseEntity<Object> deleteContents(@RequestParam List<Long> ids){

        Map<String, Object> map = new HashMap<>();
        map.put("beDeleted", recipeService.deleteList(ids));

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

 게시글을 삭제하는 요청에 대한 controller의 method이다. 기존에 게시글을 삭제하던 method와 유일하게 다른 점이라고 한다면 method에 전달받는 인자이다. 기존에는 요청의 path variable을 받아서 해당 id에 대한 data를 삭제했지만, 여러 게시글을 한 번에 삭제하는 것이므로 client에서 check 한 항목의 id들을 List로 받아오도록 했다.

 

RecipeServiceImpl.java

    @Transactional
    @Override
    public boolean deleteList(List<Long> ids) {
        try {
            recipeRepository.deleteAllById(ids);

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

 역시 Repository의 method 만이 다르다. 기존에는 deleteById method를 사용했으나, deleteAllById method로 수정해주고 client 로부터 넘겨받은 List를 전달해주었다.

 

 

 기능은 생각한대로 동작했다. 그리고 같은 기능이기 때문에 게시글 하나를 삭제하는 경우에도 지금 만든 method를 활용하도록 변경했다. 동일한 동작인데 굳이 두 개의 method로 구분해야 할 필요가 있을까 싶다.

 다만 조금 걸리는 것은 동작하는 query 이다.

 

[동작 query]

 게시글을 삭제할 때 동작하는 query를 보면 List에 있는 id 각각에 대한 게시글, 링크, 댓글을 조회한 후에 댓글, 링크를 먼저 삭제하고 게시글을 마지막으로 삭제하는 query가 동작하는 것을 볼 수 있었다. Delete query는 각 게시글에 작성된 링크의 수와 댓글의 수만큼 각각 이루어지며, 이런 형식의 query 묶음이 삭제하려는 게시글의 수 만큼 동작한다. 지워야하는 게시글이 여러 개이고, 관련된 댓글의 수가 많은 경우 이와 같은 query 동작은 문제가 될 수 있을 것 같다. 특히 이런 delete query는 이어지는 회원 탈퇴 기능과도 관련된 것인 만큼 이에 대해 추가적인 수정이 필요할 것이라 생각한다.

 

 

 바로 이어서 회원 탈퇴 기능을 구현하고, 그 후에 동작하는 query를 살펴본 후에 delete query가 어떤 식으로 동작하는지 좋을 것인가에 대해 생각해보고 수정하는 작업을 해보겠다.

 

Reference

댓글