feat(WEB): 작업자화면 중간검사 자동판정 로직 추가

- InspectionInputModal: 공정별(screen/slat/bending) 자동 판정 계산 로직
- JudgmentToggle → JudgmentDisplay로 변경 (수동 → 자동 판정)
- computeJudgment: 검사 항목 상태 기반 적합/부적합 자동 산출
- index.tsx: 관련 타입/로직 보강
This commit is contained in:
2026-02-09 21:31:07 +09:00
parent 6d8116713f
commit 2f3f7a486c
2 changed files with 83 additions and 26 deletions

View File

@@ -11,7 +11,7 @@
* - bending_wip: 재고생산(재공품) 중간검사
*/
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo } from 'react';
import {
Dialog,
DialogContent,
@@ -149,44 +149,81 @@ function OkNgToggle({
);
}
// 적합/부적합 버튼 컴포넌트
function JudgmentToggle({
value,
onChange,
}: {
value: 'pass' | 'fail' | null;
onChange: (v: 'pass' | 'fail') => void;
}) {
// 자동 판정 표시 컴포넌트
function JudgmentDisplay({ value }: { value: 'pass' | 'fail' | null }) {
return (
<div className="flex gap-2 flex-1">
<button
type="button"
onClick={() => onChange('pass')}
<div
className={cn(
'flex-1 py-2.5 rounded-lg text-sm font-bold transition-colors',
'flex-1 py-2.5 rounded-lg text-sm font-bold text-center transition-colors',
value === 'pass'
? 'bg-orange-600 text-white'
: 'bg-gray-100 text-gray-700 border border-gray-300 hover:bg-gray-200'
: 'bg-gray-100 text-gray-400 border border-gray-300'
)}
>
</button>
<button
type="button"
onClick={() => onChange('fail')}
</div>
<div
className={cn(
'flex-1 py-2.5 rounded-lg text-sm font-bold transition-colors',
'flex-1 py-2.5 rounded-lg text-sm font-bold text-center transition-colors',
value === 'fail'
? 'bg-black text-white'
: 'bg-gray-100 text-gray-700 border border-gray-300 hover:bg-gray-200'
: 'bg-gray-100 text-gray-400 border border-gray-300'
)}
>
</button>
</div>
</div>
);
}
// 공정별 자동 판정 계산
function hasMeasurement(v: number | null | undefined): boolean {
return v != null;
}
function computeJudgment(processType: InspectionProcessType, data: InspectionData): 'pass' | 'fail' | null {
switch (processType) {
case 'screen': {
const { processingStatus, sewingStatus, assemblyStatus, gapStatus, length, width } = data;
// 불량이 하나라도 있으면 즉시 부적합
if (processingStatus === 'bad' || sewingStatus === 'bad' || assemblyStatus === 'bad' || gapStatus === 'ng') return 'fail';
// 모든 상태 양호 + 측정값 입력 완료 시 적합
if (processingStatus === 'good' && sewingStatus === 'good' && assemblyStatus === 'good' && gapStatus === 'ok'
&& hasMeasurement(length) && hasMeasurement(width)) return 'pass';
return null;
}
case 'slat': {
const { processingStatus, assemblyStatus, height1, height2, length } = data;
if (processingStatus === 'bad' || assemblyStatus === 'bad') return 'fail';
if (processingStatus === 'good' && assemblyStatus === 'good'
&& hasMeasurement(height1) && hasMeasurement(height2) && hasMeasurement(length)) return 'pass';
return null;
}
case 'slat_jointbar': {
const { processingStatus, assemblyStatus, height1, height2, length3, gap4 } = data;
if (processingStatus === 'bad' || assemblyStatus === 'bad') return 'fail';
if (processingStatus === 'good' && assemblyStatus === 'good'
&& hasMeasurement(height1) && hasMeasurement(height2) && hasMeasurement(length3) && hasMeasurement(gap4)) return 'pass';
return null;
}
case 'bending': {
const { bendingStatus, length } = data;
if (bendingStatus === 'bad') return 'fail';
if (bendingStatus === 'good' && hasMeasurement(length)) return 'pass';
return null;
}
case 'bending_wip': {
const { bendingStatus, length, width } = data;
if (bendingStatus === 'bad') return 'fail';
if (bendingStatus === 'good' && hasMeasurement(length) && hasMeasurement(width)) return 'pass';
return null;
}
default:
return null;
}
}
export function InspectionInputModal({
open,
onOpenChange,
@@ -278,6 +315,17 @@ export function InspectionInputModal({
}
}, [open, productName, specification, processType, initialData]);
// 자동 판정 계산
const autoJudgment = useMemo(() => computeJudgment(processType, formData), [processType, formData]);
// 판정값 자동 동기화
useEffect(() => {
setFormData((prev) => {
if (prev.judgment === autoJudgment) return prev;
return { ...prev, judgment: autoJudgment };
});
}, [autoJudgment]);
const handleComplete = () => {
const data: InspectionData = {
...formData,
@@ -642,11 +690,8 @@ export function InspectionInputModal({
{/* 하단 고정: 판정 + 버튼 */}
<div className="shrink-0 border-t bg-white px-5 pb-5 pt-3 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]">
<div className="space-y-2 mb-4">
<span className="text-sm font-bold"></span>
<JudgmentToggle
value={formData.judgment}
onChange={(v) => setFormData((prev) => ({ ...prev, judgment: v }))}
/>
<span className="text-sm font-bold"> ()</span>
<JudgmentDisplay value={autoJudgment} />
</div>
<div className="flex gap-3">
<Button

View File

@@ -455,6 +455,7 @@ export default function WorkerScreen() {
try {
const result = await getWorkOrderInspectionData(selectedSidebarOrderId);
if (result.success && result.data?.items) {
const completionUpdates: Record<string, boolean> = {};
setInspectionDataMap((prev) => {
const next = new Map(prev);
for (const apiItem of result.data!.items) {
@@ -463,10 +464,17 @@ export default function WorkerScreen() {
const match = workItems.find((w) => w.apiItemId === apiItem.item_id);
if (match) {
next.set(match.id, apiItem.inspection_data as unknown as InspectionData);
// 중간검사 step 완료 처리
const stepKey = match.id.replace('-node-', '-') + '-중간검사';
completionUpdates[stepKey] = true;
}
}
return next;
});
// stepCompletionMap 일괄 업데이트
if (Object.keys(completionUpdates).length > 0) {
setStepCompletionMap((prev) => ({ ...prev, ...completionUpdates }));
}
}
} catch {
// 검사 데이터 로드 실패는 무시 (새 작업일 수 있음)
@@ -906,6 +914,10 @@ export default function WorkerScreen() {
return next;
});
// 중간검사 step 완료 처리
const stepKey = selectedOrder.id.replace('-node-', '-') + '-중간검사';
setStepCompletionMap((prev) => ({ ...prev, [stepKey]: true }));
// 실제 API item인 경우 서버에 저장
const targetItem = workItems.find((w) => w.id === selectedOrder.id);
if (targetItem?.apiItemId && targetItem?.workOrderId) {