fix: 구매입고 전체 프로세스 완성 (E2E 16/16 통과)

- backend: inventory_stock INSERT 시 id 누락 버그 수정
- frontend: 거래처 API supplier_mng으로 수정
- frontend: cart_items 실제 컬럼 구조 맞춤
- frontend: InboundCart 확정 로직 PC와 동일하게 정렬
- 검증: 발주→장바구니→입고등록→재고증가→발주상태변경 전체 확인
This commit is contained in:
SeongHyun Kim
2026-04-01 18:53:32 +09:00
parent 4b9b26d957
commit b6c1b08049
4 changed files with 75 additions and 62 deletions

View File

@@ -246,10 +246,10 @@ export async function create(req: AuthenticatedRequest, res: Response) {
} else {
await client.query(
`INSERT INTO inventory_stock (
company_code, item_code, warehouse_code, location_code,
id, company_code, item_code, warehouse_code, location_code,
current_qty, safety_qty, last_in_date,
created_date, updated_date, writer
) VALUES ($1, $2, $3, $4, $5, '0', NOW(), NOW(), NOW(), $6)`,
) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, '0', NOW(), NOW(), NOW(), $6)`,
[companyCode, itemCode, whCode, locCode, String(inQty), userId]
);
}

View File

@@ -140,14 +140,18 @@ export function InboundCart({
setInspectionTarget(null);
};
/* Confirm inbound */
/* Confirm inbound — PC receivingController.create 와 동일한 body 구조 */
const handleConfirm = async () => {
if (items.length === 0) return;
if (!selectedWarehouse) {
setResultMsg("오류: 입고 창고를 선택해주세요.");
return;
}
setConfirming(true);
setResultMsg(null);
try {
// 1. Generate inbound number
// 1. 입고번호 채번 (RCV-YYYY-XXXX)
let inboundNumber: string | undefined;
try {
const numRes = await apiClient.get("/receiving/generate-number");
@@ -155,24 +159,25 @@ export function InboundCart({
inboundNumber = numRes.data.data;
}
} catch {
// If number generation fails, backend will handle it
// 채번 실패 시 백엔드가 처리
}
// 2. Build payload with warehouse_code
// 2. POST /api/receiving — PC create 와 동일한 payload
const payload = {
inbound_date: new Date().toISOString().slice(0, 10),
inbound_number: inboundNumber,
warehouse_code: selectedWarehouse || undefined,
items: items.map((item) => ({
inbound_date: new Date().toISOString().slice(0, 10),
warehouse_code: selectedWarehouse,
inbound_type: "구매입고",
items: items.map((item, idx) => ({
inbound_type: "구매입고",
item_number: item.item_code,
item_name: item.item_name,
spec: item.spec,
material: item.material,
spec: item.spec || "",
material: item.material || "",
unit: "EA",
inbound_qty: String(item.inbound_qty),
unit_price: String(item.unit_price),
total_amount: String(item.inbound_qty * item.unit_price),
unit_price: String(item.unit_price || 0),
total_amount: String((item.inbound_qty || 0) * (item.unit_price || 0)),
reference_number: item.purchase_no,
supplier_code: item.supplier_code,
supplier_name: item.supplier_name,
@@ -182,27 +187,30 @@ export function InboundCart({
? "검사대기"
: "합격",
source_table: item.source_table,
source_id: item.source_id,
source_id: item.source_id || item.id,
seq_no: idx + 1,
})),
};
const res = await apiClient.post("/receiving", payload);
if (res.data?.success) {
// 3. Clean up cart_items in DB (background, non-blocking)
const sourceIds = items.map((item) => item.source_id).filter(Boolean);
if (sourceIds.length > 0) {
// 3. cart_items DB 정리 (백그라운드, 논블로킹)
// cart_items.row_key 로 삭제 (row_key = source_id 로 저장됨)
const rowKeys = items.map((item) => item.source_id || item.id).filter(Boolean);
if (rowKeys.length > 0) {
apiClient.post("/pop/execute-action", {
tasks: [{ type: "cart-save" }],
cartChanges: {
toDelete: sourceIds,
toDelete: rowKeys,
},
}).catch(() => {
// cart cleanup failed silently
// cart cleanup 실패 시 무시
});
}
setResultMsg(`${items.length}건 입고 등록 완료!`);
const inboundNo = res.data?.data?.header?.inbound_number || inboundNumber || "";
setResultMsg(`${items.length}건 입고 등록 완료! (${inboundNo})`);
setTimeout(() => {
onClear();
onClose();

View File

@@ -124,28 +124,22 @@ export function PurchaseInbound({ onCartCountChange, externalCartOpen, onExterna
const supplierInputRef = useRef<HTMLInputElement>(null);
const supplierDropdownRef = useRef<HTMLDivElement>(null);
/* Fetch all suppliers for inline search */
/* Fetch all suppliers for inline search (supplier_mng = 공급사) */
const fetchAllSuppliers = useCallback(async () => {
try {
const res = await apiClient.get("/data/customer_info", { params: { pageSize: 500 } });
const res = await apiClient.get("/data/supplier_mng", { params: { pageSize: 500 } });
const data = res.data?.data ?? res.data?.rows ?? [];
const list: Supplier[] = (Array.isArray(data) ? data : []).map((r: Record<string, unknown>) => ({
id: String(r.id ?? ""),
customer_name: String(r.customer_name ?? r.name ?? ""),
customer_code: String(r.customer_code ?? r.code ?? ""),
customer_name: String(r.supplier_name ?? r.customer_name ?? r.name ?? ""),
customer_code: String(r.supplier_code ?? r.customer_code ?? r.code ?? ""),
business_number: String(r.business_number ?? ""),
phone: String(r.phone ?? ""),
phone: String(r.contact_phone ?? r.phone ?? ""),
address: String(r.address ?? ""),
}));
setAllSuppliers(list);
} catch {
setAllSuppliers([
{ id: "d1", customer_name: "(주)한국철강", customer_code: "C001" },
{ id: "d2", customer_name: "(주)대한알루미늄", customer_code: "C002" },
{ id: "d3", customer_name: "삼성기공", customer_code: "C003" },
{ id: "d4", customer_name: "(주)금강볼트", customer_code: "C004" },
{ id: "d5", customer_name: "태양산업", customer_code: "C005" },
]);
setAllSuppliers([]);
}
}, []);
@@ -273,22 +267,29 @@ export function PurchaseInbound({ onCartCountChange, externalCartOpen, onExterna
setNumpadTarget(null);
// Save to cart_items table (background, non-blocking)
// cart_items 실제 컬럼: screen_id, source_table, row_key, row_data, quantity, unit
apiClient.post("/pop/execute-action", {
tasks: [{ type: "cart-save" }],
cartChanges: {
toCreate: [{
screen_id: "pop-purchase-inbound",
item_code: order.item_code,
item_name: order.item_name,
qty: String(Math.min(qty, order.remain_qty)),
supplier_code: order.supplier_code,
supplier_name: order.supplier_name,
purchase_no: order.purchase_no,
source_table: order.source_table,
source_id: order.id,
unit_price: String(order.unit_price || 0),
spec: order.spec || "",
material: order.material || "",
row_key: order.id,
row_data: JSON.stringify({
item_code: order.item_code,
item_name: order.item_name,
supplier_code: order.supplier_code,
supplier_name: order.supplier_name,
purchase_no: order.purchase_no,
unit_price: order.unit_price || 0,
spec: order.spec || "",
material: order.material || "",
order_qty: order.order_qty,
remain_qty: order.remain_qty,
}),
quantity: String(Math.min(qty, order.remain_qty)),
unit: "EA",
status: "pending",
}],
},
}).catch(() => {

View File

@@ -82,39 +82,43 @@ export function SupplierModal({ open, onClose, onSelect }: SupplierModalProps) {
const [sortMode, setSortMode] = useState<"korean" | "abc">("korean");
const [loading, setLoading] = useState(false);
/* Fetch suppliers */
/* Fetch suppliers (supplier_mng = 공급사/거래처) */
const fetchSuppliers = useCallback(async () => {
setLoading(true);
try {
const res = await apiClient.get("/data/customer_info", {
// 구매입고 거래처 = supplier_mng (공급사)
const res = await apiClient.get("/data/supplier_mng", {
params: { pageSize: 500 },
});
const data = res.data?.data ?? res.data?.rows ?? [];
const list: Supplier[] = (Array.isArray(data) ? data : []).map((r: Record<string, unknown>) => ({
id: String(r.id ?? ""),
customer_name: String(r.customer_name ?? r.name ?? ""),
customer_code: String(r.customer_code ?? r.code ?? ""),
customer_name: String(r.supplier_name ?? r.customer_name ?? r.name ?? ""),
customer_code: String(r.supplier_code ?? r.customer_code ?? r.code ?? ""),
business_number: String(r.business_number ?? ""),
phone: String(r.phone ?? ""),
phone: String(r.contact_phone ?? r.phone ?? ""),
address: String(r.address ?? ""),
}));
setSuppliers(list);
} catch {
// fallback: dummy suppliers
setSuppliers([
{ id: "d1", customer_name: "(주)한국철강", customer_code: "C001" },
{ id: "d2", customer_name: "(주)대한알루미늄", customer_code: "C002" },
{ id: "d3", customer_name: "삼성기공", customer_code: "C003" },
{ id: "d4", customer_name: "(주)금강볼트", customer_code: "C004" },
{ id: "d5", customer_name: "태양산업", customer_code: "C005" },
{ id: "d6", customer_name: "가나다철강", customer_code: "C006" },
{ id: "d7", customer_name: "나이스플라스틱", customer_code: "C007" },
{ id: "d8", customer_name: "다솔기계", customer_code: "C008" },
{ id: "d9", customer_name: "마산정밀", customer_code: "C009" },
{ id: "d10", customer_name: "바다금속", customer_code: "C010" },
{ id: "d11", customer_name: "사단법인 산업협회", customer_code: "C011" },
{ id: "d12", customer_name: "아진산업", customer_code: "C012" },
]);
// fallback: customer_info 시도
try {
const res2 = await apiClient.get("/data/customer_info", {
params: { pageSize: 500 },
});
const data2 = res2.data?.data ?? res2.data?.rows ?? [];
const list2: Supplier[] = (Array.isArray(data2) ? data2 : []).map((r: Record<string, unknown>) => ({
id: String(r.id ?? ""),
customer_name: String(r.customer_name ?? r.name ?? ""),
customer_code: String(r.customer_code ?? r.code ?? ""),
business_number: String(r.business_number ?? ""),
phone: String(r.phone ?? ""),
address: String(r.address ?? ""),
}));
setSuppliers(list2);
} catch {
setSuppliers([]);
}
} finally {
setLoading(false);
}