feat(WEB): 작업자화면 중간검사 자동판정 로직 추가
- InspectionInputModal: 공정별(screen/slat/bending) 자동 판정 계산 로직 - JudgmentToggle → JudgmentDisplay로 변경 (수동 → 자동 판정) - computeJudgment: 검사 항목 상태 기반 적합/부적합 자동 산출 - index.tsx: 관련 타입/로직 보강
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user