fix: key 중복 에러 + 다중공정 활성화 순서 버그

1. WorkOrderList: card key를 id+status+parent로 유니크화
2. saveResult: 분할행 완료+마스터 캐스케이드를 다음공정 활성화보다 먼저 실행
   - 기존: 다음공정 활성화 → 분할행 완료 (마스터가 아직 미완료)
   - 수정: 분할행 완료 → 마스터 완료 → 다음공정 활성화 (정확한 상태 기반)
This commit is contained in:
SeongHyun Kim
2026-04-03 15:08:24 +09:00
parent e4b31999f9
commit e4fc2a04df
2 changed files with 46 additions and 81 deletions

View File

@@ -955,6 +955,50 @@ export const saveResult = async (
}
}
// 개별 분할 행 자동완료 (다음 공정 활성화보다 먼저 실행)
if (currentSeq.rowCount > 0) {
const { seq_no: csSeq, wo_id: csWoId, current_input_qty: csInputQty, instruction_qty: csInstrQty, parent_process_id: csParentId } = currentSeq.rows[0];
const csMyInput = parseInt(csInputQty, 10) || 0;
if (newTotal >= csMyInput && csMyInput > 0) {
await pool.query(
`UPDATE work_order_process SET status = 'completed', result_status = 'confirmed',
completed_at = NOW()::text, completed_by = $3, updated_date = NOW()
WHERE id = $1 AND company_code = $2 AND status != 'completed'`,
[work_order_process_id, companyCode, userId]
);
// 같은 seq의 모든 분할 행 완료 체크 → 마스터도 completed
const csSeqNum = parseInt(csSeq, 10);
let csPrevGood = parseInt(csInstrQty, 10) || 0;
if (csSeqNum > 1) {
const prev = await pool.query(
`SELECT COALESCE(SUM(good_qty::int), 0) + COALESCE(SUM(concession_qty::int), 0) as tg
FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3`,
[csWoId, String(csSeqNum - 1), companyCode]
);
if (prev.rowCount > 0) csPrevGood = parseInt(prev.rows[0].tg, 10) || 0;
}
const sibCheck = await pool.query(
`SELECT COALESCE(SUM(input_qty::int), 0) as ti, COUNT(*) FILTER (WHERE status != 'completed') as ic
FROM work_order_process WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3 AND parent_process_id IS NOT NULL`,
[csWoId, csSeq, companyCode]
);
const csTotalInput = parseInt(sibCheck.rows[0].ti, 10) || 0;
const csIncomplete = parseInt(sibCheck.rows[0].ic, 10) || 0;
if (csIncomplete === 0 && csPrevGood - csTotalInput <= 0 && csParentId) {
await pool.query(
`UPDATE work_order_process SET status = 'completed', result_status = 'confirmed',
completed_at = NOW()::text, completed_by = $3, updated_date = NOW()
WHERE id = $1 AND company_code = $2 AND status != 'completed'`,
[csParentId, companyCode, userId]
);
}
}
await checkAndCompleteWorkInstruction(pool, csWoId, companyCode, userId);
}
// 다음 공정 활성화 (다중공정 대응)
// is_fixed_order='Y' 그룹이면 그룹 전체 완료 후 다음 활성화
if (addGood > 0 && currentSeq.rowCount > 0) {
@@ -1035,86 +1079,7 @@ export const saveResult = async (
}
}
// 개별 분할 자동완료: 이 분할 행의 접수분 전량 생산 시 completed
if (currentSeq.rowCount > 0) {
const { seq_no, wo_id, current_input_qty, instruction_qty } = currentSeq.rows[0];
const myInputQty = parseInt(current_input_qty, 10) || 0;
if (newTotal >= myInputQty && myInputQty > 0) {
await pool.query(
`UPDATE work_order_process
SET status = 'completed',
result_status = 'confirmed',
completed_at = NOW()::text,
completed_by = $3,
updated_date = NOW()
WHERE id = $1 AND company_code = $2
AND status != 'completed'`,
[work_order_process_id, companyCode, userId]
);
logger.info("[pop/production] 분할 행 자동 완료", {
work_order_process_id, newTotal, myInputQty,
});
// 같은 공정의 모든 분할 행이 completed인지 체크 -> 원본도 completed로
const seqNum = parseInt(seq_no, 10);
const instrQty = parseInt(instruction_qty, 10) || 0;
// 앞공정 양품 합산 (접수가능 잔여 계산용)
let 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
FROM work_order_process
WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3`,
[wo_id, prevSeq, companyCode]
);
if (prevProcess.rowCount > 0) {
prevGoodQty = parseInt(prevProcess.rows[0].total_good, 10) || 0;
}
}
// 같은 seq_no의 모든 분할 행 접수량 합산 + 미완료 행 카운트
const siblingCheck = await pool.query(
`SELECT
COALESCE(SUM(input_qty::int), 0) as total_input,
COUNT(*) FILTER (WHERE status != 'completed') as incomplete_count
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, seq_no, companyCode]
);
const totalInput = parseInt(siblingCheck.rows[0].total_input, 10) || 0;
const incompleteCount = parseInt(siblingCheck.rows[0].incomplete_count, 10) || 0;
const remainingAcceptable = prevGoodQty - totalInput;
// 모든 분할 행 완료 + 잔여 접수가능 0 -> 원본(마스터)도 completed
if (incompleteCount === 0 && remainingAcceptable <= 0) {
const masterId = currentSeq.rows[0].parent_process_id;
if (masterId) {
await pool.query(
`UPDATE work_order_process
SET status = 'completed',
result_status = 'confirmed',
completed_at = NOW()::text,
completed_by = $3,
updated_date = NOW()
WHERE id = $1 AND company_code = $2
AND status != 'completed'`,
[masterId, companyCode, userId]
);
logger.info("[pop/production] 원본(마스터) 공정 자동 완료", {
masterId, totalInput, prevGoodQty,
});
}
}
}
// 작업지시 전체 완료 판정
const { wo_id: woIdForWi } = currentSeq.rows[0];
await checkAndCompleteWorkInstruction(pool, woIdForWi, companyCode, userId);
// (분할행 완료 + 마스터 캐스케이드는 위에서 이미 처리됨)
}
logger.info("[pop/production] save-result 완료 (누적)", {

View File

@@ -1182,7 +1182,7 @@ export function WorkOrderList() {
const additionalAvailable = Math.max(0, planQty - inputQty);
return (
<div key={proc.id} className="card-item">
<div key={`${proc.id}-${proc.status}-${proc.parent_process_id || "m"}`} className="card-item">
<div
className={`bg-white rounded-2xl border-l-4 ${borderLeft} border border-gray-100 shadow-sm overflow-hidden flex flex-col ${
proc.status === "waiting" ? "opacity-75" : ""