win-j의 자유로운 블로그

개발일지 - 카테고리 관리페이지 수정! (AJAX + SortableJS)

개발일지 · 2026.03.24 · 조회 1


기존 카테고리는 단일 카테고리 하나만 가지고 있는 카테고리에서 이제는 하위 카테고리를 지정할 수 있도록 수정했습니다.




관리자 페이지에서 카테고리를 추가, 수정, 제거 기능을 만들었고 여기에 메인 카테고리 그리고 하단에 서브 카테고리 형식으로 만들어 프로그래밍 카테고리 밑에 개발일지 카테고리를 넣을 수 있도록 수정했습니다. 카테고리 순서나 위치 지정은 마우스로 드래그 해서 순서를 변경할 수 있도록 했고 여기에 사용된 기능은 SortableJS 와 AJAX를 사용했습니다.





<ul id="category-list">
    {% for category in categories %}
        <li data-id="{{ category.id }}">
            {{ category.name }}
        </li>
    {% endfor %}
</ul>

<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>

<script>
new Sortable(document.getElementById('category-list'), {
    animation: 150,
    onEnd: function () {

        let order = [];

        document.querySelectorAll('#category-list li').forEach((el, index) => {
            order.push({
                id: el.dataset.id,
                position: index
            });
        });

        fetch("/manager/category/sort/", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "X-CSRFToken": "{{ csrf_token }}"
            },
            body: JSON.stringify({ order: order })
        });
    }
});
</script>


SortableJS를 활용한 카테고리 프론트 설정입니다.



<!-- 카테고리 리스트 -->
<ul id="category-list">
    {% for category in categories %}
        <!-- 
            data-id:
            각 카테고리의 고유 ID를 HTML에 심어둔다.
            → 드래그 후 서버로 순서 저장할 때 필요
        -->
        <li data-id="{{ category.id }}">
            {{ category.name }}
        </li>
    {% endfor %}
</ul>

<!-- SortableJS 라이브러리 CDN -->
<!-- 드래그 정렬 기능을 쉽게 구현하기 위한 라이브러리 -->
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>

<script>
/*
    Sortable 객체 생성

    첫 번째 인자:
    → 드래그 대상이 될 DOM 요소 (ul)

    옵션 객체:
    → 동작 커스터마이징
*/
new Sortable(document.getElementById('category-list'), {

    // 드래그 이동 시 애니메이션 속도 (ms)
    animation: 150,

    /*
        onEnd 이벤트:
        드래그가 끝났을 때 실행됨
        → 순서 변경이 완료된 시점
    */
    onEnd: function () {

        // 최종 순서를 담을 배열
        let order = [];

        /*
            현재 화면에 보이는 순서대로 li를 순회
            index = 현재 위치 (0부터 시작)
        */
        document.querySelectorAll('#category-list li').forEach((el, index) => {

            /*
                각 요소의 정보를 객체로 저장
                id: 카테고리 ID (data-id에서 가져옴)
                position: 현재 위치 (정렬된 순서)
            */
            order.push({
                id: el.dataset.id,
                position: index
            });
        });

        /*
            서버로 정렬 결과 전송 (AJAX)

            URL:
            /manager/category/sort/

            방식:
            POST (데이터 변경이므로 GET이 아닌 POST 사용)
        */
        fetch("/manager/category/sort/", {
            method: "POST",

            headers: {
                // JSON 데이터 전송 명시
                "Content-Type": "application/json",

                // Django CSRF 보호를 위한 토큰
                "X-CSRFToken": "{{ csrf_token }}"
            },

            /*
                body:
                order 배열을 JSON 문자열로 변환하여 전송

                예시 데이터:
                {
                    "order": [
                        {"id": 3, "position": 0},
                        {"id": 1, "position": 1},
                        {"id": 2, "position": 2}
                    ]
                }
            */
            body: JSON.stringify({ order: order })
        });

        // (선택) 성공/실패 처리도 추가 가능
        // .then(response => response.json())
        // .then(data => console.log(data))
        // .catch(error => console.error(error));
    }
});
</script>


다음으로 Django View에서 순서를 저장하는 기능


import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from .models import Category

@csrf_exempt
def category_sort(request):
    if request.method == "POST":
        data = json.loads(request.body)
        for item in data["order"]:
            Category.objects.filter(id=item["id"]).update(order=item["position"])
        return JsonResponse({"status": "ok"})


여기서 @csrf_exempt 사용은 fetch를 사용하기 위해 반드시 사용해야 합니다.

CSRF (Cross-Site Request Forgery) 사용자가 로그인된 상태를 악용해서 의도치 않은 요청을 서버에 보내게 만드는 공격에 대비해서 서버가 토큰을 발급하고 클라이언트가 요청에 포함하여 서버 일치 여부를 확인함으로 쿠키 기반 인증 + POST 요청에서는 필수로 CSRF를 사용하는 것이 좋다.



기능의 추가 이후에는 UX 디자인을 꾸며주기만 하면 끝납니다.