Merge branch 'main' into jskim-node
This commit is contained in:
@@ -7,70 +7,70 @@
|
||||
* - 기타출고 → item_info (품목)
|
||||
*/
|
||||
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import type { Response } from "express";
|
||||
import { getPool } from "../database/db";
|
||||
import type { AuthenticatedRequest } from "../types/auth";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// 출고 목록 조회
|
||||
export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const {
|
||||
outbound_type,
|
||||
outbound_status,
|
||||
search_keyword,
|
||||
date_from,
|
||||
date_to,
|
||||
} = req.query;
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const {
|
||||
outbound_type,
|
||||
outbound_status,
|
||||
search_keyword,
|
||||
date_from,
|
||||
date_to,
|
||||
} = req.query;
|
||||
|
||||
const conditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
let paramIdx = 1;
|
||||
const conditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
let paramIdx = 1;
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 전체 조회
|
||||
} else {
|
||||
conditions.push(`om.company_code = $${paramIdx}`);
|
||||
params.push(companyCode);
|
||||
paramIdx++;
|
||||
}
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 전체 조회
|
||||
} else {
|
||||
conditions.push(`om.company_code = $${paramIdx}`);
|
||||
params.push(companyCode);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
if (outbound_type && outbound_type !== "all") {
|
||||
conditions.push(`om.outbound_type = $${paramIdx}`);
|
||||
params.push(outbound_type);
|
||||
paramIdx++;
|
||||
}
|
||||
if (outbound_type && outbound_type !== "all") {
|
||||
conditions.push(`om.outbound_type = $${paramIdx}`);
|
||||
params.push(outbound_type);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
if (outbound_status && outbound_status !== "all") {
|
||||
conditions.push(`om.outbound_status = $${paramIdx}`);
|
||||
params.push(outbound_status);
|
||||
paramIdx++;
|
||||
}
|
||||
if (outbound_status && outbound_status !== "all") {
|
||||
conditions.push(`om.outbound_status = $${paramIdx}`);
|
||||
params.push(outbound_status);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
if (search_keyword) {
|
||||
conditions.push(
|
||||
`(om.outbound_number ILIKE $${paramIdx} OR om.item_name ILIKE $${paramIdx} OR om.item_code ILIKE $${paramIdx} OR om.customer_name ILIKE $${paramIdx} OR om.reference_number ILIKE $${paramIdx})`
|
||||
);
|
||||
params.push(`%${search_keyword}%`);
|
||||
paramIdx++;
|
||||
}
|
||||
if (search_keyword) {
|
||||
conditions.push(
|
||||
`(om.outbound_number ILIKE $${paramIdx} OR om.item_name ILIKE $${paramIdx} OR om.item_code ILIKE $${paramIdx} OR om.customer_name ILIKE $${paramIdx} OR om.reference_number ILIKE $${paramIdx})`,
|
||||
);
|
||||
params.push(`%${search_keyword}%`);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
if (date_from) {
|
||||
conditions.push(`om.outbound_date >= $${paramIdx}`);
|
||||
params.push(date_from);
|
||||
paramIdx++;
|
||||
}
|
||||
if (date_to) {
|
||||
conditions.push(`om.outbound_date <= $${paramIdx}`);
|
||||
params.push(date_to);
|
||||
paramIdx++;
|
||||
}
|
||||
if (date_from) {
|
||||
conditions.push(`om.outbound_date >= $${paramIdx}`);
|
||||
params.push(date_from);
|
||||
paramIdx++;
|
||||
}
|
||||
if (date_to) {
|
||||
conditions.push(`om.outbound_date <= $${paramIdx}`);
|
||||
params.push(date_to);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
const whereClause =
|
||||
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
const whereClause =
|
||||
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
|
||||
const query = `
|
||||
const query = `
|
||||
SELECT
|
||||
om.*,
|
||||
wh.warehouse_name
|
||||
@@ -82,42 +82,52 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
ORDER BY om.created_date DESC
|
||||
`;
|
||||
|
||||
const pool = getPool();
|
||||
const result = await pool.query(query, params);
|
||||
const pool = getPool();
|
||||
const result = await pool.query(query, params);
|
||||
|
||||
logger.info("출고 목록 조회", {
|
||||
companyCode,
|
||||
rowCount: result.rowCount,
|
||||
});
|
||||
logger.info("출고 목록 조회", {
|
||||
companyCode,
|
||||
rowCount: result.rowCount,
|
||||
});
|
||||
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("출고 목록 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("출고 목록 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 출고 등록 (다건)
|
||||
export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { items, outbound_number, outbound_date, warehouse_code, location_code, manager_id, memo } = req.body;
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const {
|
||||
items,
|
||||
outbound_number,
|
||||
outbound_date,
|
||||
warehouse_code,
|
||||
location_code,
|
||||
manager_id,
|
||||
memo,
|
||||
} = req.body;
|
||||
|
||||
if (!items || !Array.isArray(items) || items.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "출고 품목이 없습니다." });
|
||||
}
|
||||
if (!items || !Array.isArray(items) || items.length === 0) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, message: "출고 품목이 없습니다." });
|
||||
}
|
||||
|
||||
await client.query("BEGIN");
|
||||
await client.query("BEGIN");
|
||||
|
||||
const insertedRows: any[] = [];
|
||||
const insertedRows: any[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const result = await client.query(
|
||||
`INSERT INTO outbound_mng (
|
||||
for (const item of items) {
|
||||
const result = await client.query(
|
||||
`INSERT INTO outbound_mng (
|
||||
id, company_code, outbound_number, outbound_type, outbound_date,
|
||||
reference_number, customer_code, customer_name,
|
||||
item_code, item_name, specification, material, unit,
|
||||
@@ -138,165 +148,202 @@ export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
$26, $27, $28,
|
||||
NOW(), $29, $29, '출고'
|
||||
) RETURNING *`,
|
||||
[
|
||||
companyCode,
|
||||
outbound_number || item.outbound_number,
|
||||
item.outbound_type,
|
||||
outbound_date || item.outbound_date,
|
||||
item.reference_number || null,
|
||||
item.customer_code || null,
|
||||
item.customer_name || null,
|
||||
item.item_code || item.item_number || null,
|
||||
item.item_name || null,
|
||||
item.spec || item.specification || null,
|
||||
item.material || null,
|
||||
item.unit || "EA",
|
||||
item.outbound_qty || 0,
|
||||
item.unit_price || 0,
|
||||
item.total_amount || 0,
|
||||
item.lot_number || null,
|
||||
warehouse_code || item.warehouse_code || null,
|
||||
location_code || item.location_code || null,
|
||||
item.outbound_status || "대기",
|
||||
manager_id || item.manager_id || null,
|
||||
memo || item.memo || null,
|
||||
item.source_type || null,
|
||||
item.sales_order_id || null,
|
||||
item.shipment_plan_id || null,
|
||||
item.item_info_id || null,
|
||||
item.destination_code || null,
|
||||
item.delivery_destination || null,
|
||||
item.delivery_address || null,
|
||||
userId,
|
||||
]
|
||||
);
|
||||
[
|
||||
companyCode,
|
||||
outbound_number || item.outbound_number,
|
||||
item.outbound_type,
|
||||
outbound_date || item.outbound_date,
|
||||
item.reference_number || null,
|
||||
item.customer_code || null,
|
||||
item.customer_name || null,
|
||||
item.item_code || item.item_number || null,
|
||||
item.item_name || null,
|
||||
item.spec || item.specification || null,
|
||||
item.material || null,
|
||||
item.unit || "EA",
|
||||
item.outbound_qty || 0,
|
||||
item.unit_price || 0,
|
||||
item.total_amount || 0,
|
||||
item.lot_number || null,
|
||||
warehouse_code || item.warehouse_code || null,
|
||||
location_code || item.location_code || null,
|
||||
item.outbound_status || "대기",
|
||||
manager_id || item.manager_id || null,
|
||||
memo || item.memo || null,
|
||||
item.source_type || null,
|
||||
item.sales_order_id || null,
|
||||
item.shipment_plan_id || null,
|
||||
item.item_info_id || null,
|
||||
item.destination_code || null,
|
||||
item.delivery_destination || null,
|
||||
item.delivery_address || null,
|
||||
userId,
|
||||
],
|
||||
);
|
||||
|
||||
insertedRows.push(result.rows[0]);
|
||||
insertedRows.push(result.rows[0]);
|
||||
|
||||
// 재고 업데이트 (inventory_stock): 출고 수량 차감
|
||||
const itemCode = item.item_code || item.item_number || null;
|
||||
const whCode = warehouse_code || item.warehouse_code || null;
|
||||
const locCode = location_code || item.location_code || null;
|
||||
const outQty = Number(item.outbound_qty) || 0;
|
||||
if (itemCode && outQty > 0) {
|
||||
const existingStock = await client.query(
|
||||
`SELECT id FROM inventory_stock
|
||||
// 재고 업데이트 (inventory_stock): 출고 수량 차감
|
||||
const itemCode = item.item_code || item.item_number || null;
|
||||
const whCode = warehouse_code || item.warehouse_code || null;
|
||||
const locCode = location_code || item.location_code || null;
|
||||
const outQty = Number(item.outbound_qty) || 0;
|
||||
if (itemCode && outQty > 0) {
|
||||
// 재고 사전 검증: 부족 시 즉시 에러 (트랜잭션 ROLLBACK)
|
||||
const stockCheck = await client.query(
|
||||
`SELECT COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) as cur
|
||||
FROM inventory_stock
|
||||
WHERE company_code = $1 AND item_code = $2
|
||||
AND COALESCE(warehouse_code, '') = COALESCE($3, '')
|
||||
AND COALESCE(location_code, '') = COALESCE($4, '')
|
||||
LIMIT 1`,
|
||||
[companyCode, itemCode, whCode || '', locCode || '']
|
||||
);
|
||||
[companyCode, itemCode, whCode || "", locCode || ""],
|
||||
);
|
||||
const currentStock = parseFloat(stockCheck.rows[0]?.cur || "0");
|
||||
if (currentStock < outQty) {
|
||||
throw new Error(
|
||||
`재고 부족: ${item.item_name || itemCode} (창고 ${whCode || "미지정"}) — 현재 재고 ${currentStock}, 요청 출고 ${outQty}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (existingStock.rows.length > 0) {
|
||||
await client.query(
|
||||
`UPDATE inventory_stock
|
||||
const existingStock = await client.query(
|
||||
`SELECT id FROM inventory_stock
|
||||
WHERE company_code = $1 AND item_code = $2
|
||||
AND COALESCE(warehouse_code, '') = COALESCE($3, '')
|
||||
AND COALESCE(location_code, '') = COALESCE($4, '')
|
||||
LIMIT 1`,
|
||||
[companyCode, itemCode, whCode || "", locCode || ""],
|
||||
);
|
||||
|
||||
if (existingStock.rows.length > 0) {
|
||||
await client.query(
|
||||
`UPDATE inventory_stock
|
||||
SET current_qty = CAST(GREATEST(COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) - $1, 0) AS text),
|
||||
last_out_date = NOW(),
|
||||
updated_date = NOW()
|
||||
WHERE id = $2`,
|
||||
[outQty, existingStock.rows[0].id]
|
||||
);
|
||||
} else {
|
||||
// 재고 레코드가 없으면 0으로 생성 (마이너스 방지)
|
||||
await client.query(
|
||||
`INSERT INTO inventory_stock (
|
||||
[outQty, existingStock.rows[0].id],
|
||||
);
|
||||
} else {
|
||||
// 재고 레코드가 없으면 0으로 생성 (마이너스 방지)
|
||||
await client.query(
|
||||
`INSERT INTO inventory_stock (
|
||||
id, company_code, item_code, warehouse_code, location_code,
|
||||
current_qty, safety_qty, last_out_date,
|
||||
created_date, updated_date, writer
|
||||
) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '0', '0', NOW(), NOW(), NOW(), $5)`,
|
||||
[companyCode, itemCode, whCode, locCode, userId]
|
||||
);
|
||||
}
|
||||
[companyCode, itemCode, whCode, locCode, userId],
|
||||
);
|
||||
}
|
||||
|
||||
// 재고 이력 기록 (inventory_history)
|
||||
const afterStockRes = await client.query(
|
||||
`SELECT current_qty FROM inventory_stock
|
||||
// 재고 이력 기록 (inventory_history)
|
||||
const afterStockRes = await client.query(
|
||||
`SELECT current_qty FROM inventory_stock
|
||||
WHERE company_code = $1 AND item_code = $2
|
||||
AND COALESCE(warehouse_code, '') = COALESCE($3, '')
|
||||
AND COALESCE(location_code, '') = COALESCE($4, '')
|
||||
LIMIT 1`,
|
||||
[companyCode, itemCode, whCode || '', locCode || '']
|
||||
);
|
||||
const afterQty = afterStockRes.rows[0]?.current_qty || '0';
|
||||
await client.query(
|
||||
`INSERT INTO inventory_history (
|
||||
[companyCode, itemCode, whCode || "", locCode || ""],
|
||||
);
|
||||
const afterQty = afterStockRes.rows[0]?.current_qty || "0";
|
||||
await client.query(
|
||||
`INSERT INTO inventory_history (
|
||||
id, company_code, item_code, warehouse_code, location_code,
|
||||
transaction_type, transaction_date, quantity, balance_qty, remark,
|
||||
writer, created_date
|
||||
) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '출고', NOW(), $5, $6, $7, $8, NOW())`,
|
||||
[companyCode, itemCode, whCode, locCode, String(-outQty), afterQty, item.outbound_type || '출고', userId]
|
||||
);
|
||||
}
|
||||
[
|
||||
companyCode,
|
||||
itemCode,
|
||||
whCode,
|
||||
locCode,
|
||||
String(-outQty),
|
||||
afterQty,
|
||||
item.outbound_type || "출고",
|
||||
userId,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 판매출고인 경우 출하지시의 ship_qty 업데이트 + 수주상세 ship_qty 반영
|
||||
if (item.outbound_type === "판매출고" && item.source_id && item.source_type === "shipment_instruction_detail") {
|
||||
const outQtyNum = Number(item.outbound_qty) || 0;
|
||||
await client.query(
|
||||
`UPDATE shipment_instruction_detail
|
||||
// 판매출고인 경우 출하지시의 ship_qty 업데이트 + 수주상세 ship_qty 반영
|
||||
if (
|
||||
item.outbound_type === "판매출고" &&
|
||||
item.source_id &&
|
||||
item.source_type === "shipment_instruction_detail"
|
||||
) {
|
||||
const outQtyNum = Number(item.outbound_qty) || 0;
|
||||
await client.query(
|
||||
`UPDATE shipment_instruction_detail
|
||||
SET ship_qty = COALESCE(ship_qty, 0) + $1,
|
||||
updated_date = NOW()
|
||||
WHERE id = $2 AND company_code = $3`,
|
||||
[outQtyNum, item.source_id, companyCode]
|
||||
);
|
||||
[outQtyNum, item.source_id, companyCode],
|
||||
);
|
||||
|
||||
// 출하지시 상세의 detail_id로 수주상세(sales_order_detail) ship_qty도 업데이트
|
||||
const sidRes = await client.query(
|
||||
`SELECT detail_id FROM shipment_instruction_detail WHERE id = $1 AND company_code = $2`,
|
||||
[item.source_id, companyCode]
|
||||
);
|
||||
const detailId = sidRes.rows[0]?.detail_id;
|
||||
if (detailId) {
|
||||
await client.query(
|
||||
`UPDATE sales_order_detail
|
||||
// 출하지시 상세의 detail_id로 수주상세(sales_order_detail) ship_qty도 업데이트
|
||||
const sidRes = await client.query(
|
||||
`SELECT detail_id FROM shipment_instruction_detail WHERE id = $1 AND company_code = $2`,
|
||||
[item.source_id, companyCode],
|
||||
);
|
||||
const detailId = sidRes.rows[0]?.detail_id;
|
||||
if (detailId) {
|
||||
await client.query(
|
||||
`UPDATE sales_order_detail
|
||||
SET ship_qty = (COALESCE(NULLIF(ship_qty,'')::numeric, 0) + $1)::text,
|
||||
balance_qty = (COALESCE(NULLIF(qty,'')::numeric, 0) - COALESCE(NULLIF(ship_qty,'')::numeric, 0) - $1)::text,
|
||||
updated_date = NOW()
|
||||
WHERE id = $2 AND company_code = $3`,
|
||||
[outQtyNum, detailId, companyCode]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
[outQtyNum, detailId, companyCode],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
await client.query("COMMIT");
|
||||
|
||||
logger.info("출고 등록 완료", {
|
||||
companyCode,
|
||||
userId,
|
||||
count: insertedRows.length,
|
||||
outbound_number,
|
||||
});
|
||||
logger.info("출고 등록 완료", {
|
||||
companyCode,
|
||||
userId,
|
||||
count: insertedRows.length,
|
||||
outbound_number,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: insertedRows,
|
||||
message: `${insertedRows.length}건 출고 등록 완료`,
|
||||
});
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
logger.error("출고 등록 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
return res.json({
|
||||
success: true,
|
||||
data: insertedRows,
|
||||
message: `${insertedRows.length}건 출고 등록 완료`,
|
||||
});
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
logger.error("출고 등록 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 출고 수정
|
||||
export async function update(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { id } = req.params;
|
||||
const {
|
||||
outbound_date, outbound_qty, unit_price, total_amount,
|
||||
lot_number, warehouse_code, location_code,
|
||||
outbound_status, manager_id: mgr, memo,
|
||||
} = req.body;
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { id } = req.params;
|
||||
const {
|
||||
outbound_date,
|
||||
outbound_qty,
|
||||
unit_price,
|
||||
total_amount,
|
||||
lot_number,
|
||||
warehouse_code,
|
||||
location_code,
|
||||
outbound_status,
|
||||
manager_id: mgr,
|
||||
memo,
|
||||
} = req.body;
|
||||
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
`UPDATE outbound_mng SET
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
`UPDATE outbound_mng SET
|
||||
outbound_date = COALESCE($1, outbound_date),
|
||||
outbound_qty = COALESCE($2, outbound_qty),
|
||||
unit_price = COALESCE($3, unit_price),
|
||||
@@ -311,73 +358,89 @@ export async function update(req: AuthenticatedRequest, res: Response) {
|
||||
updated_by = $11
|
||||
WHERE id = $12 AND company_code = $13
|
||||
RETURNING *`,
|
||||
[
|
||||
outbound_date, outbound_qty, unit_price, total_amount,
|
||||
lot_number, warehouse_code, location_code,
|
||||
outbound_status, mgr, memo,
|
||||
userId, id, companyCode,
|
||||
]
|
||||
);
|
||||
[
|
||||
outbound_date,
|
||||
outbound_qty,
|
||||
unit_price,
|
||||
total_amount,
|
||||
lot_number,
|
||||
warehouse_code,
|
||||
location_code,
|
||||
outbound_status,
|
||||
mgr,
|
||||
memo,
|
||||
userId,
|
||||
id,
|
||||
companyCode,
|
||||
],
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({ success: false, message: "출고 데이터를 찾을 수 없습니다." });
|
||||
}
|
||||
if (result.rowCount === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "출고 데이터를 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
logger.info("출고 수정", { companyCode, userId, id });
|
||||
logger.info("출고 수정", { companyCode, userId, id });
|
||||
|
||||
return res.json({ success: true, data: result.rows[0] });
|
||||
} catch (error: any) {
|
||||
logger.error("출고 수정 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
return res.json({ success: true, data: result.rows[0] });
|
||||
} catch (error: any) {
|
||||
logger.error("출고 수정 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 출고 삭제
|
||||
export async function deleteOutbound(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { id } = req.params;
|
||||
const pool = getPool();
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { id } = req.params;
|
||||
const pool = getPool();
|
||||
|
||||
const result = await pool.query(
|
||||
`DELETE FROM outbound_mng WHERE id = $1 AND company_code = $2 RETURNING id`,
|
||||
[id, companyCode]
|
||||
);
|
||||
const result = await pool.query(
|
||||
`DELETE FROM outbound_mng WHERE id = $1 AND company_code = $2 RETURNING id`,
|
||||
[id, companyCode],
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||
}
|
||||
if (result.rowCount === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "데이터를 찾을 수 없습니다." });
|
||||
}
|
||||
|
||||
logger.info("출고 삭제", { companyCode, id });
|
||||
logger.info("출고 삭제", { companyCode, id });
|
||||
|
||||
return res.json({ success: true, message: "삭제 완료" });
|
||||
} catch (error: any) {
|
||||
logger.error("출고 삭제 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
return res.json({ success: true, message: "삭제 완료" });
|
||||
} catch (error: any) {
|
||||
logger.error("출고 삭제 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 판매출고용: 출하지시 데이터 조회
|
||||
export async function getShipmentInstructions(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { keyword } = req.query;
|
||||
export async function getShipmentInstructions(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { keyword } = req.query;
|
||||
|
||||
const conditions: string[] = ["si.company_code = $1"];
|
||||
const params: any[] = [companyCode];
|
||||
let paramIdx = 2;
|
||||
const conditions: string[] = ["si.company_code = $1"];
|
||||
const params: any[] = [companyCode];
|
||||
let paramIdx = 2;
|
||||
|
||||
if (keyword) {
|
||||
conditions.push(
|
||||
`(si.instruction_no ILIKE $${paramIdx} OR sid.item_name ILIKE $${paramIdx} OR sid.item_code ILIKE $${paramIdx})`
|
||||
);
|
||||
params.push(`%${keyword}%`);
|
||||
paramIdx++;
|
||||
}
|
||||
if (keyword) {
|
||||
conditions.push(
|
||||
`(si.instruction_no ILIKE $${paramIdx} OR sid.item_name ILIKE $${paramIdx} OR sid.item_code ILIKE $${paramIdx})`,
|
||||
);
|
||||
params.push(`%${keyword}%`);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
sid.id AS detail_id,
|
||||
si.id AS instruction_id,
|
||||
si.instruction_no,
|
||||
@@ -400,42 +463,45 @@ export async function getShipmentInstructions(req: AuthenticatedRequest, res: Re
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
AND COALESCE(sid.plan_qty, 0) > COALESCE(sid.ship_qty, 0)
|
||||
ORDER BY si.instruction_date DESC, si.instruction_no`,
|
||||
params
|
||||
);
|
||||
params,
|
||||
);
|
||||
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("출하지시 데이터 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("출하지시 데이터 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 반품출고용: 발주(입고) 데이터 조회
|
||||
export async function getPurchaseOrders(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { keyword } = req.query;
|
||||
export async function getPurchaseOrders(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { keyword } = req.query;
|
||||
|
||||
const conditions: string[] = ["company_code = $1"];
|
||||
const params: any[] = [companyCode];
|
||||
let paramIdx = 2;
|
||||
const conditions: string[] = ["company_code = $1"];
|
||||
const params: any[] = [companyCode];
|
||||
let paramIdx = 2;
|
||||
|
||||
// 입고된 것만 (반품 대상)
|
||||
conditions.push(
|
||||
`COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) > 0`
|
||||
);
|
||||
// 입고된 것만 (반품 대상)
|
||||
conditions.push(
|
||||
`COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0) > 0`,
|
||||
);
|
||||
|
||||
if (keyword) {
|
||||
conditions.push(
|
||||
`(purchase_no ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx} OR item_code ILIKE $${paramIdx} OR supplier_name ILIKE $${paramIdx})`
|
||||
);
|
||||
params.push(`%${keyword}%`);
|
||||
paramIdx++;
|
||||
}
|
||||
if (keyword) {
|
||||
conditions.push(
|
||||
`(purchase_no ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx} OR item_code ILIKE $${paramIdx} OR supplier_name ILIKE $${paramIdx})`,
|
||||
);
|
||||
params.push(`%${keyword}%`);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
id, purchase_no, order_date, supplier_code, supplier_name,
|
||||
item_code, item_name, spec, material,
|
||||
COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0) AS order_qty,
|
||||
@@ -445,137 +511,146 @@ export async function getPurchaseOrders(req: AuthenticatedRequest, res: Response
|
||||
FROM purchase_order_mng
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
ORDER BY order_date DESC, purchase_no`,
|
||||
params
|
||||
);
|
||||
params,
|
||||
);
|
||||
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("발주 데이터 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("발주 데이터 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 기타출고용: 품목 데이터 조회
|
||||
export async function getItems(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { keyword } = req.query;
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { keyword } = req.query;
|
||||
|
||||
const conditions: string[] = ["company_code = $1"];
|
||||
const params: any[] = [companyCode];
|
||||
let paramIdx = 2;
|
||||
const conditions: string[] = ["company_code = $1"];
|
||||
const params: any[] = [companyCode];
|
||||
let paramIdx = 2;
|
||||
|
||||
if (keyword) {
|
||||
conditions.push(
|
||||
`(item_number ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx})`
|
||||
);
|
||||
params.push(`%${keyword}%`);
|
||||
paramIdx++;
|
||||
}
|
||||
if (keyword) {
|
||||
conditions.push(
|
||||
`(item_number ILIKE $${paramIdx} OR item_name ILIKE $${paramIdx})`,
|
||||
);
|
||||
params.push(`%${keyword}%`);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
id, item_number, item_name, size AS spec, material, unit,
|
||||
COALESCE(CAST(NULLIF(standard_price, '') AS numeric), 0) AS standard_price
|
||||
FROM item_info
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
ORDER BY item_name`,
|
||||
params
|
||||
);
|
||||
params,
|
||||
);
|
||||
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("품목 데이터 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("품목 데이터 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 출고번호 자동생성
|
||||
export async function generateNumber(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const ruleId = (req.query.ruleId as string) || (req.query.rule_id as string);
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const ruleId =
|
||||
(req.query.ruleId as string) || (req.query.rule_id as string);
|
||||
|
||||
// 1순위: POP 화면설정에서 선택한 채번규칙 사용
|
||||
if (ruleId && ruleId !== "__none__") {
|
||||
try {
|
||||
const { numberingRuleService } = await import("../services/numberingRuleService");
|
||||
const newNumber = await numberingRuleService.allocateCode(ruleId, companyCode);
|
||||
return res.json({ success: true, data: newNumber });
|
||||
} catch (e: any) {
|
||||
logger.warn("선택한 채번규칙 사용 실패, 기본 채번으로 폴백", { ruleId, error: e.message });
|
||||
}
|
||||
}
|
||||
// 1순위: POP 화면설정에서 선택한 채번규칙 사용
|
||||
if (ruleId && ruleId !== "__none__") {
|
||||
try {
|
||||
const { numberingRuleService } = await import(
|
||||
"../services/numberingRuleService"
|
||||
);
|
||||
const newNumber = await numberingRuleService.allocateCode(
|
||||
ruleId,
|
||||
companyCode,
|
||||
);
|
||||
return res.json({ success: true, data: newNumber });
|
||||
} catch (e: any) {
|
||||
logger.warn("선택한 채번규칙 사용 실패, 기본 채번으로 폴백", {
|
||||
ruleId,
|
||||
error: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 2순위: 기본 하드코딩 채번 (OUT-YYYY-XXXX)
|
||||
const pool = getPool();
|
||||
const today = new Date();
|
||||
const yyyy = today.getFullYear();
|
||||
const prefix = `OUT-${yyyy}-`;
|
||||
// 2순위: 기본 하드코딩 채번 (OUT-YYYY-XXXX)
|
||||
const pool = getPool();
|
||||
const today = new Date();
|
||||
const yyyy = today.getFullYear();
|
||||
const prefix = `OUT-${yyyy}-`;
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT outbound_number FROM outbound_mng
|
||||
const result = await pool.query(
|
||||
`SELECT outbound_number FROM outbound_mng
|
||||
WHERE company_code = $1 AND outbound_number LIKE $2
|
||||
ORDER BY outbound_number DESC LIMIT 1`,
|
||||
[companyCode, `${prefix}%`]
|
||||
);
|
||||
[companyCode, `${prefix}%`],
|
||||
);
|
||||
|
||||
let seq = 1;
|
||||
if (result.rows.length > 0) {
|
||||
const lastNo = result.rows[0].outbound_number;
|
||||
const lastSeq = parseInt(lastNo.replace(prefix, ""), 10);
|
||||
if (!isNaN(lastSeq)) seq = lastSeq + 1;
|
||||
}
|
||||
let seq = 1;
|
||||
if (result.rows.length > 0) {
|
||||
const lastNo = result.rows[0].outbound_number;
|
||||
const lastSeq = parseInt(lastNo.replace(prefix, ""), 10);
|
||||
if (!isNaN(lastSeq)) seq = lastSeq + 1;
|
||||
}
|
||||
|
||||
const newNumber = `${prefix}${String(seq).padStart(4, "0")}`;
|
||||
const newNumber = `${prefix}${String(seq).padStart(4, "0")}`;
|
||||
|
||||
return res.json({ success: true, data: newNumber });
|
||||
} catch (error: any) {
|
||||
logger.error("출고번호 생성 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
return res.json({ success: true, data: newNumber });
|
||||
} catch (error: any) {
|
||||
logger.error("출고번호 생성 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 창고 목록 조회
|
||||
export async function getWarehouses(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const pool = getPool();
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const pool = getPool();
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT warehouse_code, warehouse_name, warehouse_type
|
||||
const result = await pool.query(
|
||||
`SELECT warehouse_code, warehouse_name, warehouse_type
|
||||
FROM warehouse_info
|
||||
WHERE company_code = $1 AND COALESCE(status, '') != '삭제'
|
||||
ORDER BY warehouse_name`,
|
||||
[companyCode]
|
||||
);
|
||||
[companyCode],
|
||||
);
|
||||
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("창고 목록 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("창고 목록 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 창고별 위치 목록 조회
|
||||
export async function getLocations(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const warehouseCode = req.query.warehouse_code as string;
|
||||
const pool = getPool();
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const warehouseCode = req.query.warehouse_code as string;
|
||||
const pool = getPool();
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT location_code, location_name, warehouse_code
|
||||
const result = await pool.query(
|
||||
`SELECT location_code, location_name, warehouse_code
|
||||
FROM warehouse_location
|
||||
WHERE company_code = $1 ${warehouseCode ? "AND warehouse_code = $2" : ""}
|
||||
ORDER BY location_code`,
|
||||
warehouseCode ? [companyCode, warehouseCode] : [companyCode]
|
||||
);
|
||||
warehouseCode ? [companyCode, warehouseCode] : [companyCode],
|
||||
);
|
||||
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("위치 목록 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("위치 목록 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { type Request, type Response, Router } from "express";
|
||||
import { getPool } from "../database/db";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
|
||||
@@ -9,32 +9,32 @@ router.use(authenticateToken);
|
||||
// ---- 검사 기준 조회 (item_inspection_info) ----
|
||||
// GET /api/pop/inspection-result/info?itemCode=ITEM-001&inspectionType=입고검사
|
||||
router.get("/info", async (req: Request, res: Response) => {
|
||||
const pool = getPool();
|
||||
const companyCode = (req as any).user?.companyCode;
|
||||
const { itemCode, itemId, inspectionType } = req.query;
|
||||
const pool = getPool();
|
||||
const companyCode = (req as any).user?.companyCode;
|
||||
const { itemCode, itemId, inspectionType } = req.query;
|
||||
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보 없음" });
|
||||
}
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보 없음" });
|
||||
}
|
||||
|
||||
const conditions: string[] = ["company_code = $1", "is_active = 'Y'"];
|
||||
const params: unknown[] = [companyCode];
|
||||
let idx = 2;
|
||||
const conditions: string[] = ["company_code = $1", "is_active = 'Y'"];
|
||||
const params: unknown[] = [companyCode];
|
||||
let idx = 2;
|
||||
|
||||
if (itemCode) {
|
||||
conditions.push(`item_code = $${idx++}`);
|
||||
params.push(itemCode);
|
||||
}
|
||||
if (itemId) {
|
||||
conditions.push(`item_id = $${idx++}`);
|
||||
params.push(itemId);
|
||||
}
|
||||
if (inspectionType) {
|
||||
conditions.push(`inspection_type = $${idx++}`);
|
||||
params.push(inspectionType);
|
||||
}
|
||||
if (itemCode) {
|
||||
conditions.push(`item_code = $${idx++}`);
|
||||
params.push(itemCode);
|
||||
}
|
||||
if (itemId) {
|
||||
conditions.push(`item_id = $${idx++}`);
|
||||
params.push(itemId);
|
||||
}
|
||||
if (inspectionType) {
|
||||
conditions.push(`inspection_type = $${idx++}`);
|
||||
params.push(inspectionType);
|
||||
}
|
||||
|
||||
const sql = `
|
||||
const sql = `
|
||||
SELECT id, item_id, item_code, item_name,
|
||||
inspection_type, inspection_item_name, inspection_standard,
|
||||
inspection_method, pass_criteria, is_required, sort_order, memo
|
||||
@@ -43,149 +43,272 @@ router.get("/info", async (req: Request, res: Response) => {
|
||||
ORDER BY sort_order, inspection_item_name
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await pool.query(sql, params);
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (err: any) {
|
||||
return res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
try {
|
||||
const result = await pool.query(sql, params);
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (err: any) {
|
||||
return res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ---- 검사 결과 조회 ----
|
||||
// GET /api/pop/inspection-result?referenceId=xxx&referenceTable=yyy&screenId=zzz
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const pool = getPool();
|
||||
const companyCode = (req as any).user?.companyCode;
|
||||
const { referenceId, referenceTable, screenId } = req.query;
|
||||
const pool = getPool();
|
||||
const companyCode = (req as any).user?.companyCode;
|
||||
const { referenceId, referenceTable, screenId } = req.query;
|
||||
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보 없음" });
|
||||
}
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보 없음" });
|
||||
}
|
||||
|
||||
const conditions: string[] = ["company_code = $1"];
|
||||
const params: unknown[] = [companyCode];
|
||||
let idx = 2;
|
||||
const conditions: string[] = ["company_code = $1"];
|
||||
const params: unknown[] = [companyCode];
|
||||
let idx = 2;
|
||||
|
||||
if (referenceId) {
|
||||
conditions.push(`reference_id = $${idx++}`);
|
||||
params.push(referenceId);
|
||||
}
|
||||
if (referenceTable) {
|
||||
conditions.push(`reference_table = $${idx++}`);
|
||||
params.push(referenceTable);
|
||||
}
|
||||
if (screenId) {
|
||||
conditions.push(`screen_id = $${idx++}`);
|
||||
params.push(screenId);
|
||||
}
|
||||
if (referenceId) {
|
||||
conditions.push(`reference_id = $${idx++}`);
|
||||
params.push(referenceId);
|
||||
}
|
||||
if (referenceTable) {
|
||||
conditions.push(`reference_table = $${idx++}`);
|
||||
params.push(referenceTable);
|
||||
}
|
||||
if (screenId) {
|
||||
conditions.push(`screen_id = $${idx++}`);
|
||||
params.push(screenId);
|
||||
}
|
||||
|
||||
const sql = `
|
||||
const sql = `
|
||||
SELECT *
|
||||
FROM inspection_result
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
ORDER BY created_date DESC
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await pool.query(sql, params);
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (err: any) {
|
||||
return res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
try {
|
||||
const result = await pool.query(sql, params);
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (err: any) {
|
||||
return res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ---- 검사 결과 저장 (INSERT or UPDATE) ----
|
||||
// ---- 검사번호 채번 (PC numberingRuleService 활용) ----
|
||||
async function generateInspectionNumber(companyCode: string): Promise<string> {
|
||||
// PC 채번 서비스 동적 import (순환 참조 방지)
|
||||
const { numberingRuleService } = await import(
|
||||
"../services/numberingRuleService"
|
||||
);
|
||||
|
||||
// 1) inspection_result_mng / inspection_number 채번 규칙 조회
|
||||
const rule = await numberingRuleService.getNumberingRuleByColumn(
|
||||
companyCode,
|
||||
"inspection_result_mng",
|
||||
"inspection_number",
|
||||
);
|
||||
|
||||
if (rule && rule.ruleId) {
|
||||
// 2) PC API와 동일한 allocateCode 호출 → 실제 시퀀스 +1
|
||||
return await numberingRuleService.allocateCode(rule.ruleId, companyCode);
|
||||
}
|
||||
|
||||
// fallback: 채번 규칙 없으면 단순 SELECT MAX
|
||||
const { getPool } = await import("../database/db");
|
||||
const pool = getPool();
|
||||
const year = new Date().getFullYear();
|
||||
const prefix = `QI-${year}-`;
|
||||
const result = await pool.query(
|
||||
`SELECT inspection_number FROM inspection_result_mng
|
||||
WHERE company_code = $1 AND inspection_number LIKE $2
|
||||
ORDER BY inspection_number DESC LIMIT 1`,
|
||||
[companyCode, `${prefix}%`],
|
||||
);
|
||||
let nextSeq = 1;
|
||||
if (result.rows.length > 0) {
|
||||
const lastNumber = result.rows[0].inspection_number;
|
||||
const match = lastNumber.match(/(\d+)$/);
|
||||
if (match) nextSeq = parseInt(match[1], 10) + 1;
|
||||
}
|
||||
return `${prefix}${String(nextSeq).padStart(4, "0")}`;
|
||||
}
|
||||
|
||||
// ---- 검사번호 채번 전용 엔드포인트 (검사 모달에서 검사 완료 시) ----
|
||||
// POST /api/pop/inspection-result/allocate-number
|
||||
router.post("/allocate-number", async (req: Request, res: Response) => {
|
||||
const companyCode = (req as any).user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보 없음" });
|
||||
}
|
||||
try {
|
||||
const inspectionNumber = await generateInspectionNumber(companyCode);
|
||||
return res.json({ success: true, data: { inspectionNumber } });
|
||||
} catch (err: any) {
|
||||
return res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ---- 검사 결과 저장 (마스터 + 디테일 트랜잭션) ----
|
||||
// POST /api/pop/inspection-result
|
||||
router.post("/", async (req: Request, res: Response) => {
|
||||
const pool = getPool();
|
||||
const companyCode = (req as any).user?.companyCode;
|
||||
const writer = (req as any).user?.userId;
|
||||
const pool = getPool();
|
||||
const companyCode = (req as any).user?.companyCode;
|
||||
const writer = (req as any).user?.userId;
|
||||
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보 없음" });
|
||||
}
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 정보 없음" });
|
||||
}
|
||||
|
||||
const {
|
||||
referenceTable,
|
||||
referenceId,
|
||||
screenId,
|
||||
itemId,
|
||||
itemCode,
|
||||
itemName,
|
||||
inspectionType,
|
||||
items, // 검사 항목별 결과 배열
|
||||
overallJudgment,
|
||||
memo,
|
||||
isCompleted,
|
||||
} = req.body;
|
||||
const {
|
||||
inspectionNumber: providedNumber, // 프론트에서 미리 채번한 번호 (있으면 재사용)
|
||||
referenceTable,
|
||||
referenceId,
|
||||
screenId,
|
||||
itemId,
|
||||
itemCode,
|
||||
itemName,
|
||||
inspectionType,
|
||||
items, // 검사 항목별 결과 배열
|
||||
overallJudgment,
|
||||
totalQty,
|
||||
goodQty,
|
||||
badQty,
|
||||
defectDescription,
|
||||
memo,
|
||||
inspector,
|
||||
supplierCode,
|
||||
supplierName,
|
||||
isCompleted,
|
||||
} = req.body;
|
||||
|
||||
if (!items || !Array.isArray(items) || items.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "검사 항목이 없습니다" });
|
||||
}
|
||||
if (!items || !Array.isArray(items) || items.length === 0) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, message: "검사 항목이 없습니다" });
|
||||
}
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 기존 결과 삭제 (동일 referenceId + referenceTable 기준 덮어쓰기)
|
||||
if (referenceId && referenceTable) {
|
||||
await client.query(
|
||||
`DELETE FROM inspection_result
|
||||
// 1. 동일 referenceId + referenceTable 기존 마스터/디테일 삭제 (덮어쓰기)
|
||||
if (referenceId && referenceTable) {
|
||||
await client.query(
|
||||
`DELETE FROM inspection_result WHERE master_id IN (
|
||||
SELECT id FROM inspection_result_mng
|
||||
WHERE company_code = $1 AND reference_id = $2 AND reference_table = $3
|
||||
)`,
|
||||
[companyCode, referenceId, referenceTable],
|
||||
);
|
||||
await client.query(
|
||||
`DELETE FROM inspection_result_mng
|
||||
WHERE company_code = $1 AND reference_id = $2 AND reference_table = $3`,
|
||||
[companyCode, referenceId, referenceTable]
|
||||
);
|
||||
}
|
||||
[companyCode, referenceId, referenceTable],
|
||||
);
|
||||
}
|
||||
|
||||
const insertedIds: string[] = [];
|
||||
for (const item of items) {
|
||||
const completedFlag = isCompleted ? "Y" : "N";
|
||||
const completedDate = isCompleted ? new Date() : null;
|
||||
const insertSql = `
|
||||
INSERT INTO inspection_result (
|
||||
company_code, writer,
|
||||
// 2. 검사번호 (프론트에서 미리 받았으면 재사용, 없으면 새로 채번)
|
||||
const inspectionNumber =
|
||||
providedNumber || (await generateInspectionNumber(companyCode));
|
||||
|
||||
// 3. 마스터 INSERT
|
||||
const completedFlag = isCompleted ? "Y" : "N";
|
||||
const completedDate = isCompleted ? new Date() : null;
|
||||
const masterResult = await client.query(
|
||||
`INSERT INTO inspection_result_mng (
|
||||
company_code, writer, inspection_number,
|
||||
reference_table, reference_id, screen_id,
|
||||
item_id, item_code, item_name,
|
||||
inspection_type, total_qty, good_qty, bad_qty,
|
||||
overall_judgment, defect_description, memo,
|
||||
inspector, inspection_date,
|
||||
supplier_code, supplier_name,
|
||||
is_completed, completed_date
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, NOW(), $18, $19, $20, $21
|
||||
) RETURNING id, inspection_number`,
|
||||
[
|
||||
companyCode,
|
||||
writer,
|
||||
inspectionNumber,
|
||||
referenceTable || null,
|
||||
referenceId || null,
|
||||
screenId || null,
|
||||
itemId || null,
|
||||
itemCode || null,
|
||||
itemName || null,
|
||||
inspectionType || null,
|
||||
totalQty != null ? Number(totalQty) : null,
|
||||
goodQty != null ? Number(goodQty) : null,
|
||||
badQty != null ? Number(badQty) : null,
|
||||
overallJudgment || null,
|
||||
defectDescription || null,
|
||||
memo || null,
|
||||
inspector || writer,
|
||||
supplierCode || null,
|
||||
supplierName || null,
|
||||
completedFlag,
|
||||
completedDate,
|
||||
],
|
||||
);
|
||||
const masterId = masterResult.rows[0].id;
|
||||
|
||||
// 4. 디테일 N건 INSERT
|
||||
const insertedDetailIds: string[] = [];
|
||||
for (const item of items) {
|
||||
const detailResult = await client.query(
|
||||
`INSERT INTO inspection_result (
|
||||
company_code, writer, master_id,
|
||||
reference_table, reference_id, screen_id,
|
||||
inspection_info_id, item_id, item_code, item_name,
|
||||
inspection_type, inspection_item_name, inspection_standard, pass_criteria, is_required,
|
||||
measured_value, judgment, overall_judgment, memo,
|
||||
is_completed, completed_date
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20
|
||||
)
|
||||
RETURNING id
|
||||
`;
|
||||
const result = await client.query(insertSql, [
|
||||
companyCode,
|
||||
writer,
|
||||
referenceTable || null,
|
||||
referenceId || null,
|
||||
screenId || null,
|
||||
item.inspectionInfoId || null,
|
||||
itemId || item.itemId || null,
|
||||
itemCode || item.itemCode || null,
|
||||
itemName || item.itemName || null,
|
||||
inspectionType || item.inspectionType || null,
|
||||
item.inspectionItemName || null,
|
||||
item.inspectionStandard || null,
|
||||
item.passCriteria || null,
|
||||
item.isRequired || "Y",
|
||||
item.measuredValue || null,
|
||||
item.judgment || null,
|
||||
overallJudgment || null,
|
||||
memo || null,
|
||||
completedFlag,
|
||||
completedDate,
|
||||
]);
|
||||
insertedIds.push(result.rows[0].id);
|
||||
}
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21
|
||||
) RETURNING id`,
|
||||
[
|
||||
companyCode,
|
||||
writer,
|
||||
masterId,
|
||||
referenceTable || null,
|
||||
referenceId || null,
|
||||
screenId || null,
|
||||
item.inspectionInfoId || null,
|
||||
itemId || item.itemId || null,
|
||||
itemCode || item.itemCode || null,
|
||||
itemName || item.itemName || null,
|
||||
inspectionType || item.inspectionType || null,
|
||||
item.inspectionItemName || null,
|
||||
item.inspectionStandard || null,
|
||||
item.passCriteria || null,
|
||||
item.isRequired || "Y",
|
||||
item.measuredValue || null,
|
||||
item.judgment || null,
|
||||
overallJudgment || null,
|
||||
memo || null,
|
||||
completedFlag,
|
||||
completedDate,
|
||||
],
|
||||
);
|
||||
insertedDetailIds.push(detailResult.rows[0].id);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
return res.json({ success: true, data: { ids: insertedIds } });
|
||||
} catch (err: any) {
|
||||
await client.query("ROLLBACK");
|
||||
return res.status(500).json({ success: false, message: err.message });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
await client.query("COMMIT");
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
masterId,
|
||||
inspectionNumber,
|
||||
detailIds: insertedDetailIds,
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
await client.query("ROLLBACK");
|
||||
return res.status(500).json({ success: false, message: err.message });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user