"use client"; import useSWR from "swr"; import { useMemo, useState } from "react"; const fetcher = (url: string) => fetch(url).then((r) => r.json()); export default function AdminBoardsPage() { const { data: boardsResp, mutate: mutateBoards } = useSWR<{ boards: any[] }>("/api/admin/boards", fetcher); const { data: catsResp, mutate: mutateCats } = useSWR<{ categories: any[] }>("/api/admin/categories", fetcher); const boards = boardsResp?.boards ?? []; const categories = (catsResp?.categories ?? []).sort((a: any, b: any) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)); const groups = useMemo(() => { const map: Record = {}; for (const b of boards) { const cid = b.categoryId ?? "uncat"; if (!map[cid]) map[cid] = []; map[cid].push(b); } return categories.map((c: any) => ({ ...c, items: (map[c.id] ?? []).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)) })); }, [boards, categories]); const [savingId, setSavingId] = useState(null); async function save(b: any) { setSavingId(b.id); await fetch(`/api/admin/boards/${b.id}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify(b) }); setSavingId(null); mutateBoards(); } // DnD: 카테고리 순서 변경 async function reorderCategories(next: any[]) { // optimistic update await Promise.all(next.map((c, idx) => fetch(`/api/admin/categories/${c.id}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ sortOrder: idx + 1 }) }))); mutateCats(); } // DnD: 보드 순서 변경 (카테고리 내부) async function reorderBoards(categoryId: string, nextItems: any[]) { await Promise.all(nextItems.map((b, idx) => fetch(`/api/admin/boards/${b.id}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ sortOrder: idx + 1, categoryId }) }))); mutateBoards(); } return (

게시판 관리

{/* 대분류 리스트 (드래그로 순서 변경) */}
대분류
    {groups.map((g, idx) => ( { const arr = [...groups]; const [moved] = arr.splice(from, 1); arr.splice(to, 0, moved); reorderCategories(arr); }} onSave={async (payload) => { await fetch(`/api/admin/categories/${g.id}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) }); mutateCats(); }} /> ))}
{groups.map((g) => (
대분류: {g.name}
slug: {g.slug}
{g.items.map((b, i) => ( { const list = [...g.items]; const [mv] = list.splice(from, 1); list.splice(to, 0, mv); reorderBoards(g.id, list); }} > ))}
이름 slug 읽기 쓰기 익명 비밀댓 승인 유형 성인 정렬
))}
); } function Row({ b, onSave, saving }: { b: any; onSave: (b: any) => void; saving: boolean }) { const [edit, setEdit] = useState(b); return ( setEdit({ ...edit, name: e.target.value })} /> setEdit({ ...edit, slug: e.target.value })} /> setEdit({ ...edit, allowAnonymousPost: e.target.checked })} /> setEdit({ ...edit, allowSecretComment: e.target.checked })} /> setEdit({ ...edit, requiresApproval: e.target.checked })} /> setEdit({ ...edit, isAdultOnly: e.target.checked })} /> setEdit({ ...edit, sortOrder: Number(e.target.value) })} /> ); } function DraggableRow({ index, onMove, children }: { index: number; onMove: (from: number, to: number) => void; children: React.ReactNode }) { return ( { e.dataTransfer.setData("text/plain", String(index)); e.dataTransfer.effectAllowed = "move"; }} onDragOver={(e) => e.preventDefault()} onDrop={(e) => { e.preventDefault(); const from = Number(e.dataTransfer.getData("text/plain")); const to = index; if (!Number.isNaN(from) && from !== to) onMove(from, to); }} className="align-middle" > {children} ); } function CategoryRow({ idx, g, onMove, onSave }: { idx: number; g: any; onMove: (from: number, to: number) => void; onSave: (payload: any) => void }) { const [edit, setEdit] = useState({ name: g.name, slug: g.slug }); return (
  • { e.dataTransfer.setData("text/plain", String(idx)); e.dataTransfer.effectAllowed = "move"; }} onDragOver={(e) => e.preventDefault()} onDrop={(e) => { e.preventDefault(); const from = Number(e.dataTransfer.getData("text/plain")); const to = idx; if (!Number.isNaN(from) && from !== to) onMove(from, to); }} >
    setEdit({ ...edit, name: e.target.value })} /> setEdit({ ...edit, slug: e.target.value })} />
  • ); }