feat(header,procurement): 관리자 토글 가드 강화 + 매입발주 발주지사(HQ/KIMPO) 셀렉트
Deploy momo-erp / deploy (push) Successful in 4m27s
Deploy momo-erp / deploy (push) Successful in 4m27s
- header: 메뉴 변환 버튼은 authority_master "관리자" 권한그룹 멤버만 노출. user_type='A' 만으로는 부족 (실무자 다수에 부여돼 있음). /api/auth/me 가 isMasterAdmin 플래그 반환. - procurements: 발주서에 발주지사 셀렉트 추가 (기준 명세표 마스터 사용 — HQ/KIMPO 등). 통계/계산서 발행 시 지사별 집계 가능.
This commit is contained in:
@@ -19,7 +19,9 @@ interface ProcDetail {
|
||||
DELIVERY_PERIOD?: string;
|
||||
PAYMENT_TERMS?: string;
|
||||
FREIGHT_TERMS?: string;
|
||||
BRANCH?: string;
|
||||
}
|
||||
interface StatementBranch { CODE: string; NAME: string; IS_DEFAULT: string; SORT_ORDER: number }
|
||||
interface ProcLine {
|
||||
OBJID: string; ITEM_OBJID: string; ITEM_CODE: string; ITEM_NAME: string;
|
||||
UNIT: string; QTY: number; COST_PRICE: number; TOTAL_AMOUNT: number;
|
||||
@@ -59,6 +61,7 @@ export default function ProcurementsPage() {
|
||||
const [activeId, setActiveId] = useState("");
|
||||
const [detail, setDetail] = useState<{ proc: ProcDetail; items: ProcLine[] } | null>(null);
|
||||
const [vendors, setVendors] = useState<Vendor[]>([]);
|
||||
const [branches, setBranches] = useState<StatementBranch[]>([]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
|
||||
@@ -83,6 +86,10 @@ export default function ProcurementsPage() {
|
||||
const r = await fetch("/api/m/vendors/list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" });
|
||||
setVendors((await r.json()).RESULTLIST ?? []);
|
||||
};
|
||||
const loadBranches = async () => {
|
||||
const r = await fetch("/api/m/admin/statement-branches/list", { method: "POST" });
|
||||
setBranches((await r.json()).RESULTLIST ?? []);
|
||||
};
|
||||
|
||||
const loadDetail = useCallback(async () => {
|
||||
if (!activeId) { setDetail(null); return; }
|
||||
@@ -94,7 +101,7 @@ export default function ProcurementsPage() {
|
||||
if (j.success) setDetail({ proc: j.proc, items: j.items });
|
||||
}, [activeId]);
|
||||
|
||||
useEffect(() => { loadVendors(); }, []);
|
||||
useEffect(() => { loadVendors(); loadBranches(); }, []);
|
||||
useEffect(() => { load(); }, [load]);
|
||||
useEffect(() => { loadDetail(); }, [loadDetail]);
|
||||
|
||||
@@ -109,7 +116,7 @@ export default function ProcurementsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const updateHeader = async (patch: { vendorObjid?: string | null; memo?: string }) => {
|
||||
const updateHeader = async (patch: Record<string, unknown>) => {
|
||||
if (!detail) return;
|
||||
const res = await fetch("/api/m/procurements/update-header", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
@@ -302,7 +309,9 @@ export default function ProcurementsPage() {
|
||||
<ProcurementForm
|
||||
detail={detail}
|
||||
vendors={vendors}
|
||||
branches={branches}
|
||||
onSetVendor={(v) => updateHeader({ vendorObjid: v || null })}
|
||||
onSetBranch={(b) => updateHeader({ branch: b })}
|
||||
onSetMemo={(m) => updateHeader({ memo: m })}
|
||||
onSetTerm={(field, val) => updateHeader({ [field]: val })}
|
||||
onAddPicker={() => setPickerOpen(true)}
|
||||
@@ -342,10 +351,12 @@ export default function ProcurementsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function ProcurementForm({ detail, vendors, onSetVendor, onSetMemo, onSetTerm, onAddPicker, onUpdateLine, onDeleteLine }: {
|
||||
function ProcurementForm({ detail, vendors, branches, onSetVendor, onSetBranch, onSetMemo, onSetTerm, onAddPicker, onUpdateLine, onDeleteLine }: {
|
||||
detail: { proc: ProcDetail; items: ProcLine[] };
|
||||
vendors: Vendor[];
|
||||
branches: StatementBranch[];
|
||||
onSetVendor: (id: string) => void;
|
||||
onSetBranch: (code: string) => void;
|
||||
onSetMemo: (m: string) => void;
|
||||
onSetTerm: (field: "deliveryPlace" | "deliveryPeriod" | "paymentTerms" | "freightTerms", val: string) => void;
|
||||
onAddPicker: () => void;
|
||||
@@ -406,7 +417,7 @@ function ProcurementForm({ detail, vendors, onSetVendor, onSetMemo, onSetTerm, o
|
||||
<tbody>
|
||||
<tr>
|
||||
<th className="border border-slate-400 bg-slate-100 px-2 py-1 w-[100px] text-center">분류번호</th>
|
||||
<td className="border border-slate-400 px-3 py-1 font-semibold w-[200px]">매입발주</td>
|
||||
<td className="border border-slate-400 px-3 py-1 font-semibold w-[260px]">매입발주</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="border border-slate-400 bg-slate-100 px-2 py-1 text-center">발주서번호</th>
|
||||
@@ -416,6 +427,28 @@ function ProcurementForm({ detail, vendors, onSetVendor, onSetMemo, onSetTerm, o
|
||||
<th className="border border-slate-400 bg-slate-100 px-2 py-1 text-center">발주일</th>
|
||||
<td className="border border-slate-400 px-3 py-1">{detail.proc.PROC_DATE}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="border border-slate-400 bg-slate-100 px-2 py-1 text-center">발주지사</th>
|
||||
<td className="border border-slate-400 px-3 py-1">
|
||||
{editable ? (
|
||||
<select
|
||||
value={detail.proc.BRANCH ?? branches.find((b) => b.IS_DEFAULT === "Y")?.CODE ?? "HQ"}
|
||||
onChange={(e) => onSetBranch(e.target.value)}
|
||||
className="h-7 px-2 rounded border border-slate-300 text-[13px] bg-white"
|
||||
>
|
||||
{branches.length === 0 ? (
|
||||
<option value="HQ">본사 (HQ)</option>
|
||||
) : branches.map((b) => (
|
||||
<option key={b.CODE} value={b.CODE}>{b.NAME} ({b.CODE}){b.IS_DEFAULT === "Y" ? " ★" : ""}</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<span className="font-semibold">
|
||||
{branches.find((b) => b.CODE === (detail.proc.BRANCH ?? "HQ"))?.NAME ?? (detail.proc.BRANCH ?? "본사")} 발주
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="border border-slate-400 bg-slate-100 px-2 py-1 text-center">공급업체</th>
|
||||
<td className="border border-slate-400 px-3 py-1">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { queryOne } from "@/lib/db";
|
||||
import { SUPER_ADMIN } from "@/lib/constants";
|
||||
|
||||
export async function GET() {
|
||||
const user = await getSession();
|
||||
@@ -10,6 +11,28 @@ export async function GET() {
|
||||
// 특수 권한 (발주한도 무시 / 숨김품목 보기) + 기본 용차비 설정 같이 내려줌.
|
||||
// default_charter_* 컬럼이 운영DB 에 아직 없을 수 있어 try-catch 로 안전 처리.
|
||||
let perms = { unlimitedQty: false, viewHidden: false, defaultCharterUse: false, defaultCharterPrice: 0 };
|
||||
// 관리자 메뉴 토글 노출용 — "관리자 권한그룹" 멤버십 검사.
|
||||
// user_type='A' 만으로는 부족 (실무자에게 광범위하게 부여돼 있어 메뉴 토글 노출이 과함).
|
||||
// authority_master.auth_code='ADMIN' 또는 auth_name 에 '관리자' 포함 + active 상태 그룹에 소속된 경우만 true.
|
||||
// SUPER_ADMIN(plm_admin) 은 항상 통과.
|
||||
let isMasterAdmin = user.userId?.toLowerCase() === SUPER_ADMIN;
|
||||
try {
|
||||
if (!isMasterAdmin) {
|
||||
const row = await queryOne<{ N: string }>(
|
||||
`SELECT 1 AS "N"
|
||||
FROM authority_sub_user ASU
|
||||
JOIN authority_master AM ON AM.objid = ASU.master_objid
|
||||
WHERE ASU.user_id = $1
|
||||
AND COALESCE(AM.status, 'active') = 'active'
|
||||
AND (UPPER(COALESCE(AM.auth_code,'')) = 'ADMIN' OR AM.auth_name LIKE '%관리자%')
|
||||
LIMIT 1`,
|
||||
[user.userId]
|
||||
);
|
||||
isMasterAdmin = !!row;
|
||||
}
|
||||
} catch {
|
||||
// authority 테이블 미존재 등 — 폴백: SUPER_ADMIN 만 true
|
||||
}
|
||||
try {
|
||||
const row = await queryOne<{ U: string; V: string; CU: string; CP: string }>(
|
||||
`SELECT COALESCE(unlimited_qty, 'N') AS "U",
|
||||
@@ -36,5 +59,5 @@ export async function GET() {
|
||||
if (row) perms = { ...perms, unlimitedQty: row.U === "Y", viewHidden: row.V === "Y" };
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
return NextResponse.json({ success: true, user: { ...user, ...perms } });
|
||||
return NextResponse.json({ success: true, user: { ...user, ...perms, isMasterAdmin } });
|
||||
}
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryOne, queryRows } from "@/lib/db";
|
||||
import { pool, queryOne, queryRows } from "@/lib/db";
|
||||
import { requireMomoAdmin } from "@/lib/momo-guard";
|
||||
|
||||
// momo_procurements 에 branch 컬럼이 없으면 1회 자동 증설
|
||||
let branchColEnsured = false;
|
||||
async function ensureBranchCol() {
|
||||
if (branchColEnsured) return;
|
||||
try {
|
||||
await pool.query(`
|
||||
ALTER TABLE momo_procurements
|
||||
ADD COLUMN IF NOT EXISTS branch VARCHAR(20) DEFAULT 'HQ';
|
||||
`);
|
||||
branchColEnsured = true;
|
||||
} catch (err) {
|
||||
console.error("[procurements/ensureBranchCol]", err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const g = await requireMomoAdmin();
|
||||
if (g instanceof NextResponse) return g;
|
||||
await ensureBranchCol();
|
||||
const { objid } = await req.json();
|
||||
const proc = await queryOne(
|
||||
`SELECT P.objid AS "OBJID", P.proc_no AS "PROC_NO",
|
||||
@@ -14,6 +30,7 @@ export async function POST(req: NextRequest) {
|
||||
P.delivery_period AS "DELIVERY_PERIOD",
|
||||
P.payment_terms AS "PAYMENT_TERMS",
|
||||
P.freight_terms AS "FREIGHT_TERMS",
|
||||
COALESCE(P.branch, 'HQ') AS "BRANCH",
|
||||
V.objid AS "VENDOR_OBJID", V.supply_name AS "VENDOR_NAME"
|
||||
FROM momo_procurements P
|
||||
LEFT JOIN supply_mng V ON P.vendor_objid = V.objid::text
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
// 매입 발주서 헤더 (공급업체/메모) 수정 — OPEN/REQUESTED 상태만
|
||||
// 매입 발주서 헤더 (공급업체/메모/지사) 수정 — OPEN/REQUESTED 상태만
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pool } from "@/lib/db";
|
||||
import { requireMomoAdmin } from "@/lib/momo-guard";
|
||||
|
||||
let branchColEnsured = false;
|
||||
async function ensureBranchCol() {
|
||||
if (branchColEnsured) return;
|
||||
try {
|
||||
await pool.query(`
|
||||
ALTER TABLE momo_procurements
|
||||
ADD COLUMN IF NOT EXISTS branch VARCHAR(20) DEFAULT 'HQ';
|
||||
`);
|
||||
branchColEnsured = true;
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const g = await requireMomoAdmin();
|
||||
if (g instanceof NextResponse) return g;
|
||||
await ensureBranchCol();
|
||||
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const { objid, vendorObjid, memo, deliveryPlace, deliveryPeriod, paymentTerms, freightTerms } = body as {
|
||||
const { objid, vendorObjid, memo, deliveryPlace, deliveryPeriod, paymentTerms, freightTerms, branch } = body as {
|
||||
objid?: string; vendorObjid?: string | null; memo?: string;
|
||||
deliveryPlace?: string; deliveryPeriod?: string; paymentTerms?: string; freightTerms?: string;
|
||||
branch?: string;
|
||||
};
|
||||
if (!objid) return NextResponse.json({ success: false, message: "objid 누락" }, { status: 400 });
|
||||
|
||||
@@ -30,6 +44,7 @@ export async function POST(req: NextRequest) {
|
||||
if (deliveryPeriod !== undefined) { sets.push(`delivery_period = $${i++}`); params.push(deliveryPeriod); }
|
||||
if (paymentTerms !== undefined) { sets.push(`payment_terms = $${i++}`); params.push(paymentTerms); }
|
||||
if (freightTerms !== undefined) { sets.push(`freight_terms = $${i++}`); params.push(freightTerms); }
|
||||
if (branch !== undefined) { sets.push(`branch = $${i++}`); params.push(branch); }
|
||||
if (sets.length === 0) return NextResponse.json({ success: true });
|
||||
|
||||
await pool.query(`UPDATE momo_procurements SET ${sets.join(", ")} WHERE objid = $1`, params);
|
||||
|
||||
@@ -10,7 +10,8 @@ export function Header() {
|
||||
const router = useRouter();
|
||||
const { user, logout } = useAuthStore();
|
||||
const { topMenus, activeTopMenu, fetchTopMenus, fetchSideMenus, setMobileOpen, viewMode, setViewMode } = useMenuStore();
|
||||
const isAdminUser = !!user && (user.isAdmin || user.role === "ADMIN" || user.userType === "A");
|
||||
// 관리자 메뉴 토글 — 관리자 권한그룹 멤버만 (user_type='A' 만으로는 부족, 실무자도 다수 부여돼 있음)
|
||||
const isAdminUser = !!user && (user.isMasterAdmin === true || user.isAdmin === true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTopMenus();
|
||||
|
||||
@@ -17,6 +17,9 @@ export interface User {
|
||||
authName: string;
|
||||
partnerCd: string;
|
||||
isAdmin: boolean;
|
||||
// 관리자 메뉴 토글 노출 가드 — 관리자 권한그룹(authority_master) 멤버일 때만 true
|
||||
// /api/auth/me 에서 채워짐. user_type='A' 같은 광범위 플래그와 분리해서 사용.
|
||||
isMasterAdmin?: boolean;
|
||||
// MOMO 추가 필드 (선택 — FITO 사용자에게는 비어있음)
|
||||
role?: "USER" | "ADMIN";
|
||||
objid?: string;
|
||||
|
||||
Reference in New Issue
Block a user