이전: 품목 한 줄에 [발주수량 합계 + 전체창고 재고합계] 표시.
변경: 창고별 재고 현황의 "품목 가로" 패턴 차용 — 헤더=품목(가로), 좌측=창고(세로).
각 셀에 그 창고의 [발주수량 / 재고수량] 두 줄.
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:
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user