feat(WEB): 작업자화면 중간검사 자동판정 로직 추가
- InspectionInputModal: 공정별(screen/slat/bending) 자동 판정 계산 로직 - JudgmentToggle → JudgmentDisplay로 변경 (수동 → 자동 판정) - computeJudgment: 검사 항목 상태 기반 적합/부적합 자동 산출 - index.tsx: 관련 타입/로직 보강
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
* - bending_wip: 재고생산(재공품) 중간검사
|
* - bending_wip: 재고생산(재공품) 중간검사
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -149,44 +149,81 @@ function OkNgToggle({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 적합/부적합 버튼 컴포넌트
|
// 자동 판정 표시 컴포넌트
|
||||||
function JudgmentToggle({
|
function JudgmentDisplay({ value }: { value: 'pass' | 'fail' | null }) {
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
value: 'pass' | 'fail' | null;
|
|
||||||
onChange: (v: 'pass' | 'fail') => void;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 flex-1">
|
<div className="flex gap-2 flex-1">
|
||||||
<button
|
<div
|
||||||
type="button"
|
|
||||||
onClick={() => onChange('pass')}
|
|
||||||
className={cn(
|
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'
|
value === 'pass'
|
||||||
? 'bg-orange-600 text-white'
|
? '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>
|
</div>
|
||||||
<button
|
<div
|
||||||
type="button"
|
|
||||||
onClick={() => onChange('fail')}
|
|
||||||
className={cn(
|
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'
|
value === 'fail'
|
||||||
? 'bg-black text-white'
|
? '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>
|
</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({
|
export function InspectionInputModal({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
@@ -278,6 +315,17 @@ export function InspectionInputModal({
|
|||||||
}
|
}
|
||||||
}, [open, productName, specification, processType, initialData]);
|
}, [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 handleComplete = () => {
|
||||||
const data: InspectionData = {
|
const data: InspectionData = {
|
||||||
...formData,
|
...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="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">
|
<div className="space-y-2 mb-4">
|
||||||
<span className="text-sm font-bold">판정</span>
|
<span className="text-sm font-bold">판정 (자동)</span>
|
||||||
<JudgmentToggle
|
<JudgmentDisplay value={autoJudgment} />
|
||||||
value={formData.judgment}
|
|
||||||
onChange={(v) => setFormData((prev) => ({ ...prev, judgment: v }))}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -455,6 +455,7 @@ export default function WorkerScreen() {
|
|||||||
try {
|
try {
|
||||||
const result = await getWorkOrderInspectionData(selectedSidebarOrderId);
|
const result = await getWorkOrderInspectionData(selectedSidebarOrderId);
|
||||||
if (result.success && result.data?.items) {
|
if (result.success && result.data?.items) {
|
||||||
|
const completionUpdates: Record<string, boolean> = {};
|
||||||
setInspectionDataMap((prev) => {
|
setInspectionDataMap((prev) => {
|
||||||
const next = new Map(prev);
|
const next = new Map(prev);
|
||||||
for (const apiItem of result.data!.items) {
|
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);
|
const match = workItems.find((w) => w.apiItemId === apiItem.item_id);
|
||||||
if (match) {
|
if (match) {
|
||||||
next.set(match.id, apiItem.inspection_data as unknown as InspectionData);
|
next.set(match.id, apiItem.inspection_data as unknown as InspectionData);
|
||||||
|
// 중간검사 step 완료 처리
|
||||||
|
const stepKey = match.id.replace('-node-', '-') + '-중간검사';
|
||||||
|
completionUpdates[stepKey] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
// stepCompletionMap 일괄 업데이트
|
||||||
|
if (Object.keys(completionUpdates).length > 0) {
|
||||||
|
setStepCompletionMap((prev) => ({ ...prev, ...completionUpdates }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 검사 데이터 로드 실패는 무시 (새 작업일 수 있음)
|
// 검사 데이터 로드 실패는 무시 (새 작업일 수 있음)
|
||||||
@@ -906,6 +914,10 @@ export default function WorkerScreen() {
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 중간검사 step 완료 처리
|
||||||
|
const stepKey = selectedOrder.id.replace('-node-', '-') + '-중간검사';
|
||||||
|
setStepCompletionMap((prev) => ({ ...prev, [stepKey]: true }));
|
||||||
|
|
||||||
// 실제 API item인 경우 서버에 저장
|
// 실제 API item인 경우 서버에 저장
|
||||||
const targetItem = workItems.find((w) => w.id === selectedOrder.id);
|
const targetItem = workItems.find((w) => w.id === selectedOrder.id);
|
||||||
if (targetItem?.apiItemId && targetItem?.workOrderId) {
|
if (targetItem?.apiItemId && targetItem?.workOrderId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user