From 34ee37479642e0e928eb234855e5241adbec7092 Mon Sep 17 00:00:00 2001 From: chpark Date: Thu, 14 May 2026 01:01:06 +0900 Subject: [PATCH] =?UTF-8?q?feat(orders):=20ITEM=20=EC=88=98=EB=9F=89/?= =?UTF-8?q?=EB=B9=84=EA=B3=A0=20=EC=9D=B8=EB=9D=BC=EC=9D=B8=20=EC=A6=89?= =?UTF-8?q?=EC=8B=9C=EC=A0=80=EC=9E=A5=20+=20=EC=99=BC=EC=AA=BD=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1) ITEM 라인 수량을 QtyInput 인라인 인풋으로 (REQUESTED/APPROVED 상태). onBlur/Enter 시 /api/m/orders/items/update 호출 → 자동 저장. 2) 비고(REMARK) 는 이미 onBlur 자동 저장이었음. 출고요청/출고완료 모두 editable 이라 동작. 3) ExtraRow(택배/용차) 의 V(저장) 버튼 제거. onBlur 시 자동 저장으로 변경 — label/단가/수량 어느 인풋이든 포커스 떠나면 자동 commit. 4) 모든 라인 수정 작업 (saveRemark/saveItemQty/upsertExtra/deleteExtra) 에서 onReload + onReloadList 동시 호출 → 왼쪽 발주 리스트의 합계도 즉시 반영. 부수: 운영 DB 의 모든 user_info 비밀번호를 '1' 로 reset (사용자 요청). --- src/app/(main)/m/admin/orders/page.tsx | 81 ++++++++++++++++++-------- 1 file changed, 58 insertions(+), 23 deletions(-) diff --git a/src/app/(main)/m/admin/orders/page.tsx b/src/app/(main)/m/admin/orders/page.tsx index c36cfee..a7a7f1b 100644 --- a/src/app/(main)/m/admin/orders/page.tsx +++ b/src/app/(main)/m/admin/orders/page.tsx @@ -456,24 +456,35 @@ function StatementPreview({ }); }; - // 비고 저장 + // 비고 저장 — 명세표 + 왼쪽 리스트 모두 갱신 const saveRemark = async (lineObjid: string, remark: string) => { const res = await fetch("/api/m/orders/items/remark", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ lineObjid, remark }), }); const j = await res.json(); - if (j.success) onReload(); + if (j.success) { onReload(); onReloadList(); } else Swal.fire({ icon: "error", title: "비고 저장 실패", text: j.message }); }; + // ITEM 라인 수량 즉시 저장 — items/update API + const saveItemQty = async (lineObjid: string, qty: number) => { + const res = await fetch("/api/m/orders/items/update", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ orderObjid: order.OBJID, lines: [{ objid: lineObjid, qty }] }), + }); + const j = await res.json(); + if (j.success) { onReload(); onReloadList(); } + else Swal.fire({ icon: "error", title: "수량 저장 실패", text: j.message }); + }; + const upsertExtra = async (line: { objid?: string; kind: "DELIVERY" | "CHARTER"; label: string; unitPrice: number; qty: number }) => { const res = await fetch("/api/m/orders/lines/save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ orderObjid: order.OBJID, lines: [line] }), }); const j = await res.json(); - if (j.success) onReload(); + if (j.success) { onReload(); onReloadList(); } else Swal.fire({ icon: "error", title: "저장 실패", text: j.message }); }; const deleteExtra = async (objid: string) => { @@ -484,7 +495,7 @@ function StatementPreview({ body: JSON.stringify({ orderObjid: order.OBJID, lines: [{ objid, kind: "CHARTER", unitPrice: 0, qty: 1, delete: true }] }), }); const j = await res.json(); - if (j.success) onReload(); + if (j.success) { onReload(); onReloadList(); } else Swal.fire({ icon: "error", title: "삭제 실패", text: j.message }); }; const addNewExtra = (kind: "DELIVERY" | "CHARTER") => { @@ -671,7 +682,11 @@ function StatementPreview({ {isExtra ? "-" : fmt(it.STOCK_QTY)} - {fmt(it.QTY)} + + {editable + ? saveItemQty(it.OBJID, q)} /> + : fmt(it.QTY)} + {fmt(it.UNIT_PRICE)} {fmt(it.SUPPLY_AMOUNT)} {it.IS_TAX_FREE === "Y" ? "-" : fmt(it.VAT_AMOUNT)} @@ -752,11 +767,16 @@ function ExtraRow({ line, displaySeq, editable, onSave, onDelete, onSaveRemark } const total = Math.round(unitPrice * qty); const supply = Math.round(total / 1.1); const vat = total - supply; - const dirty = label !== (line.EXTRA_LABEL || line.ITEM_NAME) - || unitPrice !== Number(line.UNIT_PRICE) - || qty !== Number(line.QTY); const isDelivery = line.KIND === "DELIVERY"; + // onBlur 시 자동 저장 (값이 바뀐 경우만). V 버튼 제거. + const commit = () => { + const dirty = label !== (line.EXTRA_LABEL || line.ITEM_NAME) + || unitPrice !== Number(line.UNIT_PRICE) + || qty !== Number(line.QTY); + if (dirty && qty > 0 && unitPrice >= 0) onSave({ label, unitPrice, qty }); + }; + return ( {displaySeq} @@ -767,6 +787,8 @@ function ExtraRow({ line, displaySeq, editable, onSave, onDelete, onSaveRemark } setLabel(e.target.value)} + onBlur={commit} + onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); }} className="h-6 px-1.5 border border-slate-200 rounded text-[11px] bg-white w-[calc(100%-50px)] inline" /> @@ -775,11 +797,15 @@ function ExtraRow({ line, displaySeq, editable, onSave, onDelete, onSaveRemark } setQty(Number(e.target.value))} + onBlur={commit} + onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); }} className="w-full h-6 px-1 border border-slate-200 rounded text-[11px] text-right tabular-nums bg-white" /> setUnitPrice(Number(e.target.value))} + onBlur={commit} + onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); }} className="w-full h-6 px-1 border border-slate-200 rounded text-[11px] text-right tabular-nums bg-white" /> {Number(supply).toLocaleString("ko-KR")} @@ -791,21 +817,9 @@ function ExtraRow({ line, displaySeq, editable, onSave, onDelete, onSaveRemark } : {line.REMARK || ""}} -
- {dirty && ( - - )} - -
+ ); @@ -900,3 +914,24 @@ function RemarkInput({ initial, onSave }: { initial: string; onSave: (r: string) ); } + +// ITEM 라인 수량 인라인 인풋 — onBlur / Enter 시 자동 저장 +function QtyInput({ initial, onSave }: { initial: number; onSave: (q: number) => void }) { + const [val, setVal] = useState(String(initial)); + useEffect(() => { setVal(String(initial)); }, [initial]); + const commit = () => { + const n = Number(val); + if (!Number.isFinite(n) || n <= 0) { setVal(String(initial)); return; } + if (n === initial) return; + onSave(n); + }; + return ( + setVal(e.target.value)} + onBlur={commit} + onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); }} + className="w-16 h-6 px-1 border border-slate-200 rounded text-[11px] text-right tabular-nums bg-white" + /> + ); +}