From 0aa8ce90259b3b8eee390fabaf0347d0b809c34c Mon Sep 17 00:00:00 2001 From: chpark Date: Wed, 20 May 2026 22:08:07 +0900 Subject: [PATCH] =?UTF-8?q?fix(orders/new):=20=EC=B2=AB=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=EC=97=90=EB=8F=84=20=EC=9E=AC=EA=B3=A0=20=EC=B4=88?= =?UTF-8?q?=EA=B3=BC=20=EA=B2=BD=EA=B3=A0=20=EC=A6=89=EC=8B=9C=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=E2=80=94=20setCart=20=EC=BD=9C=EB=B0=B1=20?= =?UTF-8?q?=EC=95=88=20warned=20=EB=B3=80=EC=88=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 원인: - addManyToCart 가 setCart 함수형 업데이트 안에서 외부 변수 warned 에 값 세팅 - React 18 batched updates 로 콜백 실행이 한 박자 늦어 if(warned) 가 false → 첫 클릭 경고 누락 - 두 번째 클릭 때 이미 콜백이 실행돼 warned 가 true 보여 경고 표시 — 사용자가 본 현상 수정: - 함수형 업데이터 진입 전에 cart 를 동기적으로 읽어 newQty/limit 비교 - 초과면 Swal 띄우고 return — setCart 호출 자체를 안 함 (장바구니 변경 없음) - 통과 시에만 setCart 로 카트 갱신 - updateQty, setQty 도 동일 패턴(stale-closure 차단도 함수형 업데이터 밖에서) Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/(main)/m/orders/new/page.tsx | 103 ++++++++++++--------------- 1 file changed, 44 insertions(+), 59 deletions(-) diff --git a/src/app/(main)/m/orders/new/page.tsx b/src/app/(main)/m/orders/new/page.tsx index c8bf256..019a294 100644 --- a/src/app/(main)/m/orders/new/page.tsx +++ b/src/app/(main)/m/orders/new/page.tsx @@ -126,22 +126,13 @@ function ItemsBrowse() { const isDelivery = item.REQUIRES_DELIVERY === "Y"; const effStock = isDelivery ? Number.MAX_SAFE_INTEGER : stock; const limit = unlimitedQty || maxQ <= 0 ? effStock : Math.min(effStock, maxQ); - let toastTitle = ""; - let warned = false; - setCart((c) => { - const found = c.find((x) => x.item.OBJID === item.OBJID); - const newQty = (found?.qty ?? 0) + qty; - if (newQty > limit) { - warned = true; - return c; - } - toastTitle = found - ? `수량 +${qty} → ${newQty}개` - : `장바구니에 추가됨: ${item.ITEM_NAME} (${qty}개)`; - if (found) return c.map((x) => x.item.OBJID === item.OBJID ? { ...x, qty: newQty } : x); - return [...c, { item, qty }]; - }); - if (warned) { + + // setCart 함수형 업데이트 안에서 외부 변수에 warned 세팅하면 비동기 타이밍 때문에 + // 첫 클릭에는 if(warned) 체크가 한 박자 늦게 동작. 동기 체크로 변경. + const found = cart.find((x) => x.item.OBJID === item.OBJID); + const newQty = (found?.qty ?? 0) + qty; + + if (newQty > limit) { const isStockLimit = maxQ <= 0 || stock <= maxQ; Swal.fire({ icon: "warning", @@ -152,60 +143,54 @@ function ItemsBrowse() { confirmButtonColor: "#0f766e", confirmButtonText: "확인", }); - return; + return; // 차단 — 장바구니 변경 없음 } + + setCart((c) => { + const f = c.find((x) => x.item.OBJID === item.OBJID); + if (f) return c.map((x) => x.item.OBJID === item.OBJID ? { ...x, qty: newQty } : x); + return [...c, { item, qty }]; + }); Swal.fire({ toast: true, position: "top-end", icon: "success", - title: toastTitle, + title: found ? `수량 +${qty} → ${newQty}개` : `장바구니에 추가됨: ${item.ITEM_NAME} (${qty}개)`, showConfirmButton: false, timer: 1000, timerProgressBar: true, }); }; const updateQty = (objid: string, delta: number) => { - let warnLimit = -1; - let warnIsStock = false; - setCart((c) => - c.map((x) => { - if (x.item.OBJID !== objid) return x; - const newQty = x.qty + delta; - if (newQty <= 0) return x; - const stock = Number(x.item.STOCK_QTY); - const maxQ = Number(x.item.MAX_ORDER_QTY ?? 0); - const isDelivery = x.item.REQUIRES_DELIVERY === "Y"; - const effStock = isDelivery ? Number.MAX_SAFE_INTEGER : stock; - const limit = unlimitedQty || maxQ <= 0 ? effStock : Math.min(effStock, maxQ); - if (newQty > limit) { - warnLimit = limit; - warnIsStock = maxQ <= 0 || stock <= maxQ; - return x; - } - return { ...x, qty: newQty }; - }) - ); - if (warnLimit >= 0) toastLimit(warnLimit, warnIsStock); + const target = cart.find((x) => x.item.OBJID === objid); + if (!target) return; + const newQty = target.qty + delta; + if (newQty <= 0) return; + const stock = Number(target.item.STOCK_QTY); + const maxQ = Number(target.item.MAX_ORDER_QTY ?? 0); + const isDelivery = target.item.REQUIRES_DELIVERY === "Y"; + const effStock = isDelivery ? Number.MAX_SAFE_INTEGER : stock; + const limit = unlimitedQty || maxQ <= 0 ? effStock : Math.min(effStock, maxQ); + if (newQty > limit) { + toastLimit(limit, maxQ <= 0 || stock <= maxQ); + return; + } + setCart((c) => c.map((x) => x.item.OBJID === objid ? { ...x, qty: newQty } : x)); }; const setQty = (objid: string, value: number) => { - let warnLimit = -1; - let warnIsStock = false; - setCart((c) => - c.map((x) => { - if (x.item.OBJID !== objid) return x; - const stock = Number(x.item.STOCK_QTY); - const maxQ = Number(x.item.MAX_ORDER_QTY ?? 0); - const isDelivery = x.item.REQUIRES_DELIVERY === "Y"; - const effStock = isDelivery ? Number.MAX_SAFE_INTEGER : stock; - const limit = unlimitedQty || maxQ <= 0 ? effStock : Math.min(effStock, maxQ); - const requested = Math.floor(value || 0); - if (requested > limit) { - warnLimit = limit; - warnIsStock = maxQ <= 0 || stock <= maxQ; - } - const clamped = Math.max(1, Math.min(limit, requested)); - return { ...x, qty: clamped }; - }) - ); - if (warnLimit >= 0) toastLimit(warnLimit, warnIsStock); + const target = cart.find((x) => x.item.OBJID === objid); + if (!target) return; + const stock = Number(target.item.STOCK_QTY); + const maxQ = Number(target.item.MAX_ORDER_QTY ?? 0); + const isDelivery = target.item.REQUIRES_DELIVERY === "Y"; + const effStock = isDelivery ? Number.MAX_SAFE_INTEGER : stock; + const limit = unlimitedQty || maxQ <= 0 ? effStock : Math.min(effStock, maxQ); + const requested = Math.floor(value || 0); + if (requested > limit) { + toastLimit(limit, maxQ <= 0 || stock <= maxQ); + // 차단 — 기존 수량 유지 + return; + } + const clamped = Math.max(1, requested); + setCart((c) => c.map((x) => x.item.OBJID === objid ? { ...x, qty: clamped } : x)); }; const toastLimit = (limit: number, isStockLimit: boolean) => {