From a5fd64da62c2982e287cbf638deb5f7015a15357 Mon Sep 17 00:00:00 2001 From: chpark Date: Fri, 8 May 2026 10:02:14 +0900 Subject: [PATCH] =?UTF-8?q?feat(=EC=B6=9C=EA=B3=A0=EC=9A=94=EC=B2=AD):=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=95=88=EC=97=90=EC=84=9C=20=EC=88=98?= =?UTF-8?q?=EB=9F=89=20=EC=9E=85=EB=A0=A5/=EC=A1=B0=EC=A0=88=20+=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C/=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=ED=86=A0?= =?UTF-8?q?=EA=B8=80=20+=20=EA=B1=B0=EB=9E=98=EB=AA=85=EC=84=B8=ED=91=9C?= =?UTF-8?q?=20=EB=9D=BC=EC=9D=B8=20sync=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [출고요청 화면 개선 — /m/orders/new] - 카드에 [수량 입력 + 담기] 한 번에. 엔터 또는 버튼 클릭 시 그 수량만큼 카트에 추가 - 이미 담은 품목은 카드 안에 [- 1 +] 컨트롤 + [×] 빼기 버튼이 즉시 노출 · 카트 수량 그 자리에서 직접 수정. 카드 외 카트 펼치기 불필요 - 담은 품목 카드는 emerald 테두리 + 우상단에 "담은 N" 배지로 강조 [보기 모드 토글] - 검색바 우측에 [카드 / 리스트] 토글 - 카드: 기존 그리드 (이미지 위주, 시각적) - 리스트: 표 형태 (품목 많을 때 한눈에) — 행마다 동일 [수량+담기] 컨트롤 [관리자 거래명세표 라인 sync 버그 fix] - /m/admin/orders 에서 [+택배/+용차] 클릭 시 합계만 올라가고 인풋 표시값이 안 바뀌던 문제 - ExtraRow key 를 `OBJID-QTY-UNIT_PRICE-LABEL` 로 변경해 line 변경 시 컴포넌트 강제 재마운트 - useState 초기값이 새 line 값으로 확실히 반영됨 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/(main)/m/admin/orders/page.tsx | 2 +- src/app/(main)/m/orders/new/page.tsx | 318 ++++++++++++++++++++----- 2 files changed, 262 insertions(+), 58 deletions(-) diff --git a/src/app/(main)/m/admin/orders/page.tsx b/src/app/(main)/m/admin/orders/page.tsx index 1de9a36..6ea0683 100644 --- a/src/app/(main)/m/admin/orders/page.tsx +++ b/src/app/(main)/m/admin/orders/page.tsx @@ -547,7 +547,7 @@ function StatementPreview({ if (isExtra && editable) { return ( ([]); const [extras, setExtras] = useState([]); const [cartOpen, setCartOpen] = useState(false); + const [viewMode, setViewMode] = useState<"card" | "list">("card"); const fetchItems = useCallback(async () => { setLoading(true); @@ -70,29 +71,39 @@ export default function ItemsBrowse() { } }, [cartNeedsDelivery, hasDeliveryLine]); - const addToCart = (item: Item) => { + const addToCart = (item: Item) => addManyToCart(item, 1); + + const addManyToCart = (item: Item, qty: number) => { + const stock = Number(item.STOCK_QTY); + const maxQ = Number(item.MAX_ORDER_QTY ?? 0); + const limit = maxQ > 0 ? Math.min(stock, maxQ) : stock; + let toastTitle = ""; + let warned = false; setCart((c) => { const found = c.find((x) => x.item.OBJID === item.OBJID); - const stock = Number(item.STOCK_QTY); - const maxQ = Number(item.MAX_ORDER_QTY ?? 0); - const limit = maxQ > 0 ? Math.min(stock, maxQ) : stock; - if (found) { - if (found.qty + 1 > limit) { - Swal.fire({ - icon: "warning", - title: maxQ > 0 && stock > maxQ ? "1회 발주 한도 초과" : "재고 부족", - text: `현재 ${maxQ > 0 && stock > maxQ ? `1회 한도는 ${fmt(maxQ)}개` : `재고는 ${fmt(stock)}개`}입니다.`, - }); - return c; - } - return c.map((x) => x.item.OBJID === item.OBJID ? { ...x, qty: x.qty + 1 } : x); + const newQty = (found?.qty ?? 0) + qty; + if (newQty > limit) { + warned = true; + return c; } - return [...c, { item, qty: 1 }]; + 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) { + Swal.fire({ + icon: "warning", + title: maxQ > 0 && stock > maxQ ? "1회 발주 한도 초과" : "재고 부족", + text: `최대 ${fmt(limit)}개까지 담을 수 있습니다.`, + }); + return; + } Swal.fire({ toast: true, position: "top-end", icon: "success", - title: `장바구니에 추가됨: ${item.ITEM_NAME}`, - showConfirmButton: false, timer: 1200, timerProgressBar: true, + title: toastTitle, + showConfirmButton: false, timer: 1000, timerProgressBar: true, }); }; @@ -404,8 +415,8 @@ export default function ItemsBrowse() {

현재 재고가 있는 품목을 선택해 상단 장바구니에 담고 [발주 요청] 버튼으로 전송하세요.

-
-
+
+
조회 + {/* 보기 모드 토글 */} +
+ + +
{loading ? (
불러오는 중...
) : items.length === 0 ? (
검색 결과가 없습니다.
+ ) : viewMode === "list" ? ( + updateQty(o, 1)} + onMinus={(o) => updateQty(o, -1)} + onSetQty={setQty} + onRemove={removeLine} + /> ) : (
- {items.map((it) => ( -
-
- {it.IMAGE_URL ? ( - // eslint-disable-next-line @next/next/no-img-element - {it.ITEM_NAME} + {items.map((it) => { + const cartLine = cart.find((x) => x.item.OBJID === it.OBJID); + const inCart = cartLine?.qty ?? 0; + const stock = Number(it.STOCK_QTY); + const maxQ = Number(it.MAX_ORDER_QTY ?? 0); + const limit = maxQ > 0 ? Math.min(stock, maxQ) : stock; + const soldOut = stock === 0; + return ( +
0 ? "border-emerald-400 ring-2 ring-emerald-100" : "border-slate-200 hover:shadow-md"}`}> +
+ {it.IMAGE_URL ? ( + // eslint-disable-next-line @next/next/no-img-element + {it.ITEM_NAME} + ) : ( +
이미지 없음
+ )} + {inCart > 0 && ( + + 담은 {inCart} + + )} +
+
+
{it.ITEM_NAME}
+
+ {it.IS_TAX_FREE === "Y" && ( + 면세 + )} + {it.REQUIRES_DELIVERY === "Y" && ( + 택배 + )} +
+
+
{it.MAKER_NAME || "-"}
+
+
₩{fmt(it.UNIT_PRICE)}
+
0 ? "text-emerald-700" : "text-rose-500"}`}> + 재고 {fmt(stock)}{it.UNIT} +
+
+ {maxQ > 0 && ( +
1회 한도 ≤ {fmt(maxQ)}
+ )} + + {/* 수량 컨트롤 — 카드 안에서 바로 조절 */} + {soldOut ? ( +
+ 품절 +
+ ) : inCart === 0 ? ( +
+ (e.target as HTMLInputElement).select()} + onKeyDown={(e) => { + if (e.key === "Enter") { + const val = Number((e.target as HTMLInputElement).value) || 1; + const q = Math.max(1, Math.min(limit, val)); + addManyToCart(it, q); + (e.target as HTMLInputElement).value = "1"; + } + }} + id={`qty-${it.OBJID}`} + className="w-12 h-9 px-2 rounded-lg border border-slate-200 text-center text-sm tabular-nums focus:border-emerald-500 outline-none" + /> + +
) : ( -
이미지 없음
+
+ + setQty(it.OBJID, Number(e.target.value))} + className="flex-1 h-8 text-center font-bold text-sm tabular-nums bg-white border border-emerald-200 rounded-md" + /> + + +
)}
-
-
{it.ITEM_NAME}
-
- {it.IS_TAX_FREE === "Y" && ( - 면세 - )} - {it.REQUIRES_DELIVERY === "Y" && ( - 택배 - )} -
-
-
{it.MAKER_NAME || "-"}
-
-
₩{fmt(it.UNIT_PRICE)}
-
0 ? "text-emerald-700" : "text-rose-500"}`}> - 재고 {fmt(it.STOCK_QTY)}{it.UNIT} -
-
- {it.MAX_ORDER_QTY != null && Number(it.MAX_ORDER_QTY) > 0 && ( -
1회 한도 ≤ {fmt(it.MAX_ORDER_QTY)}
- )} - -
- ))} + ); + })}
)}
@@ -491,3 +598,100 @@ function Row({ label, value, color }: { label: string; value: string; color?: "v
); } + +function ListView({ items, cart, onAdd, onPlus, onMinus, onSetQty, onRemove }: { + items: Item[]; cart: CartLine[]; + onAdd: (it: Item, qty: number) => void; + onPlus: (o: string) => void; + onMinus: (o: string) => void; + onSetQty: (o: string, v: number) => void; + onRemove: (o: string) => void; +}) { + return ( +
+
+ + + + + + + + + + + + {items.map((it) => { + const cartLine = cart.find((x) => x.item.OBJID === it.OBJID); + const inCart = cartLine?.qty ?? 0; + const stock = Number(it.STOCK_QTY); + const maxQ = Number(it.MAX_ORDER_QTY ?? 0); + const limit = maxQ > 0 ? Math.min(stock, maxQ) : stock; + const soldOut = stock === 0; + return ( + 0 ? "bg-emerald-50/40" : "hover:bg-slate-50"}`}> + + + + + + + ); + })} + +
품목구분단가재고수량
+
+ {it.ITEM_NAME} + {it.REQUIRES_DELIVERY === "Y" && 택배} + {inCart > 0 && 담은 {inCart}} +
+
{it.MAKER_NAME || "-"}
+
+ + {it.IS_TAX_FREE === "Y" ? "면세" : "과세"} + + ₩{Number(it.UNIT_PRICE).toLocaleString("ko-KR")}{Number(stock).toLocaleString("ko-KR")} + {soldOut ? ( +
품절
+ ) : inCart === 0 ? ( +
+ (e.target as HTMLInputElement).select()} + onKeyDown={(e) => { + if (e.key === "Enter") { + const v = Number((e.target as HTMLInputElement).value) || 1; + onAdd(it, Math.max(1, Math.min(limit, v))); + (e.target as HTMLInputElement).value = "1"; + } + }} + id={`lqty-${it.OBJID}`} + className="w-14 h-8 px-2 rounded border border-slate-200 text-center text-sm tabular-nums" + /> + +
+ ) : ( +
+ + onSetQty(it.OBJID, Number(e.target.value))} + className="w-12 h-8 text-center font-bold text-sm tabular-nums border border-emerald-200 rounded" /> + + +
+ )} +
+
+
+ ); +}