From af6726f2b6cb0be573b5cba47b370cc770796c03 Mon Sep 17 00:00:00 2001 From: chpark Date: Thu, 21 May 2026 14:13:10 +0900 Subject: [PATCH] =?UTF-8?q?feat(orders/approve):=20=EC=B6=9C=EA=B3=A0=20?= =?UTF-8?q?=EC=8B=9C=20=EC=9E=AC=EA=B3=A0=20=EB=B6=80=EC=A1=B1=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20=EC=A0=9C=EA=B1=B0=20=E2=80=94=20=EC=9D=8C=EC=88=98?= =?UTF-8?q?=20=EC=9E=AC=EA=B3=A0=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 요구: 거래처 default 창고에 재고가 모자라도 그대로 차감해서 출고 진행. 일자별 발주/재고 + 창고별 재고 현황에서 음수(-) 로 표시되면 관리자가 다른 창고에서 부족 창고로 수동 재고 이동 처리하는 운영 정책. 변경: - "재고 부족: 현재고 N, 요청 M" 차단 + ROLLBACK 제거 → 그대로 차감 - 재고 row 자체가 없던 품목은 새 row(qty=-N) INSERT - itemsRes SQL 에 kind='ITEM' AND item_objid IS NOT NULL 가드 추가 (택배/용차/환불 라인이 잘못 차감되는 잠재 버그도 같이 차단) - stock_moves OUT 이력은 동일하게 음수 qty 로 기록 음수 재고 발생 시 운영 흐름: 1) 다른 창고에 같은 품목 재고 확인 2) 관리자 패널 → 재고 이동 (오프라인 물리 이동 + 시스템 등록) 3) 부족 창고 재고가 0 이상으로 복구 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/m/orders/approve/route.ts | 39 +++++++++++++++------------ 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/app/api/m/orders/approve/route.ts b/src/app/api/m/orders/approve/route.ts index eef2ece..417a066 100644 --- a/src/app/api/m/orders/approve/route.ts +++ b/src/app/api/m/orders/approve/route.ts @@ -33,8 +33,11 @@ export async function POST(req: NextRequest) { return NextResponse.json({ success: false, message: `현재 상태(${order.status})에서는 승인할 수 없습니다.` }, { status: 400 }); } + // ITEM 종류만 — 택배/용차/환불 라인은 재고 차감 대상이 아님 const itemsRes = await client.query( - `SELECT objid, item_objid, qty FROM momo_order_items WHERE order_objid = $1 ORDER BY seq`, + `SELECT objid, item_objid, qty FROM momo_order_items + WHERE order_objid = $1 AND COALESCE(kind,'ITEM') = 'ITEM' AND item_objid IS NOT NULL + ORDER BY seq`, [objid] ); @@ -65,29 +68,31 @@ export async function POST(req: NextRequest) { whObjid = whRes.rows[0].objid; } + // 부족해도 차감 — 음수 재고 허용. 일자별 발주/재고 + 창고별 재고 현황에 음수로 + // 표시되면 관리자가 다른 창고에서 부족 창고로 수동 이동 처리. for (const ln of itemsRes.rows) { + // 택배/용차 라인은 재고 차감 대상이 아님 — item_objid 없거나 kind 가 ITEM 이 아닌 row 는 skip. + // (approve 시 itemsRes 가 kind 필터링 없이 가져오므로 여기서 한 번 더 가드) + if (!ln.item_objid) continue; + const stk = await client.query( `SELECT qty FROM momo_stocks WHERE wh_objid = $1 AND item_objid = $2 FOR UPDATE`, [whObjid, ln.item_objid] ); - const currentQty = stk.rowCount === 0 ? 0 : Number(stk.rows[0].qty); - if (currentQty < Number(ln.qty)) { - await client.query("ROLLBACK"); - return NextResponse.json( - { success: false, message: `재고 부족: 품목 ${ln.item_objid} (현재고 ${currentQty}, 요청 ${ln.qty})` }, - { status: 400 } + if (stk.rowCount === 0) { + // 재고 row 자체가 없는 품목 — 0 에서 -N 으로 새 row 생성 + await client.query( + `INSERT INTO momo_stocks (objid, wh_objid, item_objid, qty, regdate) + VALUES ($1, $2, $3, $4, NOW())`, + [createObjectId(), whObjid, ln.item_objid, -Number(ln.qty)] + ); + } else { + await client.query( + `UPDATE momo_stocks SET qty = qty - $1, update_date = NOW() + WHERE wh_objid = $2 AND item_objid = $3`, + [ln.qty, whObjid, ln.item_objid] ); } - if (stk.rowCount === 0) { - // 안전장치: 재고 row가 없으면 만들지 않고 에러 처리 - await client.query("ROLLBACK"); - return NextResponse.json({ success: false, message: "재고 정보가 없습니다." }, { status: 400 }); - } - await client.query( - `UPDATE momo_stocks SET qty = qty - $1, update_date = NOW() - WHERE wh_objid = $2 AND item_objid = $3`, - [ln.qty, whObjid, ln.item_objid] - ); await client.query( `INSERT INTO momo_stock_moves (objid, wh_objid, item_objid, move_type, qty, ref_type, ref_objid, regdate, regid) VALUES ($1, $2, $3, 'OUT', $4, 'ORDER', $5, NOW(), $6)`,