This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Plus, Search, Trash2, History, ArrowRightLeft, Package } from "lucide-react";
|
||||
import { Plus, Search, Trash2, History, ArrowRightLeft, Package, LayoutGrid, Columns3 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
@@ -35,6 +35,29 @@ export default function InventoryPage() {
|
||||
const [trItem, setTrItem] = useState("");
|
||||
const [trQty, setTrQty] = useState(1);
|
||||
|
||||
// 매트릭스 보기 모드 — 기본 '품목 가로' (헤더=품목, 행=창고). 토글로 '창고 가로'.
|
||||
const [viewMode, setViewMode] = useState<"by-item" | "by-wh">("by-item");
|
||||
|
||||
// list 평면 → 매트릭스 pivot
|
||||
const matrix = useMemo(() => {
|
||||
const itemSet = new Map<string, { OBJID: string; CODE: string; NAME: string; UNIT: string; IS_TAX_FREE: string }>();
|
||||
const whSet = new Map<string, { OBJID: string; CODE: string; NAME: string }>();
|
||||
const cell: Record<string, Record<string, { qty: number; updateDate: string }>> = {};
|
||||
// cell[itemObjid][whObjid] = { qty, updateDate }
|
||||
for (const s of list) {
|
||||
if (!itemSet.has(s.ITEM_OBJID)) itemSet.set(s.ITEM_OBJID, {
|
||||
OBJID: s.ITEM_OBJID, CODE: s.ITEM_CODE, NAME: s.ITEM_NAME, UNIT: s.UNIT, IS_TAX_FREE: s.IS_TAX_FREE,
|
||||
});
|
||||
if (!whSet.has(s.WH_OBJID)) whSet.set(s.WH_OBJID, { OBJID: s.WH_OBJID, CODE: s.WH_CODE, NAME: s.WH_NAME });
|
||||
if (!cell[s.ITEM_OBJID]) cell[s.ITEM_OBJID] = {};
|
||||
cell[s.ITEM_OBJID][s.WH_OBJID] = { qty: Number(s.QTY), updateDate: s.UPDATE_DATE };
|
||||
}
|
||||
// 창고 7개 — list 에 없는 창고도 헤더에 포함시키려면 whs 사용
|
||||
const allWhs = [...whs].sort((a, b) => a.WH_CODE.localeCompare(b.WH_CODE));
|
||||
const itemList = Array.from(itemSet.values()).sort((a, b) => a.NAME.localeCompare(b.NAME));
|
||||
return { items: itemList, warehouses: allWhs, cell };
|
||||
}, [list, whs]);
|
||||
|
||||
const load = async () => {
|
||||
const res = await fetch("/api/m/inventory/list", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
@@ -158,46 +181,110 @@ export default function InventoryPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 데스크톱 테이블 */}
|
||||
<div className="hidden sm:block bg-white border border-slate-200 rounded-xl overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm min-w-[640px]">
|
||||
<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-center px-4 py-3">구분</th>
|
||||
<th className="text-right px-4 py-3">현재고</th>
|
||||
<th className="text-left px-4 py-3">최종 변경</th>
|
||||
<th className="text-center px-4 py-3 w-[100px]">재고이력</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{list.length === 0 ? (
|
||||
<tr><td colSpan={7} className="text-center py-12 text-slate-400">재고 데이터가 없습니다. 매입 입고로 등록하세요.</td></tr>
|
||||
) : list.map((s) => (
|
||||
<tr key={s.OBJID} className="border-t border-slate-100">
|
||||
<td className="px-4 py-3">{s.WH_NAME}</td>
|
||||
<td className="px-4 py-3 font-mono text-xs">{s.ITEM_CODE}</td>
|
||||
<td className="px-4 py-3 font-semibold">{s.ITEM_NAME}</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
{s.IS_TAX_FREE === "Y"
|
||||
? <span className="px-1.5 py-0.5 rounded bg-violet-100 text-violet-700 text-[10px] font-bold">면세</span>
|
||||
: <span className="px-1.5 py-0.5 rounded bg-rose-100 text-rose-700 text-[10px] font-bold">과세</span>}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums font-bold">{fmt(s.QTY)} {s.UNIT}</td>
|
||||
<td className="px-4 py-3 text-slate-500 text-xs">{s.UPDATE_DATE}</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<button onClick={() => setHistoryOpen({ itemObjid: s.ITEM_OBJID, whObjid: s.WH_OBJID, itemName: s.ITEM_NAME, whName: s.WH_NAME })}
|
||||
className="inline-flex items-center gap-1 h-7 px-2 rounded bg-slate-100 hover:bg-slate-200 text-slate-700 text-[11px] font-bold">
|
||||
<History size={12} /> 이력
|
||||
</button>
|
||||
</td>
|
||||
{/* 데스크톱 — 매트릭스 토글 (품목 가로 ↔ 창고 가로) */}
|
||||
<div className="hidden sm:block">
|
||||
<div className="flex items-center justify-end mb-2">
|
||||
<div className="inline-flex rounded border border-slate-300 overflow-hidden">
|
||||
<button onClick={() => setViewMode("by-item")}
|
||||
className={`h-8 px-3 text-xs font-semibold inline-flex items-center gap-1 ${viewMode === "by-item" ? "bg-emerald-700 text-white" : "bg-white text-slate-600 hover:bg-slate-50"}`}>
|
||||
<LayoutGrid size={13} /> 품목 가로
|
||||
</button>
|
||||
<button onClick={() => setViewMode("by-wh")}
|
||||
className={`h-8 px-3 text-xs font-semibold inline-flex items-center gap-1 border-l border-slate-300 ${viewMode === "by-wh" ? "bg-emerald-700 text-white" : "bg-white text-slate-600 hover:bg-slate-50"}`}>
|
||||
<Columns3 size={13} /> 창고 가로
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white border border-slate-200 rounded-xl overflow-x-auto">
|
||||
{matrix.items.length === 0 ? (
|
||||
<div className="text-center py-12 text-slate-400">재고 데이터가 없습니다. 매입 입고로 등록하세요.</div>
|
||||
) : viewMode === "by-item" ? (
|
||||
/* 품목 가로 (기본): 헤더=품목, 행=창고 */
|
||||
<table className="text-sm border-collapse">
|
||||
<thead className="bg-slate-50 text-slate-600 sticky top-0">
|
||||
<tr>
|
||||
<th className="text-left px-3 py-2 border-b border-slate-200 sticky left-0 bg-slate-50 z-10 min-w-[140px]">창고</th>
|
||||
{matrix.items.map((it) => (
|
||||
<th key={it.OBJID} className="text-right px-3 py-2 border-b border-slate-200 min-w-[100px] whitespace-nowrap">
|
||||
<div>{it.NAME}</div>
|
||||
<div className="text-[10px] text-slate-400 font-mono font-normal">{it.CODE}</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody className="tabular-nums">
|
||||
{matrix.warehouses.map((w) => (
|
||||
<tr key={w.OBJID} className="border-t border-slate-100 hover:bg-slate-50/60">
|
||||
<td className="px-3 py-2 font-semibold sticky left-0 bg-white">
|
||||
{w.WH_NAME}
|
||||
<div className="text-[10px] text-slate-400 font-mono">{w.WH_CODE}</div>
|
||||
</td>
|
||||
{matrix.items.map((it) => {
|
||||
const c = matrix.cell[it.OBJID]?.[w.OBJID];
|
||||
const qty = c ? c.qty : 0;
|
||||
return (
|
||||
<td key={it.OBJID} className={`px-3 py-2 text-right ${qty === 0 ? "text-slate-300" : "text-slate-800 font-semibold"}`}>
|
||||
{qty === 0 ? "-" : (
|
||||
<button
|
||||
onClick={() => setHistoryOpen({ itemObjid: it.OBJID, whObjid: w.OBJID, itemName: it.NAME, whName: w.WH_NAME })}
|
||||
className="hover:underline hover:text-emerald-700"
|
||||
title="재고 이력"
|
||||
>
|
||||
{fmt(qty)} {it.UNIT}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
/* 창고 가로: 헤더=창고, 행=품목 */
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead className="bg-slate-50 text-slate-600 sticky top-0">
|
||||
<tr>
|
||||
<th className="text-left px-3 py-2 border-b border-slate-200 min-w-[200px]">품목</th>
|
||||
{matrix.warehouses.map((w) => (
|
||||
<th key={w.OBJID} className="text-right px-3 py-2 border-b border-slate-200 min-w-[100px]">
|
||||
{w.WH_NAME}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="tabular-nums">
|
||||
{matrix.items.map((it) => (
|
||||
<tr key={it.OBJID} className="border-t border-slate-100 hover:bg-slate-50/60">
|
||||
<td className="px-3 py-2">
|
||||
<span className="font-semibold">{it.NAME}</span>
|
||||
<span className="ml-2 text-[10px] text-slate-400 font-mono">{it.CODE}</span>
|
||||
{it.IS_TAX_FREE === "Y"
|
||||
? <span className="ml-2 px-1 py-0.5 rounded bg-violet-100 text-violet-700 text-[9px] font-bold">면세</span>
|
||||
: <span className="ml-2 px-1 py-0.5 rounded bg-rose-100 text-rose-700 text-[9px] font-bold">과세</span>}
|
||||
</td>
|
||||
{matrix.warehouses.map((w) => {
|
||||
const c = matrix.cell[it.OBJID]?.[w.OBJID];
|
||||
const qty = c ? c.qty : 0;
|
||||
return (
|
||||
<td key={w.OBJID} className={`px-3 py-2 text-right ${qty === 0 ? "text-slate-300" : "text-slate-800 font-semibold"}`}>
|
||||
{qty === 0 ? "-" : (
|
||||
<button
|
||||
onClick={() => setHistoryOpen({ itemObjid: it.OBJID, whObjid: w.OBJID, itemName: it.NAME, whName: w.WH_NAME })}
|
||||
className="hover:underline hover:text-emerald-700"
|
||||
title="재고 이력"
|
||||
>
|
||||
{fmt(qty)} {it.UNIT}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user