fix: 구매입고 전체 프로세스 완성 (E2E 16/16 통과)
- backend: inventory_stock INSERT 시 id 누락 버그 수정 - frontend: 거래처 API supplier_mng으로 수정 - frontend: cart_items 실제 컬럼 구조 맞춤 - frontend: InboundCart 확정 로직 PC와 동일하게 정렬 - 검증: 발주→장바구니→입고등록→재고증가→발주상태변경 전체 확인
This commit is contained in:
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user