- DB: menu_info(9000000) parent_obj_id를 0 → -395553955 (사용자) 로 변경 → 상단 [사용자] 탭 안에 '모모유통' 그룹으로 표시 - /api/admin/menus/delete: cascade=true 시 자손까지 재귀 삭제 (rel_menu_auth 동시 정리) - admin-panel 메뉴 관리: 하위 메뉴가 있으면 사용자에게 confirm 후 일괄 삭제
This commit is contained in:
@@ -522,13 +522,30 @@ function MenuManagement() {
|
|||||||
if (!selected) { Swal.fire("알림", "삭제할 메뉴를 선택하세요.", "warning"); return; }
|
if (!selected) { Swal.fire("알림", "삭제할 메뉴를 선택하세요.", "warning"); return; }
|
||||||
const r = await Swal.fire({ title: "삭제 확인", text: `"${selected.MENU_NAME_KOR}" 메뉴를 삭제하시겠습니까?`, icon: "warning", showCancelButton: true, confirmButtonText: "삭제", cancelButtonText: "취소" });
|
const r = await Swal.fire({ title: "삭제 확인", text: `"${selected.MENU_NAME_KOR}" 메뉴를 삭제하시겠습니까?`, icon: "warning", showCancelButton: true, confirmButtonText: "삭제", cancelButtonText: "취소" });
|
||||||
if (!r.isConfirmed) return;
|
if (!r.isConfirmed) return;
|
||||||
const res = await fetch("/api/admin/menus/delete", {
|
|
||||||
method: "POST", headers: { "Content-Type": "application/json" },
|
const callDelete = async (cascade: boolean) => {
|
||||||
body: JSON.stringify({ objid: selected.OBJID }),
|
const res = await fetch("/api/admin/menus/delete", {
|
||||||
});
|
method: "POST", headers: { "Content-Type": "application/json" },
|
||||||
const data = await res.json();
|
body: JSON.stringify({ objid: selected.OBJID, cascade }),
|
||||||
|
});
|
||||||
|
return res.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = await callDelete(false);
|
||||||
|
if (!data.success && data.needsCascade) {
|
||||||
|
const r2 = await Swal.fire({
|
||||||
|
icon: "warning",
|
||||||
|
title: `하위 메뉴 ${data.childCount}개 함께 삭제`,
|
||||||
|
text: "선택한 메뉴와 모든 하위 메뉴를 함께 삭제합니다. 계속하시겠습니까?",
|
||||||
|
showCancelButton: true, confirmButtonText: "함께 삭제", cancelButtonText: "취소",
|
||||||
|
confirmButtonColor: "#dc2626",
|
||||||
|
});
|
||||||
|
if (!r2.isConfirmed) return;
|
||||||
|
data = await callDelete(true);
|
||||||
|
}
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
Swal.fire({ icon: "success", title: "삭제되었습니다.", timer: 1200, showConfirmButton: false });
|
Swal.fire({ icon: "success", title: `${data.deleted ?? 1}개 메뉴 삭제됨`, timer: 1500, showConfirmButton: false });
|
||||||
setSelected(null); setEditForm({}); setShowForm(false);
|
setSelected(null); setEditForm({}); setShowForm(false);
|
||||||
fetchData();
|
fetchData();
|
||||||
} else { Swal.fire("오류", data.message, "error"); }
|
} else { Swal.fire("오류", data.message, "error"); }
|
||||||
|
|||||||
@@ -1,29 +1,55 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { execute } from "@/lib/db";
|
import { pool } from "@/lib/db";
|
||||||
import { getSession } from "@/lib/session";
|
import { getSession } from "@/lib/session";
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const user = await getSession();
|
const user = await getSession();
|
||||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { objid } = body;
|
const { objid, cascade } = body as { objid?: string | number; cascade?: boolean };
|
||||||
if (!objid) return NextResponse.json({ success: false, message: "삭제할 메뉴를 선택하세요." });
|
if (!objid) return NextResponse.json({ success: false, message: "삭제할 메뉴를 선택하세요." });
|
||||||
|
|
||||||
|
const client = await pool.connect();
|
||||||
try {
|
try {
|
||||||
// 하위 메뉴가 있으면 삭제 불가
|
await client.query("BEGIN");
|
||||||
const { queryOne } = await import("@/lib/db");
|
const target = Number(objid);
|
||||||
const child = await queryOne<{ cnt: string }>(
|
|
||||||
`SELECT COUNT(*) AS cnt FROM menu_info WHERE parent_obj_id = $1`,
|
// 자손 모두 수집 (재귀)
|
||||||
[parseInt(objid, 10)]
|
const descendants = await client.query<{ objid: number }>(
|
||||||
|
`WITH RECURSIVE tree AS (
|
||||||
|
SELECT objid FROM menu_info WHERE objid = $1
|
||||||
|
UNION ALL
|
||||||
|
SELECT m.objid FROM menu_info m JOIN tree t ON m.parent_obj_id = t.objid
|
||||||
|
)
|
||||||
|
SELECT objid FROM tree`,
|
||||||
|
[target]
|
||||||
);
|
);
|
||||||
if (Number(child?.cnt || 0) > 0) {
|
|
||||||
return NextResponse.json({ success: false, message: "하위 메뉴가 있어 삭제할 수 없습니다. 하위 메뉴를 먼저 삭제하세요." });
|
const ids = descendants.rows.map((r) => r.objid);
|
||||||
|
const childCount = ids.length - 1;
|
||||||
|
|
||||||
|
// cascade 미지정인데 하위 있으면 거부
|
||||||
|
if (childCount > 0 && !cascade) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
message: `하위 메뉴 ${childCount}개가 있습니다. 함께 삭제하려면 다시 시도하세요.`,
|
||||||
|
childCount,
|
||||||
|
needsCascade: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await execute(`DELETE FROM menu_info WHERE objid = $1`, [parseInt(objid, 10)]);
|
// 권한-메뉴 매핑 정리 후 메뉴 삭제
|
||||||
return NextResponse.json({ success: true, message: "삭제되었습니다." });
|
await client.query(`DELETE FROM rel_menu_auth WHERE menu_obj_id = ANY($1::bigint[])`, [ids]);
|
||||||
|
const r = await client.query(`DELETE FROM menu_info WHERE objid = ANY($1::bigint[])`, [ids]);
|
||||||
|
await client.query("COMMIT");
|
||||||
|
return NextResponse.json({ success: true, deleted: r.rowCount, ids });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
console.error("Menu delete:", e);
|
console.error("Menu delete:", e);
|
||||||
return NextResponse.json({ success: false, message: "삭제 중 오류가 발생했습니다." });
|
const msg = e instanceof Error ? e.message : "삭제 중 오류";
|
||||||
|
return NextResponse.json({ success: false, message: msg }, { status: 500 });
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user