feat: POP 시연 준비 — 5개 화면 + 버그 수정 + 재고검증
This commit is contained in:
@@ -179,6 +179,23 @@ export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
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 || '']
|
||||
);
|
||||
const currentStock = parseFloat(stockCheck.rows[0]?.cur || '0');
|
||||
if (currentStock < outQty) {
|
||||
throw new Error(
|
||||
`재고 부족: ${item.item_name || itemCode} (창고 ${whCode || '미지정'}) — 현재 재고 ${currentStock}, 요청 출고 ${outQty}`
|
||||
);
|
||||
}
|
||||
|
||||
const existingStock = await client.query(
|
||||
`SELECT id FROM inventory_stock
|
||||
WHERE company_code = $1 AND item_code = $2
|
||||
|
||||
@@ -1157,21 +1157,30 @@ export const saveResult = async (
|
||||
}
|
||||
|
||||
if (shouldActivateNext) {
|
||||
// 다음 seq 활성화 (completed도 재활성화 — 새 양품이 들어오면 추가 접수 가능)
|
||||
const nextSeq = String(seqNum + 1);
|
||||
const nextUpdate = await pool.query(
|
||||
`UPDATE work_order_process
|
||||
SET status = 'acceptable',
|
||||
updated_date = NOW()
|
||||
WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
|
||||
AND parent_process_id IS NULL
|
||||
RETURNING id, process_name, status`,
|
||||
[wo_id, nextSeq, companyCode]
|
||||
// 다음 seq 활성화 (seq_no 비순차 대응: seqNum+1이 아니라 "현재보다 큰 가장 작은 seq_no")
|
||||
const nextSeqQuery = await pool.query(
|
||||
`SELECT MIN(CAST(seq_no AS int)) as next_seq
|
||||
FROM work_order_process
|
||||
WHERE wo_id = $1 AND company_code = $2 AND parent_process_id IS NULL
|
||||
AND CAST(seq_no AS int) > $3`,
|
||||
[wo_id, companyCode, seqNum]
|
||||
);
|
||||
if (nextUpdate.rowCount > 0) {
|
||||
logger.info("[pop/production] 다음 공정 상태 전환", {
|
||||
nextProcess: nextUpdate.rows[0],
|
||||
});
|
||||
const actualNextSeq = nextSeqQuery.rows[0]?.next_seq;
|
||||
if (actualNextSeq != null) {
|
||||
const nextUpdate = await pool.query(
|
||||
`UPDATE work_order_process
|
||||
SET status = 'acceptable',
|
||||
updated_date = NOW()
|
||||
WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
|
||||
AND parent_process_id IS NULL
|
||||
RETURNING id, process_name, status`,
|
||||
[wo_id, String(actualNextSeq), companyCode]
|
||||
);
|
||||
if (nextUpdate.rowCount > 0) {
|
||||
logger.info("[pop/production] 다음 공정 상태 전환", {
|
||||
nextProcess: nextUpdate.rows[0],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1672,17 +1681,37 @@ export const getAvailableQty = async (req: AuthenticatedRequest, res: Response)
|
||||
myInputQty = parseInt(totalAccepted.rows[0].total_input, 10) || 0;
|
||||
|
||||
prevGoodQty = instrQty;
|
||||
if (seqNum > 1) {
|
||||
const prevSeq = String(seqNum - 1);
|
||||
const prevProcess = await pool.query(
|
||||
`SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as total_good
|
||||
// 첫 공정 여부를 seq_no==1이 아니라 "이 공정보다 작은 seq_no가 있는지"로 판단
|
||||
// (라우팅 seq_no가 1, 2, 3이 아니라 10, 20, 30 같은 비순차여도 정상 동작)
|
||||
const minSeqCheck = await pool.query(
|
||||
`SELECT MIN(CAST(seq_no AS int)) as min_seq
|
||||
FROM work_order_process
|
||||
WHERE wo_id = $1 AND company_code = $2 AND parent_process_id IS NULL`,
|
||||
[wo_id, companyCode]
|
||||
);
|
||||
const minSeq = parseInt(minSeqCheck.rows[0]?.min_seq, 10) || seqNum;
|
||||
const isFirstProcess = seqNum <= minSeq;
|
||||
if (!isFirstProcess) {
|
||||
// 이전 공정 찾기 (seq_no가 더 작은 가장 가까운 공정)
|
||||
const prevProcessSeq = await pool.query(
|
||||
`SELECT MAX(CAST(seq_no AS int)) as prev_seq
|
||||
FROM work_order_process
|
||||
WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
|
||||
AND parent_process_id IS NOT NULL`,
|
||||
[wo_id, prevSeq, companyCode]
|
||||
WHERE wo_id = $1 AND company_code = $2 AND parent_process_id IS NULL
|
||||
AND CAST(seq_no AS int) < $3`,
|
||||
[wo_id, companyCode, seqNum]
|
||||
);
|
||||
if (prevProcess.rowCount > 0) {
|
||||
prevGoodQty = parseInt(prevProcess.rows[0].total_good, 10) || 0;
|
||||
const actualPrevSeq = prevProcessSeq.rows[0]?.prev_seq;
|
||||
if (actualPrevSeq != null) {
|
||||
const prevProcess = await pool.query(
|
||||
`SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as total_good
|
||||
FROM work_order_process
|
||||
WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
|
||||
AND parent_process_id IS NOT NULL`,
|
||||
[wo_id, String(actualPrevSeq), companyCode]
|
||||
);
|
||||
if (prevProcess.rowCount > 0) {
|
||||
prevGoodQty = parseInt(prevProcess.rows[0].total_good, 10) || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
availableQty = Math.max(0, prevGoodQty - myInputQty);
|
||||
@@ -1848,8 +1877,26 @@ export const acceptProcess = async (req: AuthenticatedRequest, res: Response) =>
|
||||
currentTotalInput = parseInt(totalAccepted.rows[0].total_input, 10) || 0;
|
||||
|
||||
prevGoodQty = instrQty;
|
||||
if (seqNum > 1) {
|
||||
const prevSeq = String(seqNum - 1);
|
||||
// 첫 공정 여부를 seq_no==1이 아니라 "이 공정보다 작은 seq_no가 있는지"로 판단
|
||||
const minSeqCheck = await client.query(
|
||||
`SELECT MIN(CAST(seq_no AS int)) as min_seq
|
||||
FROM work_order_process
|
||||
WHERE wo_id = $1 AND company_code = $2 AND parent_process_id IS NULL`,
|
||||
[row.wo_id, companyCode]
|
||||
);
|
||||
const minSeq = parseInt(minSeqCheck.rows[0]?.min_seq, 10) || seqNum;
|
||||
const isFirstProcess = seqNum <= minSeq;
|
||||
if (!isFirstProcess) {
|
||||
// 이전 공정 = 이 공정보다 작은 seq_no 중 가장 큰 값
|
||||
const prevProcessSeq = await client.query(
|
||||
`SELECT MAX(CAST(seq_no AS int)) as prev_seq
|
||||
FROM work_order_process
|
||||
WHERE wo_id = $1 AND company_code = $2 AND parent_process_id IS NULL
|
||||
AND CAST(seq_no AS int) < $3`,
|
||||
[row.wo_id, companyCode, seqNum]
|
||||
);
|
||||
const actualPrevSeq = prevProcessSeq.rows[0]?.prev_seq;
|
||||
const prevSeq = actualPrevSeq != null ? String(actualPrevSeq) : String(seqNum - 1);
|
||||
const prevProcess = await client.query(
|
||||
`SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as total_good
|
||||
FROM work_order_process
|
||||
|
||||
@@ -689,7 +689,13 @@ export async function getPurchaseOrders(req: AuthenticatedRequest, res: Response
|
||||
COALESCE(CAST(NULLIF(pd.unit_price, '') AS numeric), 0) AS unit_price,
|
||||
COALESCE(po.status, '') AS status,
|
||||
COALESCE(pd.due_date, po.due_date) AS due_date,
|
||||
'purchase_detail' AS source_table
|
||||
'purchase_detail' AS source_table,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1 FROM item_inspection_info iii
|
||||
WHERE iii.company_code = pd.company_code
|
||||
AND COALESCE(iii.is_active, 'Y') = 'Y'
|
||||
AND iii.item_code = COALESCE(NULLIF(pd.item_code, ''), ii.item_number)
|
||||
) THEN 'self' ELSE NULL END AS inspection_type
|
||||
FROM purchase_detail pd
|
||||
LEFT JOIN purchase_order_mng po
|
||||
ON pd.purchase_no = po.purchase_no AND pd.company_code = po.company_code
|
||||
@@ -722,7 +728,13 @@ export async function getPurchaseOrders(req: AuthenticatedRequest, res: Response
|
||||
COALESCE(CAST(NULLIF(po.unit_price, '') AS numeric), 0) AS unit_price,
|
||||
po.status,
|
||||
po.due_date,
|
||||
'purchase_order_mng' AS source_table
|
||||
'purchase_order_mng' AS source_table,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1 FROM item_inspection_info iii
|
||||
WHERE iii.company_code = po.company_code
|
||||
AND COALESCE(iii.is_active, 'Y') = 'Y'
|
||||
AND iii.item_code = po.item_code
|
||||
) THEN 'self' ELSE NULL END AS inspection_type
|
||||
FROM purchase_order_mng po
|
||||
WHERE po.company_code = $1
|
||||
AND NOT EXISTS (
|
||||
|
||||
@@ -94,7 +94,59 @@ router.get("/", async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---- 검사 결과 저장 (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();
|
||||
@@ -106,6 +158,7 @@ router.post("/", async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
const {
|
||||
inspectionNumber: providedNumber, // 프론트에서 미리 채번한 번호 (있으면 재사용)
|
||||
referenceTable,
|
||||
referenceId,
|
||||
screenId,
|
||||
@@ -115,7 +168,14 @@ router.post("/", async (req: Request, res: Response) => {
|
||||
inspectionType,
|
||||
items, // 검사 항목별 결과 배열
|
||||
overallJudgment,
|
||||
totalQty,
|
||||
goodQty,
|
||||
badQty,
|
||||
defectDescription,
|
||||
memo,
|
||||
inspector,
|
||||
supplierCode,
|
||||
supplierName,
|
||||
isCompleted,
|
||||
} = req.body;
|
||||
|
||||
@@ -127,59 +187,117 @@ router.post("/", async (req: Request, res: Response) => {
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 기존 결과 삭제 (동일 referenceId + referenceTable 기준 덮어쓰기)
|
||||
// 1. 동일 referenceId + referenceTable 기존 마스터/디테일 삭제 (덮어쓰기)
|
||||
if (referenceId && referenceTable) {
|
||||
await client.query(
|
||||
`DELETE FROM inspection_result
|
||||
`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]
|
||||
);
|
||||
}
|
||||
|
||||
const insertedIds: string[] = [];
|
||||
// 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 completedFlag = isCompleted ? "Y" : "N";
|
||||
const completedDate = isCompleted ? new Date() : null;
|
||||
const insertSql = `
|
||||
INSERT INTO inspection_result (
|
||||
company_code, writer,
|
||||
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 } });
|
||||
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 });
|
||||
|
||||
Reference in New Issue
Block a user