feat(daily-order-inventory): 단일 일자 → 기간(시작~종료) 조회 지원

- 페이지: date 두 개(시작/종료) + '오늘'·'최근 7일' 빠른 버튼
- API: targetDate 대신 startDate/endDate 사용 (단일 호환 유지),
       판매가능 품목은 기간 겹침 조건, 발주수량은 order_date BETWEEN으로 합산

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chpark
2026-05-31 22:05:23 +09:00
parent b7bc6b2bbf
commit 83ac3a5456
2 changed files with 50 additions and 23 deletions
@@ -25,7 +25,8 @@ const fmt = (n: number) => Number(n || 0).toLocaleString("ko-KR");
const today = () => new Date().toISOString().slice(0, 10);
export default function DailyOrderInventoryPage() {
const [targetDate, setTargetDate] = useState<string>(today());
const [startDate, setStartDate] = useState<string>(today());
const [endDate, setEndDate] = useState<string>(today());
const [keyword, setKeyword] = useState("");
const [warehouses, setWarehouses] = useState<Wh[]>([]);
const [items, setItems] = useState<ItemRow[]>([]);
@@ -37,7 +38,7 @@ export default function DailyOrderInventoryPage() {
const res = await fetch("/api/m/admin/daily-order-inventory", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ targetDate, keyword }),
body: JSON.stringify({ startDate, endDate, keyword }),
});
if (res.ok) {
const j = await res.json();
@@ -50,7 +51,7 @@ export default function DailyOrderInventoryPage() {
} finally {
setLoading(false);
}
}, [targetDate, keyword]);
}, [startDate, endDate, keyword]);
useEffect(() => { load(); }, [load]);
@@ -75,7 +76,8 @@ export default function DailyOrderInventoryPage() {
{ header: "분류", key: "KIND" },
...items.map((it) => ({ header: it.ITEM_NAME, key: it.ITEM_NAME })),
];
downloadXlsx(`일자별발주재고_${targetDate}`, data, cols);
const range = startDate === endDate ? startDate : `${startDate}_${endDate}`;
downloadXlsx(`일자별발주재고_${range}`, data, cols);
};
return (
@@ -87,23 +89,44 @@ export default function DailyOrderInventoryPage() {
/
</h1>
<p className="text-xs text-slate-500 mt-0.5">
<b> </b> , .
= (REQUESTED는 default / APPROVED ). = .
<b> </b> , .
= (REQUESTED는 default / APPROVED ). = ( ).
</p>
</div>
<div className="flex items-center gap-2 flex-wrap">
<input
type="date"
value={targetDate}
onChange={(e) => setTargetDate(e.target.value)}
value={startDate}
max={endDate || undefined}
onChange={(e) => setStartDate(e.target.value)}
className="h-9 px-3 rounded border border-slate-300 text-sm"
/>
<span className="text-slate-400 text-sm">~</span>
<input
type="date"
value={endDate}
min={startDate || undefined}
onChange={(e) => setEndDate(e.target.value)}
className="h-9 px-3 rounded border border-slate-300 text-sm"
/>
<button
onClick={() => setTargetDate(today())}
onClick={() => { const t = today(); setStartDate(t); setEndDate(t); }}
className="h-9 px-3 rounded border border-slate-300 bg-white text-slate-700 text-xs font-semibold"
>
</button>
<button
onClick={() => {
const end = new Date();
const start = new Date();
start.setDate(end.getDate() - 6);
setStartDate(start.toISOString().slice(0, 10));
setEndDate(end.toISOString().slice(0, 10));
}}
className="h-9 px-3 rounded border border-slate-300 bg-white text-slate-700 text-xs font-semibold"
>
7
</button>
<div className="relative">
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-400" />
<input
@@ -10,11 +10,14 @@ export async function POST(req: NextRequest) {
if (g instanceof NextResponse) return g;
const body = await req.json().catch(() => ({}));
const targetDate = (body.targetDate as string) || ""; // YYYY-MM-DD, 비우면 오늘
// 신/구 호환: startDate/endDate 우선, 없으면 targetDate(단일)로 같은 날짜 사용
const today = new Date().toISOString().slice(0, 10);
const targetDate = (body.targetDate as string) || "";
let startDate = (body.startDate as string) || targetDate || today;
let endDate = (body.endDate as string) || targetDate || today;
if (startDate > endDate) [startDate, endDate] = [endDate, startDate];
const keyword = (body.keyword as string) || "";
const date = targetDate || new Date().toISOString().slice(0, 10);
// 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"
@@ -23,18 +26,19 @@ export async function POST(req: NextRequest) {
ORDER BY wh_code`
);
// 2) 선택일에 판매가능 품목
// 2) 기간 내에 한 번이라도 판매가능했던 품목
// 품목 판매기간 [sale_start_date, sale_end_date] 과 조회 기간 [startDate, endDate] 이 겹치면 포함
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 $1::date >= I.sale_start_date::date)`,
`(I.sale_end_date IS NULL OR $1::date <= I.sale_end_date::date)`,
`(I.sale_start_date IS NULL OR I.sale_start_date::date <= $2::date)`,
`(I.sale_end_date IS NULL OR I.sale_end_date::date >= $1::date)`,
];
const params: unknown[] = [date];
const params: unknown[] = [startDate, endDate];
if (keyword) {
params.push(keyword);
itemConds.push(`(I.item_name ILIKE '%' || $2 || '%' OR I.item_code ILIKE '%' || $2 || '%')`);
itemConds.push(`(I.item_name ILIKE '%' || $3 || '%' OR I.item_code ILIKE '%' || $3 || '%')`);
}
const items = await queryRows<{
@@ -89,12 +93,12 @@ export async function POST(req: NextRequest) {
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.order_date BETWEEN $1::date AND $2::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(",")})
AND SM.item_objid IN (${items.map((_, i) => `$${i + 3}`).join(",")})
GROUP BY SM.item_objid, W.wh_code`,
[date, ...items.map((it) => it.OBJID)]
[startDate, endDate, ...items.map((it) => it.OBJID)]
);
// REQUESTED 상태(아직 출고 전) 발주는 거래처 default 창고로 배정 (fallback: WH001)
@@ -109,10 +113,10 @@ export async function POST(req: NextRequest) {
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(",")})
AND O.order_date BETWEEN $1::date AND $2::date
AND OI.item_objid IN (${items.map((_, i) => `$${i + 3}`).join(",")})
GROUP BY OI.item_objid, W.wh_code`,
[date, ...items.map((it) => it.OBJID)]
[startDate, endDate, ...items.map((it) => it.OBJID)]
);
// 매트릭스 조립 — 각 품목별 STOCK / ORDER 객체 (wh_code → qty)