feat(stock): 창고별 재고 현황 — 보기 토글 (창고 가로 ↔ 품목 가로)
Deploy momo-erp / deploy (push) Successful in 2m17s

토글 버튼 2개로 표시 형식 전환:
- '창고 가로' (기본): 헤더=창고 7개, 행=품목 (가로로 김)
- '품목 가로': 헤더=품목, 행=창고 7줄 (오른쪽으로 길게, 좌측 sticky)

각 모드에서 동일하게 셀당 '발주수량(현재고) / 여유분' 2행. 여유분 음수면 rose 강조.
This commit is contained in:
chpark
2026-05-14 22:09:19 +09:00
parent fac0f0d83e
commit 280495d741
+117 -50
View File
@@ -1,7 +1,7 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { RefreshCcw, Warehouse, Download } from "lucide-react";
import { RefreshCcw, Warehouse, Download, LayoutGrid, Columns3 } from "lucide-react";
import { downloadXlsx } from "@/lib/xlsx-export";
interface Wh { OBJID: string; WH_CODE: string; WH_NAME: string }
@@ -31,6 +31,8 @@ export default function WhStockStatusPage() {
const [warehouses, setWarehouses] = useState<Wh[]>([]);
const [items, setItems] = useState<ItemRow[]>([]);
const [loading, setLoading] = useState(false);
// 보기 모드: by-wh = 헤더가 창고(가로로 김) / by-item = 헤더가 품목(오른쪽으로 길게)
const [viewMode, setViewMode] = useState<"by-wh" | "by-item">("by-wh");
const load = useCallback(async () => {
setLoading(true);
@@ -80,7 +82,18 @@ export default function WhStockStatusPage() {
(///) . <b></b> = , <b></b> = ( ).
</p>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 flex-wrap">
{/* 보기 모드 토글 */}
<div className="inline-flex rounded border border-slate-300 overflow-hidden">
<button onClick={() => setViewMode("by-wh")}
className={`h-9 px-3 text-xs font-semibold inline-flex items-center gap-1 ${viewMode === "by-wh" ? "bg-emerald-700 text-white" : "bg-white text-slate-600 hover:bg-slate-50"}`}>
<Columns3 size={14} />
</button>
<button onClick={() => setViewMode("by-item")}
className={`h-9 px-3 text-xs font-semibold inline-flex items-center gap-1 border-l border-slate-300 ${viewMode === "by-item" ? "bg-emerald-700 text-white" : "bg-white text-slate-600 hover:bg-slate-50"}`}>
<LayoutGrid size={14} />
</button>
</div>
<input type="date" value={dateFrom} onChange={(e) => setRange([e.target.value, dateTo])}
className="h-9 px-3 rounded border border-slate-300 text-sm" />
<span className="text-slate-400">~</span>
@@ -100,54 +113,108 @@ export default function WhStockStatusPage() {
</div>
<div className="bg-white border border-slate-200 rounded-xl overflow-x-auto">
<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-[160px]"></th>
<th className="text-center px-3 py-2 border-b border-slate-200 min-w-[100px]"></th>
{warehouses.map((w) => (
<th key={w.WH_CODE} className="text-right px-3 py-2 border-b border-slate-200 min-w-[88px]">
{w.WH_NAME}
</th>
))}
</tr>
</thead>
<tbody className="tabular-nums">
{items.length === 0 ? (
<tr><td colSpan={2 + warehouses.length} className="text-center py-12 text-slate-400">
{loading ? "조회 중..." : "데이터가 없습니다."}
</td></tr>
) : items.flatMap((it) => [
<tr key={`${it.ITEM_OBJID}-stock`} className="border-t border-slate-100 hover:bg-slate-50/60">
<td className="px-3 py-2 align-top font-semibold" rowSpan={2}>
{it.ITEM_NAME}
<div className="text-[10px] text-slate-400 font-mono">{it.ITEM_CODE}</div>
</td>
<td className="px-3 py-1.5 text-center text-[11px] text-slate-700 bg-slate-50/70"></td>
{warehouses.map((w) => {
const v = Number(it.STOCK[w.WH_CODE] ?? 0);
return (
<td key={w.WH_CODE} className={`px-3 py-1.5 text-right ${v === 0 ? "text-slate-300" : "text-slate-800"}`}>
{v === 0 ? "-" : fmt(v)}
</td>
);
})}
</tr>,
<tr key={`${it.ITEM_OBJID}-avail`} className="border-b border-slate-100">
<td className="px-3 py-1.5 text-center text-[11px] text-emerald-700 bg-emerald-50/40 font-semibold"></td>
{warehouses.map((w) => {
const v = Number(it.AVAILABLE[w.WH_CODE] ?? 0);
const negative = v < 0;
return (
<td key={w.WH_CODE} className={`px-3 py-1.5 text-right ${negative ? "text-rose-600 font-bold" : v === 0 ? "text-slate-300" : "text-emerald-700 font-semibold"}`}>
{v === 0 ? "-" : fmt(v)}
</td>
);
})}
</tr>,
])}
</tbody>
</table>
{viewMode === "by-wh" ? (
/* 보기 1: 헤더=창고(가로), 행=품목 */
<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-[160px]"></th>
<th className="text-center px-3 py-2 border-b border-slate-200 min-w-[100px]"></th>
{warehouses.map((w) => (
<th key={w.WH_CODE} className="text-right px-3 py-2 border-b border-slate-200 min-w-[88px]">
{w.WH_NAME}
</th>
))}
</tr>
</thead>
<tbody className="tabular-nums">
{items.length === 0 ? (
<tr><td colSpan={2 + warehouses.length} className="text-center py-12 text-slate-400">
{loading ? "조회 중..." : "데이터가 없습니다."}
</td></tr>
) : items.flatMap((it) => [
<tr key={`${it.ITEM_OBJID}-stock`} className="border-t border-slate-100 hover:bg-slate-50/60">
<td className="px-3 py-2 align-top font-semibold" rowSpan={2}>
{it.ITEM_NAME}
<div className="text-[10px] text-slate-400 font-mono">{it.ITEM_CODE}</div>
</td>
<td className="px-3 py-1.5 text-center text-[11px] text-slate-700 bg-slate-50/70"></td>
{warehouses.map((w) => {
const v = Number(it.STOCK[w.WH_CODE] ?? 0);
return (
<td key={w.WH_CODE} className={`px-3 py-1.5 text-right ${v === 0 ? "text-slate-300" : "text-slate-800"}`}>
{v === 0 ? "-" : fmt(v)}
</td>
);
})}
</tr>,
<tr key={`${it.ITEM_OBJID}-avail`} className="border-b border-slate-100">
<td className="px-3 py-1.5 text-center text-[11px] text-emerald-700 bg-emerald-50/40 font-semibold"></td>
{warehouses.map((w) => {
const v = Number(it.AVAILABLE[w.WH_CODE] ?? 0);
const negative = v < 0;
return (
<td key={w.WH_CODE} className={`px-3 py-1.5 text-right ${negative ? "text-rose-600 font-bold" : v === 0 ? "text-slate-300" : "text-emerald-700 font-semibold"}`}>
{v === 0 ? "-" : fmt(v)}
</td>
);
})}
</tr>,
])}
</tbody>
</table>
) : (
/* 보기 2: 헤더=품목(가로), 행=창고 7줄 — 전치 */
<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-[120px]"></th>
<th className="text-center px-3 py-2 border-b border-slate-200 sticky left-[120px] bg-slate-50 z-10 min-w-[90px]"></th>
{items.map((it) => (
<th key={it.ITEM_OBJID} className="text-right px-3 py-2 border-b border-slate-200 min-w-[100px] whitespace-nowrap">
<div>{it.ITEM_NAME}</div>
<div className="text-[10px] text-slate-400 font-mono font-normal">{it.ITEM_CODE}</div>
</th>
))}
</tr>
</thead>
<tbody className="tabular-nums">
{items.length === 0 || warehouses.length === 0 ? (
<tr><td colSpan={2 + items.length} className="text-center py-12 text-slate-400">
{loading ? "조회 중..." : "데이터가 없습니다."}
</td></tr>
) : warehouses.flatMap((w) => [
<tr key={`${w.WH_CODE}-stock`} className="border-t border-slate-100 hover:bg-slate-50/60">
<td className="px-3 py-2 align-top font-semibold sticky left-0 bg-white" rowSpan={2}>
{w.WH_NAME}
<div className="text-[10px] text-slate-400 font-mono">{w.WH_CODE}</div>
</td>
<td className="px-3 py-1.5 text-center text-[11px] text-slate-700 bg-slate-50/70 sticky left-[120px]"></td>
{items.map((it) => {
const v = Number(it.STOCK[w.WH_CODE] ?? 0);
return (
<td key={it.ITEM_OBJID} className={`px-3 py-1.5 text-right ${v === 0 ? "text-slate-300" : "text-slate-800"}`}>
{v === 0 ? "-" : fmt(v)}
</td>
);
})}
</tr>,
<tr key={`${w.WH_CODE}-avail`} className="border-b border-slate-100">
<td className="px-3 py-1.5 text-center text-[11px] text-emerald-700 bg-emerald-50/40 font-semibold sticky left-[120px]"></td>
{items.map((it) => {
const v = Number(it.AVAILABLE[w.WH_CODE] ?? 0);
const negative = v < 0;
return (
<td key={it.ITEM_OBJID} className={`px-3 py-1.5 text-right ${negative ? "text-rose-600 font-bold" : v === 0 ? "text-slate-300" : "text-emerald-700 font-semibold"}`}>
{v === 0 ? "-" : fmt(v)}
</td>
);
})}
</tr>,
])}
</tbody>
</table>
)}
</div>
</div>
);