feat(admin/auth): 권한관리 통합 화면 — 권한그룹/직원/메뉴 동시 매핑
Deploy momo-erp / deploy (push) Failing after 37s

[권한그룹 사용자 추가 SQL 에러 fix]
- $3 파라미터에 ::text 명시 캐스트로 inconsistent types 해결
  ("inconsistent types deduced for parameter $3")

[새 UI - admin-panel/auth]
- 좌측: 권한 목록 + 검색 + 생성 (목록에서 클릭으로 활성화, 더블클릭으로 수정/삭제)
- 우중·우우: 권한있는/권한없는 직원 패널 (체크박스 + 전체선택 + 검색)
  · ‹ 추가  / 제거 › 버튼 즉시 반영
- 하단: 메뉴 전체 트리 (체크 즉시 서버 반영)
- 모달 헬퍼 안 띄우고 한 화면에서 모두 처리 → 사용 흐름 단순화

[새 스키마/API]
- db/migrations/020_authority_sub_menu.sql
- /api/admin/auth/menus  : 그룹의 메뉴 OBJID + 전체 메뉴 트리
- /api/admin/auth/menus/toggle : 단일 메뉴 ON/OFF

[거래명세표]
- 수량 컬럼 너비 w-14 → w-20 (모바일에서 잘리던 문제)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chpark
2026-05-08 14:36:16 +09:00
parent 004a8e4a6b
commit a3ab0d7629
6 changed files with 419 additions and 4 deletions
+14
View File
@@ -0,0 +1,14 @@
-- 권한그룹 ↔ 메뉴 매핑 테이블
-- 권한 관리 화면에서 그룹별로 노출 메뉴를 체크박스로 매핑하기 위함
CREATE TABLE IF NOT EXISTS authority_sub_menu (
objid numeric PRIMARY KEY,
master_objid numeric NOT NULL,
menu_objid numeric NOT NULL,
writer varchar(100),
regdate timestamp without time zone DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_authority_sub_menu_master ON authority_sub_menu(master_objid);
CREATE INDEX IF NOT EXISTS idx_authority_sub_menu_menu ON authority_sub_menu(menu_objid);
CREATE UNIQUE INDEX IF NOT EXISTS uq_authority_sub_menu_pair ON authority_sub_menu(master_objid, menu_objid);
+1 -1
View File
@@ -577,7 +577,7 @@ function StatementPreview({
<th className="border border-slate-300 px-1.5 py-1.5 text-left"></th> <th className="border border-slate-300 px-1.5 py-1.5 text-left"></th>
<th className="border border-slate-300 px-1.5 py-1.5 w-12"></th> <th className="border border-slate-300 px-1.5 py-1.5 w-12"></th>
<th className="border border-slate-300 px-1.5 py-1.5 w-14 js-no-export"></th> <th className="border border-slate-300 px-1.5 py-1.5 w-14 js-no-export"></th>
<th className="border border-slate-300 px-1.5 py-1.5 w-14"></th> <th className="border border-slate-300 px-1.5 py-1.5 w-20"></th>
<th className="border border-slate-300 px-1.5 py-1.5 w-20"></th> <th className="border border-slate-300 px-1.5 py-1.5 w-20"></th>
<th className="border border-slate-300 px-1.5 py-1.5"></th> <th className="border border-slate-300 px-1.5 py-1.5"></th>
<th className="border border-slate-300 px-1.5 py-1.5"></th> <th className="border border-slate-300 px-1.5 py-1.5"></th>
+326 -1
View File
@@ -800,9 +800,334 @@ function MenuManagement() {
} }
// ========================================== // ==========================================
// 권한 관리 (authMngList.jsp 대응) // 권한 관리 — 좌(목록) / 중(있는·없는 직원) / 하(메뉴 트리) 통합 화면
// ========================================== // ==========================================
interface AuthGroup { OBJID: string; AUTH_NAME: string; AUTH_CODE: string; USER_CNT: number; STATUS: string }
interface AuthUserRow { USER_ID: string; USER_NAME: string; DEPT_NAME?: string; OBJID?: string }
interface MenuRow { OBJID: string; PARENT_OBJ_ID: string; MENU_NAME_KOR: string; MENU_URL: string; SEQ: number; MENU_TYPE: number; STATUS: string }
function AuthManagement() { function AuthManagement() {
const [groups, setGroups] = useState<AuthGroup[]>([]);
const [groupQuery, setGroupQuery] = useState("");
const [activeGroup, setActiveGroup] = useState<AuthGroup | null>(null);
// 좌측 권한 목록 검색
const filteredGroups = useMemo(() => groups.filter((g) =>
!groupQuery || g.AUTH_NAME.toLowerCase().includes(groupQuery.toLowerCase()) || g.AUTH_CODE?.toLowerCase().includes(groupQuery.toLowerCase())
), [groups, groupQuery]);
// 권한 그룹 목록 조회
const fetchGroups = useCallback(async () => {
const res = await fetch("/api/admin/auth", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" });
const j = await res.json();
setGroups((j.RESULTLIST || []) as AuthGroup[]);
}, []);
useEffect(() => { fetchGroups(); }, [fetchGroups]);
// 권한 그룹 생성/이름 수정/삭제 (인라인)
const onCreate = async () => {
const r = await Swal.fire({
title: "권한그룹 생성",
html: `<input id="sw_name" class="swal2-input" placeholder="권한명 (예: 영업팀 권한)">
<input id="sw_code" class="swal2-input" placeholder="권한CODE (예: SALES_TEAM)">`,
showCancelButton: true, confirmButtonText: "생성",
preConfirm: () => ({
auth_name: (document.getElementById("sw_name") as HTMLInputElement).value,
auth_code: (document.getElementById("sw_code") as HTMLInputElement).value,
}),
});
if (!r.isConfirmed || !r.value?.auth_name) return;
const res = await fetch("/api/admin/auth/save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...r.value, status: "active", actionType: "regist" }) });
const j = await res.json();
if (j.success) { fetchGroups(); }
else Swal.fire("오류", j.message ?? "생성 실패", "error");
};
const onRename = async (g: AuthGroup) => {
const r = await Swal.fire({
title: "권한 그룹 수정", icon: "info",
html: `<input id="sw_name" class="swal2-input" value="${g.AUTH_NAME}">
<input id="sw_code" class="swal2-input" value="${g.AUTH_CODE ?? ""}">`,
showCancelButton: true, showDenyButton: true, denyButtonText: "삭제", confirmButtonText: "저장",
preConfirm: () => ({
auth_name: (document.getElementById("sw_name") as HTMLInputElement).value,
auth_code: (document.getElementById("sw_code") as HTMLInputElement).value,
}),
});
if (r.isDenied) {
const ok = await Swal.fire({ icon: "warning", title: "권한그룹 삭제", text: g.AUTH_NAME, showCancelButton: true });
if (!ok.isConfirmed) return;
const res = await fetch("/api/admin/auth/delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ objid: g.OBJID }) });
if ((await res.json()).success) { if (activeGroup?.OBJID === g.OBJID) setActiveGroup(null); fetchGroups(); }
return;
}
if (!r.isConfirmed || !r.value?.auth_name) return;
const res = await fetch("/api/admin/auth/save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ objid: g.OBJID, ...r.value, status: g.STATUS }) });
if ((await res.json()).success) fetchGroups();
};
return (
<div className="space-y-3">
<div>
<h2 className="text-lg font-bold text-slate-900"> </h2>
<p className="text-xs text-slate-500 mt-0.5"> / , .</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-[260px_1fr] gap-3">
{/* 좌측: 권한 그룹 목록 */}
<div className="bg-white border border-slate-200 rounded-xl flex flex-col">
<div className="px-3 py-2 border-b border-slate-100 flex items-center justify-between">
<div className="text-sm font-bold text-slate-700 inline-flex items-center gap-1.5"><Shield size={14} className="text-emerald-700" /> </div>
<button onClick={onCreate} className="inline-flex items-center gap-1 h-7 px-2.5 rounded bg-emerald-600 text-white text-[11px] font-bold hover:bg-emerald-700"><span>+ </span></button>
</div>
<div className="px-3 py-2 border-b border-slate-100">
<input value={groupQuery} onChange={(e) => setGroupQuery(e.target.value)} placeholder="검색..." className="w-full h-8 px-2 text-xs border border-slate-200 rounded" />
</div>
<div className="flex-1 overflow-auto max-h-[calc(100vh-360px)] divide-y divide-slate-100">
{filteredGroups.length === 0 ? (
<div className="p-6 text-center text-xs text-slate-400"> .</div>
) : filteredGroups.map((g) => (
<button
key={g.OBJID}
onClick={() => setActiveGroup(g)}
onDoubleClick={() => onRename(g)}
className={cn("w-full text-left px-3 py-2 hover:bg-slate-50 transition-colors", activeGroup?.OBJID === g.OBJID && "bg-emerald-50/70 border-l-4 border-l-emerald-600")}
title="더블클릭: 수정/삭제"
>
<div className="text-sm font-bold text-slate-800">{g.AUTH_NAME}</div>
<div className="text-[10px] text-slate-400 font-mono">{g.AUTH_CODE || "-"}</div>
</button>
))}
</div>
</div>
{/* 우측: 활성 그룹의 직원 + 메뉴 매핑 */}
<div className="space-y-3">
{activeGroup ? (
<>
<AuthGroupMembers group={activeGroup} onChanged={fetchGroups} />
<AuthGroupMenus group={activeGroup} />
</>
) : (
<div className="bg-white border border-slate-200 rounded-xl p-12 text-center text-slate-400 text-sm">
[+ ] .
</div>
)}
</div>
</div>
</div>
);
}
// 직원 — 권한있는 / 권한없는 양쪽 패널
function AuthGroupMembers({ group, onChanged }: { group: AuthGroup; onChanged: () => void }) {
const [members, setMembers] = useState<AuthUserRow[]>([]);
const [available, setAvailable] = useState<AuthUserRow[]>([]);
const [memberQ, setMemberQ] = useState("");
const [availQ, setAvailQ] = useState("");
const [chkMember, setChkMember] = useState<Set<string>>(new Set());
const [chkAvail, setChkAvail] = useState<Set<string>>(new Set());
const reload = useCallback(async () => {
setChkMember(new Set()); setChkAvail(new Set());
const [m, u] = await Promise.all([
fetch("/api/admin/auth/members", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ masterObjid: group.OBJID }) }).then((r) => r.json()),
fetch("/api/admin/auth/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ masterObjid: group.OBJID }) }).then((r) => r.json()),
]);
setMembers((m.RESULTLIST || []) as AuthUserRow[]);
setAvailable((u.RESULTLIST || []) as AuthUserRow[]);
}, [group.OBJID]);
useEffect(() => { reload(); }, [reload]);
const filteredMembers = useMemo(() => members.filter((m) => !memberQ || (m.USER_NAME ?? "").includes(memberQ) || (m.USER_ID ?? "").includes(memberQ) || (m.DEPT_NAME ?? "").includes(memberQ)), [members, memberQ]);
const filteredAvail = useMemo(() => available.filter((m) => !availQ || (m.USER_NAME ?? "").includes(availQ) || (m.USER_ID ?? "").includes(availQ) || (m.DEPT_NAME ?? "").includes(availQ)), [available, availQ]);
const toggleAllMember = (on: boolean) => setChkMember(on ? new Set(filteredMembers.map((m) => String(m.OBJID))) : new Set());
const toggleAllAvail = (on: boolean) => setChkAvail(on ? new Set(filteredAvail.map((m) => String(m.USER_ID))) : new Set());
const addSelected = async () => {
const userIds = Array.from(chkAvail);
if (userIds.length === 0) { Swal.fire("알림", "추가할 직원을 선택하세요.", "warning"); return; }
const res = await fetch("/api/admin/auth/members/save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ masterObjid: group.OBJID, userIds }) });
const j = await res.json().catch(() => ({ success: false, message: "응답 파싱 실패" }));
if (!j.success) { Swal.fire({ icon: "error", title: "추가 실패", text: j.message }); return; }
await reload(); onChanged();
};
const removeSelected = async () => {
const memberObjids = Array.from(chkMember);
if (memberObjids.length === 0) { Swal.fire("알림", "제거할 직원을 선택하세요.", "warning"); return; }
const res = await fetch("/api/admin/auth/members/delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ masterObjid: group.OBJID, memberObjids }) });
const j = await res.json().catch(() => ({ success: false, message: "응답 파싱 실패" }));
if (!j.success) { Swal.fire({ icon: "error", title: "제거 실패", text: j.message }); return; }
await reload(); onChanged();
};
return (
<div className="grid grid-cols-1 lg:grid-cols-[1fr_auto_1fr] gap-3">
{/* 권한있는 직원 */}
<div className="bg-white border border-slate-200 rounded-xl flex flex-col">
<div className="px-3 py-2 border-b border-slate-100 text-sm font-bold text-slate-700 inline-flex items-center gap-1.5">
<Users size={14} className="text-emerald-700" /> ({members.length})
</div>
<div className="p-2 flex items-center gap-2 border-b border-slate-100">
<label className="text-xs inline-flex items-center gap-1.5">
<input type="checkbox" onChange={(e) => toggleAllMember(e.target.checked)} className="w-4 h-4 accent-emerald-600" />
</label>
<input value={memberQ} onChange={(e) => setMemberQ(e.target.value)} placeholder="검색" className="ml-auto h-7 w-40 px-2 text-xs border border-slate-200 rounded" />
</div>
<div className="flex-1 overflow-auto max-h-[40vh]">
<table className="w-full text-xs">
<thead className="bg-slate-50 sticky top-0">
<tr><th className="w-8 p-1.5"></th><th className="text-left p-1.5"></th><th className="text-left p-1.5"></th><th className="text-left p-1.5">ID</th></tr>
</thead>
<tbody>
{filteredMembers.map((m) => {
const id = String(m.OBJID);
return (
<tr key={id} className="border-t hover:bg-slate-50">
<td className="text-center p-1.5"><input type="checkbox" checked={chkMember.has(id)} onChange={(e) => { const s = new Set(chkMember); if (e.target.checked) s.add(id); else s.delete(id); setChkMember(s); }} className="w-4 h-4 accent-emerald-600" /></td>
<td className="p-1.5">{m.DEPT_NAME || "-"}</td>
<td className="p-1.5 font-semibold">{m.USER_NAME}</td>
<td className="p-1.5 text-slate-400 font-mono">{m.USER_ID}</td>
</tr>
);
})}
{filteredMembers.length === 0 && <tr><td colSpan={4} className="p-6 text-center text-slate-400"> </td></tr>}
</tbody>
</table>
</div>
</div>
{/* 추가/제거 버튼 */}
<div className="flex flex-row lg:flex-col items-center justify-center gap-2 px-2">
<button onClick={addSelected} className="h-9 w-24 rounded bg-emerald-600 text-white text-xs font-bold hover:bg-emerald-700 disabled:opacity-50"> </button>
<button onClick={removeSelected} className="h-9 w-24 rounded border border-slate-300 text-slate-700 text-xs font-bold hover:bg-slate-50"> </button>
</div>
{/* 권한없는 직원 */}
<div className="bg-white border border-slate-200 rounded-xl flex flex-col">
<div className="px-3 py-2 border-b border-slate-100 text-sm font-bold text-slate-700 inline-flex items-center gap-1.5">
<Users size={14} className="text-slate-400" /> ({available.length})
</div>
<div className="p-2 flex items-center gap-2 border-b border-slate-100">
<label className="text-xs inline-flex items-center gap-1.5">
<input type="checkbox" onChange={(e) => toggleAllAvail(e.target.checked)} className="w-4 h-4 accent-emerald-600" />
</label>
<input value={availQ} onChange={(e) => setAvailQ(e.target.value)} placeholder="검색" className="ml-auto h-7 w-40 px-2 text-xs border border-slate-200 rounded" />
</div>
<div className="flex-1 overflow-auto max-h-[40vh]">
<table className="w-full text-xs">
<thead className="bg-slate-50 sticky top-0">
<tr><th className="w-8 p-1.5"></th><th className="text-left p-1.5"></th><th className="text-left p-1.5"></th><th className="text-left p-1.5">ID</th></tr>
</thead>
<tbody>
{filteredAvail.map((m) => {
const id = String(m.USER_ID);
return (
<tr key={id} className="border-t hover:bg-slate-50">
<td className="text-center p-1.5"><input type="checkbox" checked={chkAvail.has(id)} onChange={(e) => { const s = new Set(chkAvail); if (e.target.checked) s.add(id); else s.delete(id); setChkAvail(s); }} className="w-4 h-4 accent-emerald-600" /></td>
<td className="p-1.5">{m.DEPT_NAME || "-"}</td>
<td className="p-1.5 font-semibold">{m.USER_NAME}</td>
<td className="p-1.5 text-slate-400 font-mono">{m.USER_ID}</td>
</tr>
);
})}
{filteredAvail.length === 0 && <tr><td colSpan={4} className="p-6 text-center text-slate-400"> </td></tr>}
</tbody>
</table>
</div>
</div>
</div>
);
}
// 메뉴 트리 — 체크 즉시 토글
function AuthGroupMenus({ group }: { group: AuthGroup }) {
const [menus, setMenus] = useState<MenuRow[]>([]);
const [assigned, setAssigned] = useState<Set<string>>(new Set());
const [pending, setPending] = useState<Set<string>>(new Set());
const reload = useCallback(async () => {
const res = await fetch("/api/admin/auth/menus", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ masterObjid: group.OBJID }) });
const j = await res.json();
setMenus((j.MENULIST || []) as MenuRow[]);
setAssigned(new Set(((j.ASSIGNED || []) as string[]).map(String)));
}, [group.OBJID]);
useEffect(() => { reload(); }, [reload]);
// 트리 빌드
const tree = useMemo(() => {
const byParent = new Map<string, MenuRow[]>();
for (const m of menus) {
const p = String(m.PARENT_OBJ_ID ?? "");
if (!byParent.has(p)) byParent.set(p, []);
byParent.get(p)!.push(m);
}
const roots = menus.filter((m) => !m.PARENT_OBJ_ID || m.PARENT_OBJ_ID === "0" || m.PARENT_OBJ_ID === "" || !menus.some((x) => String(x.OBJID) === String(m.PARENT_OBJ_ID)));
return { byParent, roots };
}, [menus]);
const toggle = async (menu: MenuRow) => {
const id = String(menu.OBJID);
const on = !assigned.has(id);
setPending((p) => new Set(p).add(id));
setAssigned((prev) => { const n = new Set(prev); if (on) n.add(id); else n.delete(id); return n; });
try {
const res = await fetch("/api/admin/auth/menus/toggle", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ masterObjid: group.OBJID, menuObjid: id, on }) });
const j = await res.json();
if (!j.success) {
Swal.fire({ icon: "error", title: "메뉴 토글 실패", text: j.message ?? "" });
// 롤백
setAssigned((prev) => { const n = new Set(prev); if (on) n.delete(id); else n.add(id); return n; });
}
} finally {
setPending((p) => { const n = new Set(p); n.delete(id); return n; });
}
};
const renderNode = (m: MenuRow, depth: number): React.ReactNode => {
const id = String(m.OBJID);
const children = tree.byParent.get(id) || [];
return (
<div key={id}>
<label className={cn("flex items-center gap-2 py-1 px-2 hover:bg-slate-50 cursor-pointer text-sm", depth > 0 && "pl-[" + (depth * 20 + 8) + "px]")} style={{ paddingLeft: depth * 20 + 8 }}>
<input
type="checkbox"
checked={assigned.has(id)}
disabled={pending.has(id)}
onChange={() => toggle(m)}
className="w-4 h-4 accent-emerald-600 cursor-pointer"
/>
<span className={cn("font-medium", depth === 0 ? "text-slate-900 font-bold" : "text-slate-700")}>{m.MENU_NAME_KOR}</span>
{m.MENU_URL && <span className="text-[10px] text-slate-400 font-mono">{m.MENU_URL}</span>}
{m.STATUS !== "active" && <span className="text-[10px] text-rose-500"></span>}
</label>
{children.map((c) => renderNode(c, depth + 1))}
</div>
);
};
return (
<div className="bg-white border border-slate-200 rounded-xl">
<div className="px-3 py-2 border-b border-slate-100">
<div className="text-sm font-bold text-slate-700 inline-flex items-center gap-1.5"><Menu size={14} className="text-emerald-700" /> </div>
<p className="text-[11px] text-slate-400 mt-0.5"> · </p>
</div>
<div className="max-h-[40vh] overflow-auto py-1">
{tree.roots.length === 0 ? (
<div className="p-6 text-center text-slate-400 text-sm"> .</div>
) : tree.roots.map((m) => renderNode(m, 0))}
</div>
</div>
);
}
// 권한 관리 — 레거시 모달형 (단계적 deprecation)
function _AuthManagementLegacy() {
const [data, setData] = useState<Record<string, unknown>[]>([]); const [data, setData] = useState<Record<string, unknown>[]>([]);
const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]); const [selectedRows, setSelectedRows] = useState<Record<string, unknown>[]>([]);
// 등록/수정 폼 // 등록/수정 폼
+2 -2
View File
@@ -27,9 +27,9 @@ export async function POST(request: NextRequest) {
const objid = createObjectId(); const objid = createObjectId();
const rowCount = await execute( const rowCount = await execute(
`INSERT INTO AUTHORITY_SUB_USER (OBJID, MASTER_OBJID, USER_ID, WRITER, REGDATE) `INSERT INTO AUTHORITY_SUB_USER (OBJID, MASTER_OBJID, USER_ID, WRITER, REGDATE)
SELECT $1::numeric, $2::numeric, $3, $4, now() SELECT $1::numeric, $2::numeric, $3::text, $4::text, now()
WHERE NOT EXISTS ( WHERE NOT EXISTS (
SELECT 1 FROM AUTHORITY_SUB_USER WHERE USER_ID = $3 AND MASTER_OBJID = $2::numeric SELECT 1 FROM AUTHORITY_SUB_USER WHERE USER_ID = $3::text AND MASTER_OBJID = $2::numeric
)`, )`,
[objid, masterObjid, userId, user.userId] [objid, masterObjid, userId, user.userId]
); );
+36
View File
@@ -0,0 +1,36 @@
import { NextRequest, NextResponse } from "next/server";
import { queryRows } from "@/lib/db";
import { getSession } from "@/lib/session";
// 특정 권한그룹이 가진 메뉴 OBJID 목록 + 전체 메뉴 트리
export async function POST(request: NextRequest) {
const user = await getSession();
if (!user) return NextResponse.json({ success: false }, { status: 401 });
const { masterObjid } = await request.json().catch(() => ({}));
// 전체 메뉴 트리
const menus = await queryRows(`
SELECT objid::text AS "OBJID",
parent_obj_id::text AS "PARENT_OBJ_ID",
menu_name_kor AS "MENU_NAME_KOR",
menu_url AS "MENU_URL",
seq::int AS "SEQ",
menu_type::int AS "MENU_TYPE",
COALESCE(status,'active') AS "STATUS"
FROM menu_info
ORDER BY menu_type, seq
`);
// 그룹이 가진 메뉴 OBJID 목록
let assigned: string[] = [];
if (masterObjid) {
const rows = await queryRows<{ MENU_OBJID: string }>(
`SELECT menu_objid::text AS "MENU_OBJID" FROM authority_sub_menu WHERE master_objid = $1::numeric`,
[masterObjid]
);
assigned = rows.map((r) => r.MENU_OBJID);
}
return NextResponse.json({ success: true, MENULIST: menus, ASSIGNED: assigned });
}
@@ -0,0 +1,40 @@
import { NextRequest, NextResponse } from "next/server";
import { execute } from "@/lib/db";
import { getSession } from "@/lib/session";
import { createObjectId } from "@/lib/utils";
// 권한그룹의 메뉴 ON/OFF 토글
export async function POST(request: NextRequest) {
const user = await getSession();
if (!user) return NextResponse.json({ success: false }, { status: 401 });
const body = await request.json().catch(() => ({}));
const { masterObjid, menuObjid, on } = body as { masterObjid: string; menuObjid: string; on: boolean };
if (!masterObjid || !menuObjid) {
return NextResponse.json({ success: false, message: "필수 파라미터 누락" }, { status: 400 });
}
try {
if (on) {
const objid = createObjectId();
await execute(
`INSERT INTO authority_sub_menu (objid, master_objid, menu_objid, writer, regdate)
SELECT $1::numeric, $2::numeric, $3::numeric, $4::text, now()
WHERE NOT EXISTS (
SELECT 1 FROM authority_sub_menu WHERE master_objid=$2::numeric AND menu_objid=$3::numeric
)`,
[objid, masterObjid, menuObjid, user.userId]
);
} else {
await execute(
`DELETE FROM authority_sub_menu WHERE master_objid=$1::numeric AND menu_objid=$2::numeric`,
[masterObjid, menuObjid]
);
}
return NextResponse.json({ success: true });
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error("[auth/menus/toggle]", msg);
return NextResponse.json({ success: false, message: msg }, { status: 500 });
}
}