feat(menu): 사용자 → 대메뉴 → 소메뉴 2단 트리로 재구성, 모모 자체 회원/권한/메뉴 제거
Deploy momo-erp / deploy (push) Successful in 46s
Deploy momo-erp / deploy (push) Successful in 46s
DB: - [사용자] 그룹 아래에 5개 대메뉴 신규 (거래처 주문/마스터 관리/매입·입고/출고·정산/통계) - 각 대메뉴 아래에 모모 페이지 소메뉴로 배치 (URL 직접 연결) - 기존 [DASHBOARD] 대메뉴 활용 — 자식 [대시보드 → /m/dashboard] 추가, 기존 dashboard.do 비활성화 코드: - /m/admin/users, /m/admin/roles, /m/admin/menus 페이지 삭제 - /api/m/users, /api/m/roles, /api/m/menus 삭제 - 모모 사이드바 [시스템] 그룹 제거 (기존 admin-panel 사용자/권한/메뉴 관리 활용) → plm_admin 로그인 후 [사용자] 그룹 펼치면 깔끔한 2단 트리 표시, 각 소메뉴 클릭 시 /m/* 페이지로 정상 이동
This commit is contained in:
@@ -1,28 +1,53 @@
|
||||
-- 모모유통 페이지를 FITO menu_info 의 [사용자] 그룹 직속 자식으로 평탄 등록
|
||||
-- [사용자] objid = -395553955
|
||||
-- 모모유통 페이지를 FITO menu_info 의 [사용자] 그룹 아래에 대메뉴/소메뉴 2단 구조로 등록
|
||||
-- 기존 [DASHBOARD] 대메뉴 활용 + 거래처/마스터/매입/출고-정산/통계 5개 신규 대메뉴
|
||||
BEGIN;
|
||||
|
||||
DELETE FROM menu_info WHERE objid BETWEEN 9000000 AND 9000099;
|
||||
-- 기존 모모 메뉴 정리
|
||||
DELETE FROM menu_info WHERE objid BETWEEN 9000000 AND 9000599;
|
||||
|
||||
-- ===== 신규 대메뉴 (parent = -395553955 [사용자]) =====
|
||||
INSERT INTO menu_info (objid, menu_type, parent_obj_id, menu_name_kor, menu_name_eng, seq, menu_url, status, system_name, regdate) VALUES
|
||||
(9000001, '1', -395553955, '대시보드', 'Dashboard', 510, '/m/dashboard', 'active', 'PMS', NOW()),
|
||||
(9000002, '1', -395553955, '품목 검색(거래처)', 'Items', 520, '/m/items', 'active', 'PMS', NOW()),
|
||||
(9000003, '1', -395553955, '출고 요청(거래처)', 'New Order', 521, '/m/orders/new', 'active', 'PMS', NOW()),
|
||||
(9000004, '1', -395553955, '내 출고 이력', 'My Orders', 522, '/m/orders', 'active', 'PMS', NOW()),
|
||||
(9000005, '1', -395553955, '품목 관리', 'Item Master', 530, '/m/admin/items', 'active', 'PMS', NOW()),
|
||||
(9000006, '1', -395553955, '매입처 관리', 'Vendors', 531, '/m/admin/vendors', 'active', 'PMS', NOW()),
|
||||
(9000007, '1', -395553955, '창고 관리', 'Warehouses', 532, '/m/admin/warehouses', 'active', 'PMS', NOW()),
|
||||
(9000008, '1', -395553955, '매입 발주', 'Procurements', 540, '/m/admin/procurements', 'active', 'PMS', NOW()),
|
||||
(9000009, '1', -395553955, '입고 처리', 'Inbound', 541, '/m/admin/inbounds', 'active', 'PMS', NOW()),
|
||||
(9000010, '1', -395553955, '재고 관리', 'Inventory', 542, '/m/admin/inventory', 'active', 'PMS', NOW()),
|
||||
(9000011, '1', -395553955, '출고 관리', 'Outbound', 550, '/m/admin/orders', 'active', 'PMS', NOW()),
|
||||
(9000012, '1', -395553955, '입금 관리', 'Payments', 551, '/m/admin/payments', 'active', 'PMS', NOW()),
|
||||
(9000013, '1', -395553955, '계산서 발행', 'Invoices', 552, '/m/admin/invoices', 'active', 'PMS', NOW()),
|
||||
(9000014, '1', -395553955, '월간 매출', 'Stat Monthly', 560, '/m/admin/statistics', 'active', 'PMS', NOW()),
|
||||
(9000015, '1', -395553955, '일자별 매출', 'Stat Daily', 561, '/m/admin/statistics/daily', 'active', 'PMS', NOW()),
|
||||
(9000016, '1', -395553955, '원가/마진', 'Margin', 562, '/m/admin/statistics/margin', 'active', 'PMS', NOW()),
|
||||
(9000017, '1', -395553955, '회원 관리', 'Users', 590, '/m/admin/users', 'active', 'PMS', NOW()),
|
||||
(9000018, '1', -395553955, '권한 관리', 'Roles', 591, '/m/admin/roles', 'active', 'PMS', NOW()),
|
||||
(9000019, '1', -395553955, '메뉴 관리(모모)', 'Momo Menus', 592, '/m/admin/menus', 'active', 'PMS', NOW());
|
||||
(9000100, '1', -395553955, '거래처 주문', 'Customer Orders', 600, '', 'active', 'PMS', NOW()),
|
||||
(9000200, '1', -395553955, '마스터 관리', 'Master', 650, '', 'active', 'PMS', NOW()),
|
||||
(9000300, '1', -395553955, '매입/입고', 'Purchase', 700, '', 'active', 'PMS', NOW()),
|
||||
(9000400, '1', -395553955, '출고/정산', 'Outbound', 750, '', 'active', 'PMS', NOW()),
|
||||
(9000500, '1', -395553955, '통계', 'Statistics', 800, '', 'active', 'PMS', NOW());
|
||||
|
||||
-- ===== 기존 [DASHBOARD] 대메뉴(1837127121) 아래 자식 =====
|
||||
INSERT INTO menu_info (objid, menu_type, parent_obj_id, menu_name_kor, menu_name_eng, seq, menu_url, status, system_name, regdate) VALUES
|
||||
(9000001, '1', 1837127121, '대시보드', 'Dashboard', 1, '/m/dashboard', 'active', 'PMS', NOW());
|
||||
|
||||
-- 기존 [DASHBOARD] 대메뉴의 다른 자식(예: dashboard.do)은 비활성화
|
||||
UPDATE menu_info SET status='inactive' WHERE parent_obj_id = 1837127121 AND objid != 9000001;
|
||||
|
||||
-- ===== 거래처 주문 (9000100) =====
|
||||
INSERT INTO menu_info (objid, menu_type, parent_obj_id, menu_name_kor, menu_name_eng, seq, menu_url, status, system_name, regdate) VALUES
|
||||
(9000101, '1', 9000100, '품목 검색', 'Items', 10, '/m/items', 'active', 'PMS', NOW()),
|
||||
(9000102, '1', 9000100, '출고 요청', 'New Order', 11, '/m/orders/new', 'active', 'PMS', NOW()),
|
||||
(9000103, '1', 9000100, '내 출고 이력', 'My Orders', 12, '/m/orders', 'active', 'PMS', NOW());
|
||||
|
||||
-- ===== 마스터 관리 (9000200) =====
|
||||
INSERT INTO menu_info (objid, menu_type, parent_obj_id, menu_name_kor, menu_name_eng, seq, menu_url, status, system_name, regdate) VALUES
|
||||
(9000201, '1', 9000200, '품목 관리', 'Item Master', 10, '/m/admin/items', 'active', 'PMS', NOW()),
|
||||
(9000202, '1', 9000200, '매입처 관리', 'Vendors', 11, '/m/admin/vendors', 'active', 'PMS', NOW()),
|
||||
(9000203, '1', 9000200, '창고 관리', 'Warehouses', 12, '/m/admin/warehouses', 'active', 'PMS', NOW());
|
||||
|
||||
-- ===== 매입/입고 (9000300) =====
|
||||
INSERT INTO menu_info (objid, menu_type, parent_obj_id, menu_name_kor, menu_name_eng, seq, menu_url, status, system_name, regdate) VALUES
|
||||
(9000301, '1', 9000300, '매입 발주', 'Procurements', 10, '/m/admin/procurements', 'active', 'PMS', NOW()),
|
||||
(9000302, '1', 9000300, '입고 처리', 'Inbound', 11, '/m/admin/inbounds', 'active', 'PMS', NOW()),
|
||||
(9000303, '1', 9000300, '재고 관리', 'Inventory', 12, '/m/admin/inventory', 'active', 'PMS', NOW());
|
||||
|
||||
-- ===== 출고/정산 (9000400) =====
|
||||
INSERT INTO menu_info (objid, menu_type, parent_obj_id, menu_name_kor, menu_name_eng, seq, menu_url, status, system_name, regdate) VALUES
|
||||
(9000401, '1', 9000400, '출고 관리', 'Outbound', 10, '/m/admin/orders', 'active', 'PMS', NOW()),
|
||||
(9000402, '1', 9000400, '입금 관리', 'Payments', 11, '/m/admin/payments', 'active', 'PMS', NOW()),
|
||||
(9000403, '1', 9000400, '계산서 발행', 'Invoices', 12, '/m/admin/invoices', 'active', 'PMS', NOW());
|
||||
|
||||
-- ===== 통계 (9000500) =====
|
||||
INSERT INTO menu_info (objid, menu_type, parent_obj_id, menu_name_kor, menu_name_eng, seq, menu_url, status, system_name, regdate) VALUES
|
||||
(9000501, '1', 9000500, '월간 매출', 'Stat Monthly', 10, '/m/admin/statistics', 'active', 'PMS', NOW()),
|
||||
(9000502, '1', 9000500, '일자별 매출', 'Stat Daily', 11, '/m/admin/statistics/daily', 'active', 'PMS', NOW()),
|
||||
(9000503, '1', 9000500, '원가/마진', 'Margin', 12, '/m/admin/statistics/margin', 'active', 'PMS', NOW());
|
||||
|
||||
COMMIT;
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { requireMomoAdmin } from "@/lib/momo-guard";
|
||||
|
||||
export async function POST() {
|
||||
const g = await requireMomoAdmin();
|
||||
if (g instanceof NextResponse) return g;
|
||||
const rows = await queryRows(
|
||||
`SELECT objid AS "OBJID", menu_code AS "MENU_CODE", menu_name AS "MENU_NAME",
|
||||
menu_url AS "MENU_URL", group_name AS "GROUP_NAME",
|
||||
sort_order AS "SORT_ORDER", is_system AS "IS_SYSTEM"
|
||||
FROM momo_menus WHERE COALESCE(is_del,'N') != 'Y'
|
||||
ORDER BY group_name, sort_order ASC`
|
||||
);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
import { requireMomoAdmin } from "@/lib/momo-guard";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const g = await requireMomoAdmin();
|
||||
if (g instanceof NextResponse) return g;
|
||||
const { objid, actionType, menuCode, menuName, menuUrl, groupName, sortOrder } = await req.json();
|
||||
if (!menuCode || !menuName) return NextResponse.json({ success: false, message: "코드/이름 필수" }, { status: 400 });
|
||||
|
||||
if (actionType === "regist") {
|
||||
await execute(
|
||||
`INSERT INTO momo_menus (objid, menu_code, menu_name, menu_url, group_name, sort_order, is_system, regdate)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,'N',NOW())`,
|
||||
[createObjectId(), menuCode.toUpperCase(), menuName, menuUrl ?? null, groupName ?? null, Number(sortOrder) || 0]
|
||||
);
|
||||
} else {
|
||||
await execute(
|
||||
`UPDATE momo_menus SET menu_name=$2, menu_url=$3, group_name=$4, sort_order=$5
|
||||
WHERE objid=$1 AND COALESCE(is_system,'N')!='Y'`,
|
||||
[objid, menuName, menuUrl ?? null, groupName ?? null, Number(sortOrder) || 0]
|
||||
);
|
||||
}
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { requireMomoAdmin } from "@/lib/momo-guard";
|
||||
|
||||
export async function POST() {
|
||||
const g = await requireMomoAdmin();
|
||||
if (g instanceof NextResponse) return g;
|
||||
const rows = await queryRows(
|
||||
`SELECT R.objid AS "OBJID", R.role_code AS "ROLE_CODE", R.role_name AS "ROLE_NAME",
|
||||
R.description AS "DESCRIPTION", R.is_system AS "IS_SYSTEM",
|
||||
(SELECT COUNT(*) FROM momo_users WHERE role = R.role_code AND COALESCE(is_del,'N')!='Y') AS "USER_CNT",
|
||||
(SELECT COUNT(*) FROM momo_role_menus WHERE role_code = R.role_code) AS "MENU_CNT"
|
||||
FROM momo_roles R WHERE COALESCE(R.is_del,'N') != 'Y'
|
||||
ORDER BY R.role_code`
|
||||
);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { createObjectId } from "@/lib/utils";
|
||||
import { requireMomoAdmin } from "@/lib/momo-guard";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const g = await requireMomoAdmin();
|
||||
if (g instanceof NextResponse) return g;
|
||||
const { objid, actionType, roleCode, roleName, description } = await req.json();
|
||||
if (!roleCode || !roleName) return NextResponse.json({ success: false, message: "코드와 이름 필수" }, { status: 400 });
|
||||
|
||||
if (actionType === "regist") {
|
||||
await execute(
|
||||
`INSERT INTO momo_roles (objid, role_code, role_name, description, is_system, regdate)
|
||||
VALUES ($1,$2,$3,$4,'N',NOW())`,
|
||||
[createObjectId(), roleCode.toUpperCase(), roleName, description ?? null]
|
||||
);
|
||||
} else {
|
||||
await execute(
|
||||
`UPDATE momo_roles SET role_name=$2, description=$3 WHERE objid=$1 AND COALESCE(is_system,'N')!='Y'`,
|
||||
[objid, roleName, description ?? null]
|
||||
);
|
||||
}
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { queryRows } from "@/lib/db";
|
||||
import { requireMomoAdmin } from "@/lib/momo-guard";
|
||||
|
||||
export async function POST() {
|
||||
const g = await requireMomoAdmin();
|
||||
if (g instanceof NextResponse) return g;
|
||||
|
||||
const rows = await queryRows(
|
||||
`SELECT objid AS "OBJID", email AS "EMAIL", company_name AS "COMPANY_NAME",
|
||||
ceo_name AS "CEO_NAME", phone AS "PHONE", biz_no AS "BIZ_NO",
|
||||
role AS "ROLE", status AS "STATUS",
|
||||
TO_CHAR(regdate, 'YYYY-MM-DD') AS "REGDATE"
|
||||
FROM momo_users WHERE COALESCE(is_del,'N') != 'Y'
|
||||
ORDER BY regdate DESC`
|
||||
);
|
||||
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execute } from "@/lib/db";
|
||||
import { requireMomoAdmin } from "@/lib/momo-guard";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const g = await requireMomoAdmin();
|
||||
if (g instanceof NextResponse) return g;
|
||||
|
||||
const { objid, role, status } = await req.json();
|
||||
if (!objid) return NextResponse.json({ success: false, message: "objid 누락" }, { status: 400 });
|
||||
|
||||
const sets: string[] = [];
|
||||
const params: unknown[] = [objid];
|
||||
let i = 2;
|
||||
if (role) { sets.push(`role = $${i++}`); params.push(role); }
|
||||
if (status) { sets.push(`status = $${i++}`); params.push(status); }
|
||||
if (sets.length === 0) return NextResponse.json({ success: false, message: "변경 사항 없음" });
|
||||
|
||||
sets.push(`update_date = NOW()`);
|
||||
await execute(`UPDATE momo_users SET ${sets.join(", ")} WHERE objid = $1`, params);
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, FormEvent } from "react";
|
||||
import { Plus, Pencil, Layers } from "lucide-react";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
interface Menu { OBJID: string; MENU_CODE: string; MENU_NAME: string; MENU_URL: string; GROUP_NAME: string; SORT_ORDER: number; IS_SYSTEM: string }
|
||||
|
||||
export default function MenusPage() {
|
||||
const [list, setList] = useState<Menu[]>([]);
|
||||
const [editing, setEditing] = useState<Partial<Menu> | null>(null);
|
||||
|
||||
const load = async () => {
|
||||
const res = await fetch("/api/m/menus/list", { method: "POST" });
|
||||
setList((await res.json()).RESULTLIST ?? []);
|
||||
};
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const save = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!editing) return;
|
||||
const res = await fetch("/api/m/menus/save", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
objid: editing.OBJID, actionType: editing.OBJID ? "update" : "regist",
|
||||
menuCode: editing.MENU_CODE, menuName: editing.MENU_NAME,
|
||||
menuUrl: editing.MENU_URL, groupName: editing.GROUP_NAME, sortOrder: editing.SORT_ORDER,
|
||||
}),
|
||||
});
|
||||
if ((await res.json()).success) {
|
||||
Swal.fire({ icon: "success", title: "저장됨", timer: 1200, showConfirmButton: false });
|
||||
setEditing(null); load();
|
||||
}
|
||||
};
|
||||
|
||||
// 그룹별로
|
||||
const grouped: Record<string, Menu[]> = {};
|
||||
for (const m of list) (grouped[m.GROUP_NAME || "_"] ||= []).push(m);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold flex items-center gap-2"><Layers className="text-emerald-700" /> 메뉴 관리</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">현재 사이드바와 동기화된 마스터. 추후 ERP 확장 시 권한별 메뉴 노출 정책에 사용.</p>
|
||||
</div>
|
||||
<button onClick={() => setEditing({ SORT_ORDER: 100 })} className="px-4 h-10 inline-flex items-center gap-2 rounded-lg bg-emerald-700 text-white text-sm font-bold">
|
||||
<Plus size={16} /> 메뉴 추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{Object.entries(grouped).map(([group, items]) => (
|
||||
<div key={group} className="bg-white border rounded-xl overflow-hidden">
|
||||
<div className="bg-slate-50 px-4 py-2.5 font-bold text-sm border-b">
|
||||
{group === "_" ? "(그룹 없음)" : group}
|
||||
</div>
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-50/50 text-slate-600 text-xs">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-2 w-12">순서</th>
|
||||
<th className="text-left px-4 py-2">코드</th>
|
||||
<th className="text-left px-4 py-2">이름</th>
|
||||
<th className="text-left px-4 py-2">URL</th>
|
||||
<th className="text-center px-4 py-2 w-20">유형</th>
|
||||
<th className="text-right px-4 py-2 w-16"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((m) => (
|
||||
<tr key={m.OBJID} className="border-t border-slate-100">
|
||||
<td className="px-4 py-2 text-slate-500">{m.SORT_ORDER}</td>
|
||||
<td className="px-4 py-2 font-mono text-xs">{m.MENU_CODE}</td>
|
||||
<td className="px-4 py-2 font-semibold">{m.MENU_NAME}</td>
|
||||
<td className="px-4 py-2 text-slate-500 text-xs font-mono">{m.MENU_URL}</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
{m.IS_SYSTEM === "Y"
|
||||
? <span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-100 text-amber-700 font-bold">시스템</span>
|
||||
: <span className="text-[10px] px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-700 font-bold">사용자</span>}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right">
|
||||
{m.IS_SYSTEM !== "Y" && (
|
||||
<button onClick={() => setEditing(m)} className="text-slate-500 hover:text-emerald-700 p-1"><Pencil size={14} /></button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{editing && (
|
||||
<div className="fixed inset-0 bg-slate-900/60 z-50 flex items-center justify-center p-4" onClick={() => setEditing(null)}>
|
||||
<form onSubmit={save} onClick={(e) => e.stopPropagation()} className="bg-white rounded-xl max-w-md w-full p-6">
|
||||
<h3 className="font-bold mb-4">{editing.OBJID ? "메뉴 수정" : "메뉴 추가"}</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<input required placeholder="코드 *" value={editing.MENU_CODE ?? ""} disabled={!!editing.OBJID}
|
||||
onChange={(e) => setEditing({ ...editing, MENU_CODE: e.target.value.toUpperCase() })}
|
||||
className="col-span-2 h-10 px-3 rounded-lg border border-slate-200 disabled:bg-slate-100 font-mono" />
|
||||
<input required placeholder="이름 *" value={editing.MENU_NAME ?? ""}
|
||||
onChange={(e) => setEditing({ ...editing, MENU_NAME: e.target.value })}
|
||||
className="col-span-2 h-10 px-3 rounded-lg border border-slate-200" />
|
||||
<input placeholder="그룹" value={editing.GROUP_NAME ?? ""}
|
||||
onChange={(e) => setEditing({ ...editing, GROUP_NAME: e.target.value })}
|
||||
className="h-10 px-3 rounded-lg border border-slate-200" />
|
||||
<input type="number" placeholder="순서" value={editing.SORT_ORDER ?? 100}
|
||||
onChange={(e) => setEditing({ ...editing, SORT_ORDER: Number(e.target.value) })}
|
||||
className="h-10 px-3 rounded-lg border border-slate-200" />
|
||||
<input placeholder="URL" value={editing.MENU_URL ?? ""}
|
||||
onChange={(e) => setEditing({ ...editing, MENU_URL: e.target.value })}
|
||||
className="col-span-2 h-10 px-3 rounded-lg border border-slate-200 font-mono text-sm" />
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end mt-5">
|
||||
<button type="button" onClick={() => setEditing(null)} className="px-4 h-10 rounded-lg border border-slate-200 text-sm font-semibold">취소</button>
|
||||
<button type="submit" className="px-5 h-10 rounded-lg bg-emerald-700 text-white text-sm font-bold">저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, FormEvent } from "react";
|
||||
import { Plus, Pencil, ShieldCheck } from "lucide-react";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
interface Role { OBJID: string; ROLE_CODE: string; ROLE_NAME: string; DESCRIPTION: string; IS_SYSTEM: string; USER_CNT: number; MENU_CNT: number }
|
||||
|
||||
export default function RolesPage() {
|
||||
const [list, setList] = useState<Role[]>([]);
|
||||
const [editing, setEditing] = useState<Partial<Role> | null>(null);
|
||||
|
||||
const load = async () => {
|
||||
const res = await fetch("/api/m/roles/list", { method: "POST" });
|
||||
setList((await res.json()).RESULTLIST ?? []);
|
||||
};
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const save = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!editing) return;
|
||||
const res = await fetch("/api/m/roles/save", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
objid: editing.OBJID, actionType: editing.OBJID ? "update" : "regist",
|
||||
roleCode: editing.ROLE_CODE, roleName: editing.ROLE_NAME, description: editing.DESCRIPTION,
|
||||
}),
|
||||
});
|
||||
if ((await res.json()).success) {
|
||||
Swal.fire({ icon: "success", title: "저장됨", timer: 1200, showConfirmButton: false });
|
||||
setEditing(null); load();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold flex items-center gap-2"><ShieldCheck className="text-emerald-700" /> 권한 관리</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">현재는 거래처(USER) / 관리자(ADMIN) 2종. 추후 ERP 확장 시 매입담당/정산담당 등 세분화 가능.</p>
|
||||
</div>
|
||||
<button onClick={() => setEditing({})} className="px-4 h-10 inline-flex items-center gap-2 rounded-lg bg-emerald-700 text-white text-sm font-bold">
|
||||
<Plus size={16} /> 권한 추가
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-white border rounded-xl overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-50 text-slate-600">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3">코드</th>
|
||||
<th className="text-left px-4 py-3">이름</th>
|
||||
<th className="text-left px-4 py-3">설명</th>
|
||||
<th className="text-right px-4 py-3">사용자</th>
|
||||
<th className="text-right px-4 py-3">접근 메뉴</th>
|
||||
<th className="text-center px-4 py-3">유형</th>
|
||||
<th className="text-right px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{list.map((r) => (
|
||||
<tr key={r.OBJID} className="border-t border-slate-100">
|
||||
<td className="px-4 py-3 font-mono text-xs">{r.ROLE_CODE}</td>
|
||||
<td className="px-4 py-3 font-semibold">{r.ROLE_NAME}</td>
|
||||
<td className="px-4 py-3 text-slate-600 text-xs">{r.DESCRIPTION || "-"}</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums">{r.USER_CNT}명</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums">{r.MENU_CNT}개</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
{r.IS_SYSTEM === "Y"
|
||||
? <span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-100 text-amber-700 font-bold">시스템</span>
|
||||
: <span className="text-[10px] px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-700 font-bold">사용자정의</span>}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
{r.IS_SYSTEM !== "Y" && (
|
||||
<button onClick={() => setEditing(r)} className="text-slate-500 hover:text-emerald-700 p-1"><Pencil size={14} /></button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{editing && (
|
||||
<div className="fixed inset-0 bg-slate-900/60 z-50 flex items-center justify-center p-4" onClick={() => setEditing(null)}>
|
||||
<form onSubmit={save} onClick={(e) => e.stopPropagation()} className="bg-white rounded-xl max-w-md w-full p-6">
|
||||
<h3 className="font-bold mb-4">{editing.OBJID ? "권한 수정" : "권한 추가"}</h3>
|
||||
<div className="space-y-3">
|
||||
<input required placeholder="코드 (예: SETTLE)" value={editing.ROLE_CODE ?? ""} disabled={!!editing.OBJID}
|
||||
onChange={(e) => setEditing({ ...editing, ROLE_CODE: e.target.value.toUpperCase() })}
|
||||
className="w-full h-10 px-3 rounded-lg border border-slate-200 disabled:bg-slate-100 font-mono" />
|
||||
<input required placeholder="이름 (예: 정산 담당자)" value={editing.ROLE_NAME ?? ""}
|
||||
onChange={(e) => setEditing({ ...editing, ROLE_NAME: e.target.value })}
|
||||
className="w-full h-10 px-3 rounded-lg border border-slate-200" />
|
||||
<textarea placeholder="설명 (선택)" value={editing.DESCRIPTION ?? ""}
|
||||
onChange={(e) => setEditing({ ...editing, DESCRIPTION: e.target.value })}
|
||||
rows={3} className="w-full px-3 py-2 rounded-lg border border-slate-200" />
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end mt-5">
|
||||
<button type="button" onClick={() => setEditing(null)} className="px-4 h-10 rounded-lg border border-slate-200 text-sm font-semibold">취소</button>
|
||||
<button type="submit" className="px-5 h-10 rounded-lg bg-emerald-700 text-white text-sm font-bold">저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
interface UserRow {
|
||||
OBJID: string; EMAIL: string; COMPANY_NAME: string;
|
||||
CEO_NAME: string; PHONE: string; BIZ_NO: string;
|
||||
ROLE: string; STATUS: string; REGDATE: string;
|
||||
}
|
||||
|
||||
export default function AdminUsersPage() {
|
||||
const [rows, setRows] = useState<UserRow[]>([]);
|
||||
|
||||
const load = async () => {
|
||||
const res = await fetch("/api/m/users/list", { method: "POST" });
|
||||
setRows((await res.json()).RESULTLIST ?? []);
|
||||
};
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const toggleAdmin = async (u: UserRow) => {
|
||||
const ok = await Swal.fire({
|
||||
icon: "question",
|
||||
title: u.ROLE === "ADMIN" ? "관리자 권한을 해제할까요?" : "관리자로 승격할까요?",
|
||||
text: u.COMPANY_NAME,
|
||||
showCancelButton: true,
|
||||
});
|
||||
if (!ok.isConfirmed) return;
|
||||
const res = await fetch("/api/m/users/save", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ objid: u.OBJID, role: u.ROLE === "ADMIN" ? "USER" : "ADMIN" }),
|
||||
});
|
||||
if ((await res.json()).success) load();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-2xl font-bold">회원 관리</h1>
|
||||
<div className="bg-white border border-slate-200 rounded-xl overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-50 text-slate-600">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3">이메일</th>
|
||||
<th className="text-left px-4 py-3">업체명</th>
|
||||
<th className="text-left px-4 py-3">대표</th>
|
||||
<th className="text-left px-4 py-3">연락처</th>
|
||||
<th className="text-left px-4 py-3">권한</th>
|
||||
<th className="text-left px-4 py-3">가입일</th>
|
||||
<th className="text-right px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((u) => (
|
||||
<tr key={u.OBJID} className="border-t border-slate-100">
|
||||
<td className="px-4 py-3 font-mono text-xs">{u.EMAIL}</td>
|
||||
<td className="px-4 py-3 font-semibold">{u.COMPANY_NAME}</td>
|
||||
<td className="px-4 py-3">{u.CEO_NAME || "-"}</td>
|
||||
<td className="px-4 py-3 text-slate-600">{u.PHONE || "-"}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-semibold ${u.ROLE === "ADMIN" ? "bg-emerald-100 text-emerald-700" : "bg-slate-100 text-slate-700"}`}>
|
||||
{u.ROLE === "ADMIN" ? "관리자" : "거래처"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-500 text-xs">{u.REGDATE}</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<button onClick={() => toggleAdmin(u)} className="text-xs px-3 h-8 rounded-md border border-slate-200 hover:bg-slate-50 font-semibold">
|
||||
{u.ROLE === "ADMIN" ? "권한해제" : "관리자승격"}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
LayoutDashboard, Package, ShoppingCart, Warehouse, TrendingUp, Users,
|
||||
Building2, ClipboardList, Truck, Receipt, PackagePlus, Wallet, ShieldCheck, Layers,
|
||||
LayoutDashboard, Package, ShoppingCart, Warehouse, TrendingUp,
|
||||
Building2, ClipboardList, Truck, Receipt, PackagePlus, Wallet,
|
||||
} from "lucide-react";
|
||||
|
||||
interface MenuLink {
|
||||
@@ -36,10 +36,6 @@ const MENU: MenuLink[] = [
|
||||
{ href: "/m/admin/statistics", label: "월간 매출", icon: TrendingUp, roles: ["ADMIN"], group: "통계" },
|
||||
{ href: "/m/admin/statistics/daily", label: "일자별", icon: TrendingUp, roles: ["ADMIN"], group: "통계" },
|
||||
{ href: "/m/admin/statistics/margin", label: "원가/마진", icon: TrendingUp, roles: ["ADMIN"], group: "통계" },
|
||||
|
||||
{ href: "/m/admin/users", label: "회원 관리", icon: Users, roles: ["ADMIN"], group: "시스템" },
|
||||
{ href: "/m/admin/roles", label: "권한 관리", icon: ShieldCheck, roles: ["ADMIN"], group: "시스템" },
|
||||
{ href: "/m/admin/menus", label: "메뉴 관리", icon: Layers, roles: ["ADMIN"], group: "시스템" },
|
||||
];
|
||||
|
||||
export function MomoSidebar({ role }: { role: "USER" | "ADMIN" }) {
|
||||
|
||||
Reference in New Issue
Block a user