feat(daily-order-inventory): 창고 × 품목 매트릭스 뷰로 전환
Deploy momo-erp / deploy (push) Successful in 1m53s

이전: 품목 한 줄에 [발주수량 합계 + 전체창고 재고합계] 표시.
변경: 창고별 재고 현황의 "품목 가로" 패턴 차용 — 헤더=품목(가로), 좌측=창고(세로).
      각 셀에 그 창고의 [발주수량 / 재고수량] 두 줄.

API:
- WAREHOUSES + ITEMS(STOCK/ORDER 매트릭스) 형태로 응답
- 발주수량 산정:
  • APPROVED/INVOICED/PAID 발주는 momo_stock_moves OUT 이력의 실제 출고 창고 기준
  • REQUESTED 발주(아직 출고 전)는 거래처 default_wh_objid 로 가상 배정 (fallback WH001)
- 재고수량은 momo_stocks 현재값 그대로

UI:
- 상단 [전체 합계] 두 줄(발주/재고) — 모든 창고 합산
- 각 창고(WH001~WH007) 2행씩 — 발주수량 / 재고수량
- 음수 재고는 적색 강조 (창고별 재고 현황과 동일 톤)
- 엑셀: 창고별 행 + 분류(발주/재고) + 품목 컬럼

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chpark
2026-05-21 14:21:08 +09:00
parent af6726f2b6
commit 1a209ceb29
2 changed files with 251 additions and 141 deletions
@@ -4,7 +4,8 @@ import { useCallback, useEffect, useState } from "react";
import { RefreshCcw, CalendarDays, Download, Search } from "lucide-react";
import { downloadXlsx } from "@/lib/xlsx-export";
interface Row {
interface Wh { OBJID: string; WH_CODE: string; WH_NAME: string }
interface ItemRow {
OBJID: string;
ITEM_CODE: string;
ITEM_NAME: string;
@@ -14,8 +15,10 @@ interface Row {
SALE_START_DATE: string | null;
SALE_END_DATE: string | null;
VENDOR_NAME: string | null;
ORDER_QTY: string | number;
STOCK_QTY: string | number;
STOCK: Record<string, number>; // wh_code → 현재고
ORDER: Record<string, number>; // wh_code → 발주수량
TOTAL_STOCK: number;
TOTAL_ORDER: number;
}
const fmt = (n: number) => Number(n || 0).toLocaleString("ko-KR");
@@ -24,7 +27,8 @@ const today = () => new Date().toISOString().slice(0, 10);
export default function DailyOrderInventoryPage() {
const [targetDate, setTargetDate] = useState<string>(today());
const [keyword, setKeyword] = useState("");
const [rows, setRows] = useState<Row[]>([]);
const [warehouses, setWarehouses] = useState<Wh[]>([]);
const [items, setItems] = useState<ItemRow[]>([]);
const [loading, setLoading] = useState(false);
const load = useCallback(async () => {
@@ -37,9 +41,11 @@ export default function DailyOrderInventoryPage() {
});
if (res.ok) {
const j = await res.json();
setRows(j.RESULTLIST ?? []);
setWarehouses(j.WAREHOUSES ?? []);
setItems(j.ITEMS ?? []);
} else {
setRows([]);
setWarehouses([]);
setItems([]);
}
} finally {
setLoading(false);
@@ -48,28 +54,28 @@ export default function DailyOrderInventoryPage() {
useEffect(() => { load(); }, [load]);
const totalOrderQty = rows.reduce((a, r) => a + Number(r.ORDER_QTY || 0), 0);
const totalStockQty = rows.reduce((a, r) => a + Number(r.STOCK_QTY || 0), 0);
const shortageCnt = rows.filter((r) => Number(r.STOCK_QTY) < Number(r.ORDER_QTY)).length;
const totalOrderQty = items.reduce((a, r) => a + Number(r.TOTAL_ORDER || 0), 0);
const totalStockQty = items.reduce((a, r) => a + Number(r.TOTAL_STOCK || 0), 0);
const onExport = () => {
if (rows.length === 0) return;
downloadXlsx(
`일자별발주재고_${targetDate}`,
rows,
[
{ header: "품목코드", key: "ITEM_CODE", width: 14 },
{ header: "품목명", key: "ITEM_NAME", width: 28 },
{ header: "단위", key: "UNIT", width: 8 },
{ header: "매입처", key: (r) => r.VENDOR_NAME ?? "", width: 18 },
{ header: "면세", key: (r) => (r.IS_TAX_FREE === "Y" ? "면세" : "과세"), width: 8 },
{ header: "판매시작", key: (r) => r.SALE_START_DATE ?? "", width: 12 },
{ header: "판매종료", key: (r) => r.SALE_END_DATE ?? "", width: 12 },
{ header: "발주수량", key: (r) => Number(r.ORDER_QTY), width: 12 },
{ header: "재고수량", key: (r) => Number(r.STOCK_QTY), width: 12 },
{ header: "차이(재고-발주)", key: (r) => Number(r.STOCK_QTY) - Number(r.ORDER_QTY), width: 14 },
]
);
if (items.length === 0 || warehouses.length === 0) return;
type Row = Record<string, string | number>;
const data: Row[] = [];
for (const w of warehouses) {
const orderRow: Row = { WH: `${w.WH_NAME} (${w.WH_CODE})`, KIND: "발주수량" };
const stockRow: Row = { WH: `${w.WH_NAME} (${w.WH_CODE})`, KIND: "재고수량" };
for (const it of items) {
orderRow[it.ITEM_NAME] = Number(it.ORDER[w.WH_CODE] ?? 0);
stockRow[it.ITEM_NAME] = Number(it.STOCK[w.WH_CODE] ?? 0);
}
data.push(orderRow, stockRow);
}
const cols = [
{ header: "창고", key: "WH" },
{ header: "분류", key: "KIND" },
...items.map((it) => ({ header: it.ITEM_NAME, key: it.ITEM_NAME })),
];
downloadXlsx(`일자별발주재고_${targetDate}`, data, cols);
};
return (
@@ -81,7 +87,8 @@ export default function DailyOrderInventoryPage() {
/
</h1>
<p className="text-xs text-slate-500 mt-0.5">
<b> </b> <b> </b>(···) <b> </b> .
<b> </b> , .
= (REQUESTED는 default / APPROVED ). = .
</p>
</div>
<div className="flex items-center gap-2 flex-wrap">
@@ -117,7 +124,7 @@ export default function DailyOrderInventoryPage() {
</button>
<button
onClick={onExport}
disabled={rows.length === 0}
disabled={items.length === 0}
className="h-9 px-3 rounded bg-emerald-700 text-white text-sm font-bold inline-flex items-center gap-1 hover:bg-emerald-800 disabled:opacity-40"
>
<Download size={14} />
@@ -126,73 +133,97 @@ export default function DailyOrderInventoryPage() {
</div>
{/* 요약 카드 */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<SummaryCard label="판매가능 품목" value={`${fmt(rows.length)}`} color="slate" />
<div className="grid grid-cols-3 sm:grid-cols-3 gap-3">
<SummaryCard label="판매가능 품목" value={`${fmt(items.length)}`} color="slate" />
<SummaryCard label="발주수량 합계" value={`${fmt(totalOrderQty)}`} color="rose" />
<SummaryCard label="재고수량 합계" value={`${fmt(totalStockQty)}`} color="emerald" />
<SummaryCard label="재고 부족 품목" value={`${fmt(shortageCnt)}`} color="amber" />
</div>
{/* 그리드 */}
{/* 매트릭스: 헤더=품목, 행=창고 */}
<div className="bg-white border border-slate-200 rounded-xl overflow-x-auto">
<table className="w-full text-sm border-collapse">
<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 min-w-[120px]"></th>
<th className="text-left px-3 py-2 border-b border-slate-200 min-w-[200px]"></th>
<th className="text-center px-3 py-2 border-b border-slate-200 w-[60px]"></th>
<th className="text-left px-3 py-2 border-b border-slate-200 min-w-[120px]"></th>
<th className="text-center px-3 py-2 border-b border-slate-200 w-[60px]"></th>
<th className="text-center px-3 py-2 border-b border-slate-200 min-w-[160px]"></th>
<th className="text-right px-3 py-2 border-b border-slate-200 min-w-[100px]"></th>
<th className="text-right px-3 py-2 border-b border-slate-200 min-w-[100px]"></th>
<th className="text-right px-3 py-2 border-b border-slate-200 min-w-[100px]"></th>
<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.OBJID} className="text-right px-3 py-2 border-b border-slate-200 min-w-[110px] whitespace-nowrap">
<div>{it.ITEM_NAME}</div>
<div className="text-[10px] text-slate-400 font-mono font-normal">{it.ITEM_CODE}</div>
{it.SALE_END_DATE && (
<div className="text-[10px] text-rose-600 font-semibold tabular-nums">~ {it.SALE_END_DATE}</div>
)}
</th>
))}
</tr>
</thead>
<tbody className="tabular-nums">
{rows.length === 0 ? (
{items.length === 0 || warehouses.length === 0 ? (
<tr>
<td colSpan={9} className="text-center py-12 text-slate-400">
<td colSpan={2 + items.length} className="text-center py-12 text-slate-400">
{loading ? "조회 중..." : "데이터가 없습니다."}
</td>
</tr>
) : (
rows.map((r) => {
const orderQty = Number(r.ORDER_QTY);
const stockQty = Number(r.STOCK_QTY);
const diff = stockQty - orderQty;
const shortage = diff < 0;
return (
<tr key={r.OBJID} className="border-t border-slate-100 hover:bg-slate-50/60">
<td className="px-3 py-2 font-mono text-[11px] text-slate-500">{r.ITEM_CODE}</td>
<td className="px-3 py-2 font-semibold text-slate-800">{r.ITEM_NAME}</td>
<td className="px-3 py-2 text-center text-slate-600">{r.UNIT || "-"}</td>
<td className="px-3 py-2 text-slate-600">{r.VENDOR_NAME || "-"}</td>
<td className="px-3 py-2 text-center">
<span className={`inline-block px-1.5 py-0.5 rounded text-[10px] font-bold ${
r.IS_TAX_FREE === "Y" ? "bg-violet-100 text-violet-700" : "bg-rose-100 text-rose-700"
}`}>
{r.IS_TAX_FREE === "Y" ? "면세" : "과세"}
</span>
) : [
/* 전체 합계 — 모든 창고의 발주수량/재고수량 합 (상단 강조 행) */
<tr key="__total-order" className="bg-emerald-50/70 border-y-2 border-emerald-300 font-bold">
<td className="px-3 py-2 align-top sticky left-0 bg-emerald-50/70" rowSpan={2}>
<div className="text-emerald-800"> </div>
<div className="text-[10px] text-emerald-600 font-normal"> </div>
</td>
<td className="px-3 py-1.5 text-center text-[11px] text-rose-700 bg-rose-50/60 sticky left-[120px]"></td>
{items.map((it) => {
const v = Number(it.TOTAL_ORDER || 0);
return (
<td key={it.OBJID} className={`px-3 py-1.5 text-right ${v === 0 ? "text-slate-300" : "text-rose-700"}`}>
{v === 0 ? "-" : fmt(v)}
</td>
<td className="px-3 py-2 text-center text-[11px] text-slate-500">
{r.SALE_START_DATE || "상시"} ~ {r.SALE_END_DATE || "상시"}
);
})}
</tr>,
<tr key="__total-stock" className="bg-emerald-50/70 border-b-2 border-emerald-300 font-bold">
<td className="px-3 py-1.5 text-center text-[11px] text-emerald-700 bg-emerald-100/60 sticky left-[120px]"></td>
{items.map((it) => {
const v = Number(it.TOTAL_STOCK || 0);
const negative = v < 0;
return (
<td key={it.OBJID} className={`px-3 py-1.5 text-right ${negative ? "text-rose-600" : v === 0 ? "text-slate-300" : "text-emerald-700"}`}>
{v === 0 ? "-" : fmt(v)}
</td>
<td className={`px-3 py-2 text-right font-semibold ${orderQty === 0 ? "text-slate-300" : "text-rose-700"}`}>
{orderQty === 0 ? "-" : fmt(orderQty)}
</td>
<td className={`px-3 py-2 text-right font-semibold ${stockQty === 0 ? "text-slate-300" : "text-emerald-700"}`}>
{stockQty === 0 ? "-" : fmt(stockQty)}
</td>
<td className={`px-3 py-2 text-right font-bold ${
shortage ? "text-rose-600" : diff === 0 ? "text-slate-300" : "text-slate-700"
}`}>
{diff === 0 ? "-" : fmt(diff)}
</td>
</tr>
);
})
)}
);
})}
</tr>,
/* 창고별 — 각 창고 발주수량/재고수량 두 줄 */
...warehouses.flatMap((w) => [
<tr key={`${w.WH_CODE}-order`} 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-rose-700 bg-rose-50/40 sticky left-[120px]"></td>
{items.map((it) => {
const v = Number(it.ORDER[w.WH_CODE] ?? 0);
return (
<td key={it.OBJID} className={`px-3 py-1.5 text-right ${v === 0 ? "text-slate-300" : "text-rose-700 font-semibold"}`}>
{v === 0 ? "-" : fmt(v)}
</td>
);
})}
</tr>,
<tr key={`${w.WH_CODE}-stock`} 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.STOCK[w.WH_CODE] ?? 0);
const negative = v < 0;
return (
<td key={it.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>
@@ -200,12 +231,11 @@ export default function DailyOrderInventoryPage() {
);
}
function SummaryCard({ label, value, color }: { label: string; value: string; color: "slate" | "rose" | "emerald" | "amber" }) {
function SummaryCard({ label, value, color }: { label: string; value: string; color: "slate" | "rose" | "emerald" }) {
const cls = {
slate: "bg-slate-50 border-slate-200 text-slate-800",
rose: "bg-rose-50 border-rose-200 text-rose-800",
emerald: "bg-emerald-50 border-emerald-200 text-emerald-800",
amber: "bg-amber-50 border-amber-200 text-amber-800",
}[color];
return (
<div className={`rounded-xl border ${cls} p-4`}>
@@ -1,24 +1,10 @@
// 일자별 발주/재고 현황
// 일자별 발주/재고 현황 — 창고 × 품목 매트릭스
// 선택한 날짜에 판매 가능한 품목(sale_start_date ~ sale_end_date) 만 추려서
// 해당 일자에 발주된 수량 합계 + 전체 창고 재고 합계를 반환한다.
// 각 창고별 발주수량과 현재고를 반환한다.
import { NextRequest, NextResponse } from "next/server";
import { queryRows } from "@/lib/db";
import { requireMomoAdmin } from "@/lib/momo-guard";
interface Row {
OBJID: string;
ITEM_CODE: string;
ITEM_NAME: string;
UNIT: string;
UNIT_PRICE: string;
IS_TAX_FREE: string;
SALE_START_DATE: string | null;
SALE_END_DATE: string | null;
ORDER_QTY: string;
STOCK_QTY: string;
VENDOR_NAME: string | null;
}
export async function POST(req: NextRequest) {
const g = await requireMomoAdmin();
if (g instanceof NextResponse) return g;
@@ -27,58 +13,152 @@ export async function POST(req: NextRequest) {
const targetDate = (body.targetDate as string) || ""; // YYYY-MM-DD, 비우면 오늘
const keyword = (body.keyword as string) || "";
const params: unknown[] = [];
params.push(targetDate || new Date().toISOString().slice(0, 10));
const dateIdx = params.length; // $1
const date = targetDate || new Date().toISOString().slice(0, 10);
const conditions: string[] = [
// 1) 활성 창고
const warehouses = await queryRows<{ OBJID: string; WH_CODE: string; WH_NAME: string }>(
`SELECT objid AS "OBJID", wh_code AS "WH_CODE", wh_name AS "WH_NAME"
FROM momo_warehouses
WHERE COALESCE(is_del,'N') != 'Y'
ORDER BY wh_code`
);
// 2) 선택일에 판매가능한 품목
const itemConds: string[] = [
"COALESCE(I.is_del, 'N') != 'Y'",
"COALESCE(I.is_hidden, 'N') != 'Y'",
"UPPER(COALESCE(I.status, '')) = 'ACTIVE'",
// 분 단위 시간까지 들어 있어도 날짜 단위로 겹침 비교 (선택일이 판매기간 내에 포함되면 노출)
`(I.sale_start_date IS NULL OR $${dateIdx}::date >= I.sale_start_date::date)`,
`(I.sale_end_date IS NULL OR $${dateIdx}::date <= I.sale_end_date::date)`,
`(I.sale_start_date IS NULL OR $1::date >= I.sale_start_date::date)`,
`(I.sale_end_date IS NULL OR $1::date <= I.sale_end_date::date)`,
];
const params: unknown[] = [date];
if (keyword) {
params.push(keyword);
const kIdx = params.length;
conditions.push(`(I.item_name ILIKE '%' || $${kIdx} || '%' OR I.item_code ILIKE '%' || $${kIdx} || '%')`);
itemConds.push(`(I.item_name ILIKE '%' || $2 || '%' OR I.item_code ILIKE '%' || $2 || '%')`);
}
const sql = `
SELECT
I.objid AS "OBJID",
I.item_code AS "ITEM_CODE",
I.item_name AS "ITEM_NAME",
I.unit AS "UNIT",
I.unit_price AS "UNIT_PRICE",
I.is_tax_free AS "IS_TAX_FREE",
TO_CHAR(I.sale_start_date, 'YYYY-MM-DD HH24:MI') AS "SALE_START_DATE",
TO_CHAR(I.sale_end_date, 'YYYY-MM-DD HH24:MI') AS "SALE_END_DATE",
V.supply_name AS "VENDOR_NAME",
COALESCE((
SELECT SUM(OI.qty)
FROM momo_order_items OI
JOIN momo_orders O ON O.objid = OI.order_objid
WHERE OI.item_objid = I.objid
AND COALESCE(OI.kind, 'ITEM') = 'ITEM'
AND COALESCE(O.is_del, 'N') != 'Y'
AND O.status IN ('REQUESTED','APPROVED','INVOICED','PAID')
AND O.order_date = $${dateIdx}::date
), 0) AS "ORDER_QTY",
COALESCE((
SELECT SUM(S.qty)
FROM momo_stocks S
JOIN momo_warehouses W ON W.objid = S.wh_objid
WHERE S.item_objid = I.objid
AND COALESCE(W.is_del, 'N') != 'Y'
), 0) AS "STOCK_QTY"
FROM momo_items I
LEFT JOIN supply_mng V ON I.vendor_objid = V.objid::text
WHERE ${conditions.join(" AND ")}
ORDER BY I.item_name ASC
`;
const items = await queryRows<{
OBJID: string; ITEM_CODE: string; ITEM_NAME: string;
UNIT: string; UNIT_PRICE: string; IS_TAX_FREE: string;
SALE_START_DATE: string | null; SALE_END_DATE: string | null;
VENDOR_NAME: string | null;
}>(
`SELECT
I.objid AS "OBJID",
I.item_code AS "ITEM_CODE",
I.item_name AS "ITEM_NAME",
I.unit AS "UNIT",
I.unit_price AS "UNIT_PRICE",
I.is_tax_free AS "IS_TAX_FREE",
TO_CHAR(I.sale_start_date, 'YYYY-MM-DD HH24:MI') AS "SALE_START_DATE",
TO_CHAR(I.sale_end_date, 'YYYY-MM-DD HH24:MI') AS "SALE_END_DATE",
V.supply_name AS "VENDOR_NAME"
FROM momo_items I
LEFT JOIN supply_mng V ON I.vendor_objid = V.objid::text
WHERE ${itemConds.join(" AND ")}
ORDER BY I.item_name ASC`,
params
);
const rows = await queryRows<Row>(sql, params);
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
if (items.length === 0) {
return NextResponse.json({ WAREHOUSES: warehouses, ITEMS: [], TOTAL_CNT: 0 });
}
// 3) 창고별 재고 (해당 일자 시점이 아닌 현재 보유 재고. 일자별 시계열 재고는 별도)
const stockRows = await queryRows<{ ITEM_OBJID: string; WH_CODE: string; STOCK_QTY: string }>(
`SELECT S.item_objid::text AS "ITEM_OBJID",
W.wh_code AS "WH_CODE",
S.qty AS "STOCK_QTY"
FROM momo_stocks S
JOIN momo_warehouses W ON W.objid = S.wh_objid
WHERE COALESCE(W.is_del,'N') != 'Y'
AND S.item_objid IN (${items.map((_, i) => `$${i + 1}`).join(",")})`,
items.map((it) => it.OBJID)
);
// 4) 창고별 발주수량 — 출고된 창고 기준 (momo_stock_moves OUT 이력으로 매핑)
// APPROVED 이후만 stock_moves 가 기록되므로, 그 이전(REQUESTED) 발주는
// 거래처(user_info)의 default_wh_objid 로 가상 배정 (출고 예정 창고)
const moveRows = await queryRows<{ ITEM_OBJID: string; WH_CODE: string; ORDER_QTY: string }>(
`SELECT SM.item_objid::text AS "ITEM_OBJID",
W.wh_code AS "WH_CODE",
SUM(-SM.qty)::text AS "ORDER_QTY"
FROM momo_stock_moves SM
JOIN momo_warehouses W ON W.objid = SM.wh_objid
JOIN momo_orders O ON O.objid::text = SM.ref_objid::text
WHERE SM.ref_type = 'ORDER'
AND SM.move_type = 'OUT'
AND COALESCE(W.is_del,'N') != 'Y'
AND O.order_date = $1::date
AND O.status IN ('APPROVED','INVOICED','PAID')
AND COALESCE(O.is_del,'N') != 'Y'
AND SM.item_objid IN (${items.map((_, i) => `$${i + 2}`).join(",")})
GROUP BY SM.item_objid, W.wh_code`,
[date, ...items.map((it) => it.OBJID)]
);
// REQUESTED 상태(아직 출고 전) 발주는 거래처 default 창고로 배정 (fallback: WH001)
const pendingRows = await queryRows<{ ITEM_OBJID: string; WH_CODE: string; ORDER_QTY: string }>(
`SELECT OI.item_objid::text AS "ITEM_OBJID",
COALESCE(W.wh_code, 'WH001') AS "WH_CODE",
SUM(OI.qty)::text AS "ORDER_QTY"
FROM momo_order_items OI
JOIN momo_orders O ON O.objid = OI.order_objid
LEFT JOIN user_info U ON U.user_id = O.customer_objid
LEFT JOIN momo_warehouses W ON W.objid = U.default_wh_objid
WHERE COALESCE(OI.kind, 'ITEM') = 'ITEM'
AND COALESCE(O.is_del, 'N') != 'Y'
AND O.status = 'REQUESTED'
AND O.order_date = $1::date
AND OI.item_objid IN (${items.map((_, i) => `$${i + 2}`).join(",")})
GROUP BY OI.item_objid, W.wh_code`,
[date, ...items.map((it) => it.OBJID)]
);
// 매트릭스 조립 — 각 품목별 STOCK / ORDER 객체 (wh_code → qty)
type ItemMatrix = {
OBJID: string; ITEM_CODE: string; ITEM_NAME: string;
UNIT: string; UNIT_PRICE: string; IS_TAX_FREE: string;
SALE_START_DATE: string | null; SALE_END_DATE: string | null;
VENDOR_NAME: string | null;
STOCK: Record<string, number>; // wh_code → 현재고
ORDER: Record<string, number>; // wh_code → 발주수량
TOTAL_STOCK: number;
TOTAL_ORDER: number;
};
const byItem = new Map<string, ItemMatrix>();
for (const it of items) {
byItem.set(it.OBJID, {
OBJID: it.OBJID,
ITEM_CODE: it.ITEM_CODE,
ITEM_NAME: it.ITEM_NAME,
UNIT: it.UNIT,
UNIT_PRICE: it.UNIT_PRICE,
IS_TAX_FREE: it.IS_TAX_FREE,
SALE_START_DATE: it.SALE_START_DATE,
SALE_END_DATE: it.SALE_END_DATE,
VENDOR_NAME: it.VENDOR_NAME,
STOCK: {}, ORDER: {}, TOTAL_STOCK: 0, TOTAL_ORDER: 0,
});
}
for (const s of stockRows) {
const m = byItem.get(s.ITEM_OBJID);
if (!m) continue;
const q = Number(s.STOCK_QTY);
m.STOCK[s.WH_CODE] = (m.STOCK[s.WH_CODE] || 0) + q;
m.TOTAL_STOCK += q;
}
for (const o of [...moveRows, ...pendingRows]) {
const m = byItem.get(o.ITEM_OBJID);
if (!m) continue;
const q = Number(o.ORDER_QTY);
m.ORDER[o.WH_CODE] = (m.ORDER[o.WH_CODE] || 0) + q;
m.TOTAL_ORDER += q;
}
return NextResponse.json({
WAREHOUSES: warehouses,
ITEMS: Array.from(byItem.values()),
TOTAL_CNT: byItem.size,
});
}