feat: 리워크 추적 시스템 + 합류 판정 + 버그 12건 수정

리워크 추적:
- 다음 공정 접수 시 리워크 자동 감지 (rework_source_id별 개별 추적)
- 합류 판정: 일반 물량 있으면 마크 해제(합류), 없으면 마크 유지
- 창고 입고 시 마크 자동 해제 (is_rework=NULL, 이력 보존)
- 접수가능 카드: 합류 불가 시 리워크 배지 표시

버그 수정:
- 접수가능 이중 집계 (master+SPLIT 합산 → SPLIT만)
- completed master 재활성화
- 리워크 접수 불가 (자체 input 기반으로 변경)
- master input_qty 덮어쓰기 방지
- 리워크 카드A 접수 시 카드B 사라짐 (개별 전량 판정)
- 진행중/완료 탭 마스터 행 제외
- 리워크 진행중 카드 중복 (마스터 숨김, SPLIT만 표시)
- 리워크 SPLIT에 is_rework 전달
- 재작업 회차 이중 카운트 (마스터만 카운트)
- reworkAvailable이 available 초과 (clamp 처리)
- 불량 수량 키패드 기본값 빈값
- 불량 처리 공정선택 UI 연결

테스트 검증: 전체 시나리오 PASS (집계  흐름  재고  마크 )
This commit is contained in:
SeongHyun Kim
2026-04-06 22:27:23 +09:00
parent e94d298997
commit 15029dc58d
3 changed files with 168 additions and 41 deletions

View File

@@ -1696,6 +1696,41 @@ export const getAvailableQty = async (req: AuthenticatedRequest, res: Response)
instructionQty: instrQty,
});
// 앞공정에서 리워크로 완료된 양품 수량 (마크 표시용)
// rework_source_id별로 개별 추적하여 정확한 미소진 리워크 수량 계산
let reworkAvailableQty = 0;
if (!isRework && seqNum > 1) {
const prevSeq = String(seqNum - 1);
// 앞공정의 리워크 완료 SPLIT들 (rework_source_id별)
const reworkSplits = await pool.query(
`SELECT rework_source_id, COALESCE(SUM(good_qty::int), 0) as rg
FROM work_order_process
WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
AND parent_process_id IS NOT NULL
AND is_rework = 'Y'
AND status = 'completed'
AND good_qty::int > 0
GROUP BY rework_source_id`,
[wo_id, prevSeq, companyCode]
);
// 현재 공정에서 각 rework_source_id별로 소비된 수량
for (const rs of reworkSplits.rows) {
const srcId = rs.rework_source_id;
const srcGood = parseInt(rs.rg, 10) || 0;
const consumedResult = await pool.query(
`SELECT COALESCE(SUM(input_qty::int), 0) as consumed
FROM work_order_process
WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
AND parent_process_id IS NOT NULL
AND is_rework = 'Y'
AND rework_source_id = $4`,
[wo_id, seq_no, companyCode, srcId]
);
const consumed = parseInt(consumedResult.rows[0]?.consumed, 10) || 0;
reworkAvailableQty += Math.max(0, srcGood - consumed);
}
}
return res.json({
success: true,
data: {
@@ -1703,6 +1738,7 @@ export const getAvailableQty = async (req: AuthenticatedRequest, res: Response)
myInputQty,
availableQty,
instructionQty: instrQty,
reworkAvailableQty, // 리워크 물량 포함 수량
},
});
} catch (error: any) {
@@ -1841,32 +1877,65 @@ export const acceptProcess = async (req: AuthenticatedRequest, res: Response) =>
const batchId = req.body.batch_id || `BATCH-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
const hasBatchCol = _batchMigrationDone;
// 리워크 정보 전달: 리워크 카드 접수 또는 프론트에서 전달한 경우
// 리워크 정보 전달: 리워크 카드 접수 / 프론트 전달 / 자동 감지
let splitIsRework: string | null = null;
let splitReworkSourceId: string | null = null;
if (isRework) {
// 리워크 카드에서 직접 접수
// 케이스 1: 리워크 카드에서 직접 접수
const parentReworkInfo = await client.query(
`SELECT is_rework, rework_source_id FROM work_order_process WHERE id = $1`, [work_order_process_id]
);
splitIsRework = parentReworkInfo?.rows[0]?.is_rework || null;
splitReworkSourceId = parentReworkInfo?.rows[0]?.rework_source_id || null;
} else if (req.body.rework_source_id) {
// 프론트에서 리워크 추적 정보 전달 (다음 공정 접수 시)
// 원본 불량 공정의 seq_no를 조회하여 현재 공정과 비교
const originProc = await client.query(
`SELECT seq_no FROM work_order_process WHERE id = $1`, [req.body.rework_source_id]
// 케이스 2: 프론트에서 리워크 추적 정보 전달
splitIsRework = "Y";
splitReworkSourceId = req.body.rework_source_id;
} else if (seqNum > 1) {
// 케이스 3: 자동 감지 — 앞공정에서 리워크로 완료된 양품이 있는지 확인
const prevSeq = String(seqNum - 1);
// rework_source_id별로 개별 추적
const prevReworkSplits = await client.query(
`SELECT rework_source_id, COALESCE(SUM(good_qty::int), 0) as rework_good
FROM work_order_process
WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
AND parent_process_id IS NOT NULL
AND is_rework = 'Y'
AND status = 'completed'
AND good_qty::int > 0
GROUP BY rework_source_id`,
[row.wo_id, prevSeq, companyCode]
);
const originSeq = parseInt(originProc.rows[0]?.seq_no, 10) || 0;
const currentSeqNum = parseInt(row.seq_no, 10);
if (currentSeqNum < originSeq) {
// 아직 원본 공정에 도달 안 함 → 리워크 마크 유지
splitIsRework = "Y";
splitReworkSourceId = req.body.rework_source_id;
// 각 rework_source별로 미소진 수량 확인
for (const rs of prevReworkSplits.rows) {
const srcId = rs.rework_source_id;
const srcGood = parseInt(rs.rework_good, 10) || 0;
const consumedResult = await client.query(
`SELECT COALESCE(SUM(input_qty::int), 0) as consumed
FROM work_order_process
WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
AND parent_process_id IS NOT NULL
AND is_rework = 'Y'
AND rework_source_id = $4`,
[row.wo_id, row.seq_no, companyCode, srcId]
);
const consumed = parseInt(consumedResult.rows[0]?.consumed, 10) || 0;
const remaining = srcGood - consumed;
if (remaining > 0 && qty <= remaining) {
// 합류 판정: 일반 물량이 있으면 합류(마크 없음), 없으면 마크 부착
const normalAvailable = availableQty - remaining;
if (normalAvailable <= 0) {
// 일반 물량 없음 → 합류 불가 → 리워크 마크
splitIsRework = "Y";
splitReworkSourceId = srcId;
}
// normalAvailable > 0 → 합류 가능 → 마크 없음 (splitIsRework = null)
break;
}
}
// currentSeqNum >= originSeq → 원본 공정 도달 → 마크 해제 (splitIsRework = null)
}
// 분할 행 INSERT (batch_id는 컬럼 존재 시에만, 리워크 정보 포함)
@@ -1914,8 +1983,16 @@ export const acceptProcess = async (req: AuthenticatedRequest, res: Response) =>
);
} else {
newTotalInput = currentTotalInput; // 리워크는 기존 합계 유지
// 리워크 카드: 전량 접수 시 status를 completed로 변경 (추가 접수 방지 + 탭에서 숨김)
if (qty >= reworkInputQty) {
// 리워크 카드: 전량 접수 시에만 이 카드만 completed로 변경
// (다른 리워크 카드에 영향 없도록 id 정확히 지정)
const reworkAlreadyAccepted = await client.query(
`SELECT COALESCE(SUM(input_qty::int), 0) as total
FROM work_order_process
WHERE parent_process_id = $1 AND company_code = $2`,
[work_order_process_id, companyCode]
);
const totalReworkAccepted = (parseInt(reworkAlreadyAccepted.rows[0]?.total, 10) || 0) + qty;
if (totalReworkAccepted >= reworkInputQty) {
await client.query(
`UPDATE work_order_process SET status = 'completed', updated_date = NOW()
WHERE id = $1 AND company_code = $2`,
@@ -2352,7 +2429,7 @@ export const inventoryInbound = async (
// 1. work_order_process에서 wo_id, good_qty, parent_process_id, 기존 target_warehouse_id 조회
const procResult = await client.query(
`SELECT wo_id, good_qty, concession_qty, parent_process_id, target_warehouse_id, seq_no
`SELECT wo_id, good_qty, concession_qty, parent_process_id, target_warehouse_id, seq_no, is_rework
FROM work_order_process
WHERE id = $1 AND company_code = $2`,
[work_order_process_id, companyCode]
@@ -2442,12 +2519,22 @@ export const inventoryInbound = async (
);
}
// 6. 리워크 마크 해제 (창고 입고 = 정상 제품 인정, 이력은 rework_source_id에 영구 보존)
if (proc.is_rework === "Y") {
await client.query(
`UPDATE work_order_process SET is_rework = NULL, updated_date = NOW()
WHERE id = $1 AND company_code = $2`,
[work_order_process_id, companyCode]
);
}
await client.query("COMMIT");
logger.info("[pop/production] 독립 재고 입고 완료", {
companyCode, userId, work_order_process_id,
itemCode, warehouse_code, location_code: effectiveLocationCode,
qty: goodQty,
reworkCleared: proc.is_rework === "Y",
});
return res.json({