From 9e9922e2194484d0576ca193f06b793af7b6eb08 Mon Sep 17 00:00:00 2001 From: chpark Date: Tue, 12 May 2026 00:46:37 +0900 Subject: [PATCH] =?UTF-8?q?feat(perm):=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=ED=8A=B9=EC=88=98=EA=B6=8C=ED=95=9C(=EB=B0=9C=EC=A3=BC?= =?UTF-8?q?=ED=95=9C=EB=8F=84=20=EB=AC=B4=EC=8B=9C=C2=B7=EC=88=A8=EA=B9=80?= =?UTF-8?q?=20=ED=92=88=EB=AA=A9=20=EB=B3=B4=EA=B8=B0)=20UI=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C=20+=20=EC=B6=9C=EA=B3=A0=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [사용자 관리] - /api/admin/users 목록에 UNLIMITED_QTY / VIEW_HIDDEN / USER_TYPE 컬럼 반환 - UserManagement 그리드에 '발주한도무시' / '숨김품목보기' 컬럼 추가 (✅/—) - 사용자 수정 폼: '거래처 특수 권한' → '특수 권한 (발주 시 적용)' 으로 라벨 변경, 거래처(C) 전용이던 조건을 풀어서 일반 사용자(U) 도 권한 부여 가능 [출고요청 (/m/orders/new)] - /api/auth/me 가 unlimitedQty / viewHidden 반환 - 클라이언트가 unlimitedQty true 면 MAX_ORDER_QTY 무시하고 재고만큼 발주 가능 - '한도 ≤ N' 라벨도 권한자에겐 숨김 (백엔드 검증 — /api/m/items/list 의 view_hidden, /api/m/orders/save 의 unlimited_qty 우회 — 는 이미 구현돼 있어 그대로 동작) Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/(main)/m/orders/new/page.tsx | 20 ++++++++++++++------ src/app/admin-panel/page.tsx | 13 ++++++++----- src/app/admin-panel/user-form/page.tsx | 4 ++-- src/app/api/admin/users/route.ts | 3 +++ src/app/api/auth/me/route.ts | 13 ++++++++++++- 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/app/(main)/m/orders/new/page.tsx b/src/app/(main)/m/orders/new/page.tsx index 4a1a969..5c5ceb4 100644 --- a/src/app/(main)/m/orders/new/page.tsx +++ b/src/app/(main)/m/orders/new/page.tsx @@ -39,6 +39,14 @@ export default function ItemsBrowse() { const [extras, setExtras] = useState([]); const [cartOpen, setCartOpen] = useState(false); const [viewMode, setViewMode] = useState<"card" | "list">("card"); + // 현재 사용자의 발주 한도 우회 권한 (관리자 또는 unlimited_qty='Y' 거래처) + const [unlimitedQty, setUnlimitedQty] = useState(false); + + useEffect(() => { + fetch("/api/auth/me").then((r) => r.json()).then((d) => { + if (d?.user) setUnlimitedQty(!!d.user.unlimitedQty || d.user.role === "ADMIN" || d.user.isAdmin === true); + }).catch(() => {}); + }, []); const fetchItems = useCallback(async () => { setLoading(true); @@ -76,7 +84,7 @@ export default function ItemsBrowse() { const addManyToCart = (item: Item, qty: number) => { const stock = Number(item.STOCK_QTY); const maxQ = Number(item.MAX_ORDER_QTY ?? 0); - const limit = maxQ > 0 ? Math.min(stock, maxQ) : stock; + const limit = unlimitedQty || maxQ <= 0 ? stock : Math.min(stock, maxQ); let toastTitle = ""; let warned = false; setCart((c) => { @@ -115,7 +123,7 @@ export default function ItemsBrowse() { if (newQty <= 0) return x; const stock = Number(x.item.STOCK_QTY); const maxQ = Number(x.item.MAX_ORDER_QTY ?? 0); - const limit = maxQ > 0 ? Math.min(stock, maxQ) : stock; + const limit = unlimitedQty || maxQ <= 0 ? stock : Math.min(stock, maxQ); if (newQty > limit) return x; return { ...x, qty: newQty }; }) @@ -128,7 +136,7 @@ export default function ItemsBrowse() { if (x.item.OBJID !== objid) return x; const stock = Number(x.item.STOCK_QTY); const maxQ = Number(x.item.MAX_ORDER_QTY ?? 0); - const limit = maxQ > 0 ? Math.min(stock, maxQ) : stock; + const limit = unlimitedQty || maxQ <= 0 ? stock : Math.min(stock, maxQ); const clamped = Math.max(1, Math.min(limit, Math.floor(value || 0))); return { ...x, qty: clamped }; }) @@ -474,7 +482,7 @@ export default function ItemsBrowse() { const inCart = cartLine?.qty ?? 0; const stock = Number(it.STOCK_QTY); const maxQ = Number(it.MAX_ORDER_QTY ?? 0); - const limit = maxQ > 0 ? Math.min(stock, maxQ) : stock; + const limit = unlimitedQty || maxQ <= 0 ? stock : Math.min(stock, maxQ); const soldOut = stock === 0; return (
0 ? "border-emerald-400 ring-2 ring-emerald-100" : "border-slate-200 hover:shadow-md"}`}> @@ -506,7 +514,7 @@ export default function ItemsBrowse() { {fmt(stock)}{it.UNIT}
- {maxQ > 0 && ( + {maxQ > 0 && !unlimitedQty && (
한도 ≤ {fmt(maxQ)}
)} @@ -623,7 +631,7 @@ function ListView({ items, cart, onAdd, onPlus, onMinus, onSetQty, onRemove }: { const inCart = cartLine?.qty ?? 0; const stock = Number(it.STOCK_QTY); const maxQ = Number(it.MAX_ORDER_QTY ?? 0); - const limit = maxQ > 0 ? Math.min(stock, maxQ) : stock; + const limit = unlimitedQty || maxQ <= 0 ? stock : Math.min(stock, maxQ); const soldOut = stock === 0; return ( 0 ? "bg-emerald-50/40" : "hover:bg-slate-50"}`}> diff --git a/src/app/admin-panel/page.tsx b/src/app/admin-panel/page.tsx index e772cf8..7779b11 100644 --- a/src/app/admin-panel/page.tsx +++ b/src/app/admin-panel/page.tsx @@ -266,14 +266,17 @@ function UserManagement() { const [data, setData] = useState[]>([]); const [selectedRows, setSelectedRows] = useState[]>([]); + const yn = (v: unknown) => v === "Y" ? "✅" : "—"; const columns: GridColumn[] = [ - { title: "부서명", field: "DEPT_NAME", width: 120 }, - { title: "사용자 ID", field: "USER_ID", width: 120, cellClick: (row) => openUserForm(String(row.USER_ID)) }, + { title: "부서명", field: "DEPT_NAME", width: 110 }, + { title: "사용자 ID", field: "USER_ID", width: 110, cellClick: (row) => openUserForm(String(row.USER_ID)) }, { title: "사용자명", field: "USER_NAME", width: 100, hozAlign: "center" }, - { title: "전화번호", field: "CELL_PHONE", width: 130 }, - { title: "이메일", field: "EMAIL", width: 180 }, + { title: "전화번호", field: "CELL_PHONE", width: 120 }, + { title: "이메일", field: "EMAIL", width: 160 }, + { title: "발주한도무시", field: "UNLIMITED_QTY", width: 110, hozAlign: "center", formatter: (cell) => yn(cell) }, + { title: "숨김품목보기", field: "VIEW_HIDDEN", width: 110, hozAlign: "center", formatter: (cell) => yn(cell) }, { title: "등록일", field: "REGDATE", width: 100, hozAlign: "center" }, - { title: "상태", field: "STATUS", width: 80, hozAlign: "center" }, + { title: "상태", field: "STATUS", width: 70, hozAlign: "center" }, ]; const fetchData = useCallback(async () => { diff --git a/src/app/admin-panel/user-form/page.tsx b/src/app/admin-panel/user-form/page.tsx index ee4dc6f..b77e3b2 100644 --- a/src/app/admin-panel/user-form/page.tsx +++ b/src/app/admin-panel/user-form/page.tsx @@ -118,9 +118,9 @@ function UserForm() { )} - {isCustomer && !isNew && ( + {!isNew && (
-

거래처 특수 권한

+

특수 권한 (발주 시 적용)