feat: 모모유통 메뉴를 [사용자] 그룹 아래로 이동 + 메뉴 삭제 cascade 지원
Deploy momo-erp / deploy (push) Successful in 46s

- 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:
chpark
2026-04-25 23:51:30 +09:00
parent e8dc97a32f
commit dca5675009
2 changed files with 61 additions and 18 deletions
+23 -6
View File
@@ -522,13 +522,30 @@ function MenuManagement() {
if (!selected) { Swal.fire("알림", "삭제할 메뉴를 선택하세요.", "warning"); return; }
const r = await Swal.fire({ title: "삭제 확인", text: `"${selected.MENU_NAME_KOR}" 메뉴를 삭제하시겠습니까?`, icon: "warning", showCancelButton: true, confirmButtonText: "삭제", cancelButtonText: "취소" });
if (!r.isConfirmed) return;
const res = await fetch("/api/admin/menus/delete", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ objid: selected.OBJID }),
});
const data = await res.json();
const callDelete = async (cascade: boolean) => {
const res = await fetch("/api/admin/menus/delete", {
method: "POST", headers: { "Content-Type": "application/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) {
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);
fetchData();
} else { Swal.fire("오류", data.message, "error"); }
+38 -12
View File
@@ -1,29 +1,55 @@
import { NextRequest, NextResponse } from "next/server";
import { execute } from "@/lib/db";
import { pool } from "@/lib/db";
import { getSession } from "@/lib/session";
export async function POST(request: NextRequest) {
const user = await getSession();
if (!user) return NextResponse.json({ success: false }, { status: 401 });
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: "삭제할 메뉴를 선택하세요." });
const client = await pool.connect();
try {
// 하위 메뉴가 있으면 삭제 불가
const { queryOne } = await import("@/lib/db");
const child = await queryOne<{ cnt: string }>(
`SELECT COUNT(*) AS cnt FROM menu_info WHERE parent_obj_id = $1`,
[parseInt(objid, 10)]
await client.query("BEGIN");
const target = Number(objid);
// 자손 모두 수집 (재귀)
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) {
await client.query("ROLLBACK");
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();
}
}