fix: cart_items DB 저장 + 입고번호 확정시점 채번 + 장바구니 조회

- cart_items: id를 crypto.randomUUID()로 생성 (NOT NULL 대응)
- cart_items: row_data에 품목 정보 JSON 저장
- 입고번호: 장바구니에서 미리 표시 안 함 → 확정 시 생성
- 장바구니 조회: 올바른 API 파라미터로 수정
- 담기 취소 시 dbId로 정확한 cart_items DELETE
This commit is contained in:
SeongHyun Kim
2026-04-01 22:03:37 +09:00
parent d20b65bd12
commit 4b5bccb86a
3 changed files with 41 additions and 41 deletions

View File

@@ -22,6 +22,8 @@ interface Warehouse {
export interface CartItem {
id: string;
/** cart_items 테이블의 PK (UUID) — DB 삭제용 */
dbId?: string;
/** purchase_detail or purchase_order_mng */
source_table: string;
/** PK of the source row */

View File

@@ -108,8 +108,7 @@ export function InboundCartPage() {
screen_id: "pop-purchase-inbound",
status: "pending",
pageSize: 100,
sortColumn: "created_date",
sortDirection: "desc",
orderBy: "created_date DESC",
},
});
@@ -190,18 +189,8 @@ export function InboundCartPage() {
}, []);
/* ------------------------------------------------------------------ */
/* Generate inbound number */
/* Generate inbound number (확정 시점에만 호출 — 동시접속 충돌 방지) */
/* ------------------------------------------------------------------ */
const fetchInboundNumber = useCallback(async () => {
try {
const res = await apiClient.get("/receiving/generate-number");
if (res.data?.success && res.data?.data) {
setInboundNumber(res.data.data);
}
} catch {
/* generate on confirm as fallback */
}
}, []);
/* ------------------------------------------------------------------ */
/* Initial load */
@@ -211,8 +200,7 @@ export function InboundCartPage() {
fetchedRef.current = true;
fetchCartItems();
fetchWarehouses();
fetchInboundNumber();
}, [fetchCartItems, fetchWarehouses, fetchInboundNumber]);
}, [fetchCartItems, fetchWarehouses]);
/* ------------------------------------------------------------------ */
/* Selection */
@@ -376,17 +364,16 @@ export function InboundCartPage() {
setResultMsg(null);
try {
// Use existing inbound number or generate new one
let finalNumber = inboundNumber;
if (!finalNumber) {
try {
const numRes = await apiClient.get("/receiving/generate-number");
if (numRes.data?.success && numRes.data?.data) {
finalNumber = numRes.data.data;
}
} catch {
/* backend will handle */
// 확정 시점에 채번 (동시접속 충돌 방지)
let finalNumber = "";
try {
const numRes = await apiClient.get("/receiving/generate-number");
if (numRes.data?.success && numRes.data?.data) {
finalNumber = numRes.data.data;
setInboundNumber(finalNumber);
}
} catch {
/* backend will handle */
}
// POST /api/receiving -- same payload structure as PC
@@ -559,11 +546,9 @@ export function InboundCartPage() {
| {selectedWarehouseName}
</span>
)}
{inboundNumber && (
<span className="ml-auto text-[11px] font-mono text-gray-500">
{inboundNumber}
</span>
)}
<span className="ml-auto text-[11px] font-mono text-gray-400">
{inboundNumber || "확정 시 자동생성"}
</span>
</div>
{/* Info fields: 3 columns */}
@@ -609,13 +594,17 @@ export function InboundCartPage() {
</button>
</div>
{/* Inbound number (readonly) */}
{/* Inbound number (readonly — 확정 시점에 채번) */}
<div>
<label className="text-[11px] font-semibold text-gray-400 mb-1 block">
</label>
<div className="w-full px-3 py-2.5 border border-gray-200 rounded-lg text-sm bg-gray-50 text-gray-600 font-mono">
{inboundNumber || "자동 채번"}
<div className="w-full px-3 py-2.5 border border-gray-200 rounded-lg text-sm bg-gray-50 font-mono">
{inboundNumber ? (
<span className="text-gray-700">{inboundNumber}</span>
) : (
<span className="text-gray-400"> </span>
)}
</div>
</div>
</div>

View File

@@ -238,8 +238,10 @@ export function PurchaseInbound({ onCartCountChange }: PurchaseInboundProps = {}
const order = numpadTarget;
if (cartItems.find((c) => c.id === order.id)) return;
const cartDbId = crypto.randomUUID();
const newItem: CartItem = {
id: order.id,
dbId: cartDbId,
source_table: order.source_table,
source_id: order.id,
purchase_no: order.purchase_no,
@@ -262,11 +264,12 @@ export function PurchaseInbound({ onCartCountChange }: PurchaseInboundProps = {}
setNumpadTarget(null);
// Save to cart_items table (background, non-blocking)
// cart_items 실제 컬럼: screen_id, source_table, row_key, row_data, quantity, unit
// cart_items.id는 NOT NULL + 자동생성 없음 → 프론트에서 UUID 생성 필수
apiClient.post("/pop/execute-action", {
tasks: [{ type: "cart-save" }],
cartChanges: {
toCreate: [{
id: cartDbId,
screen_id: "pop-purchase-inbound",
source_table: order.source_table,
row_key: order.id,
@@ -281,9 +284,12 @@ export function PurchaseInbound({ onCartCountChange }: PurchaseInboundProps = {}
material: order.material || "",
order_qty: order.order_qty,
remain_qty: order.remain_qty,
order_date: order.order_date || "",
inspection_type: order.inspection_type,
}),
quantity: String(Math.min(qty, order.remain_qty)),
unit: "EA",
package_entries: packages.length > 0 ? JSON.stringify(packages) : null,
status: "pending",
}],
},
@@ -294,14 +300,17 @@ export function PurchaseInbound({ onCartCountChange }: PurchaseInboundProps = {}
/* Remove from cart (cancel) */
const handleRemoveFromCart = (id: string) => {
const item = cartItems.find((c) => c.id === id);
setCartItems((prev) => prev.filter((c) => c.id !== id));
// Delete from cart_items DB (background)
apiClient.post("/pop/execute-action", {
tasks: [{ type: "cart-save" }],
cartChanges: {
toDelete: [id],
},
}).catch(() => {});
// Delete from cart_items DB (background) — dbId(UUID)로 삭제
if (item?.dbId) {
apiClient.post("/pop/execute-action", {
tasks: [{ type: "cart-save" }],
cartChanges: {
toDelete: [item.dbId],
},
}).catch(() => {});
}
};
/* Search */