Files
sam-react-prod/src/components/production/WorkOrders/documents/ScreenInspectionContent.tsx

471 lines
23 KiB
TypeScript
Raw Normal View History

'use client';
/**
*
*
* :
* - : "중간검사성적서 (스크린)" +
* - 기본정보: 제품명/, / , , | LOT NO, , ,
* - 기준서: 도해 + ////
* , , , (//)
* - DATA: No, (/), (/), (/),
* (/), (/), (/OK·NG선택), ()
* - / ()
*/
import { useState, useCallback, useMemo, forwardRef, useImperativeHandle, useEffect } from 'react';
import type { WorkOrder } from '../types';
import type { InspectionData } from '@/components/production/WorkerScreen/InspectionInputModal';
import type { WorkItemData } from '@/components/production/WorkerScreen/types';
import type { InspectionDataMap } from './InspectionReportModal';
export interface InspectionContentRef {
getInspectionData: () => unknown;
}
export interface ScreenInspectionContentProps {
data: WorkOrder;
readOnly?: boolean;
inspectionData?: InspectionData;
/** 작업 아이템 목록 - 행 개수 동적 생성용 */
workItems?: WorkItemData[];
/** 아이템별 검사 데이터 맵 */
inspectionDataMap?: InspectionDataMap;
/** 기준서 도해 이미지 URL */
schematicImage?: string;
/** 검사기준 이미지 URL */
inspectionStandardImage?: string;
}
type CheckStatus = '양호' | '불량' | null;
type GapResult = 'OK' | 'NG' | null;
interface InspectionRow {
id: number;
itemId?: string; // 작업 아이템 ID
itemName?: string; // 작업 아이템 이름
processStatus: CheckStatus; // 가공상태 결모양
sewingStatus: CheckStatus; // 재봉상태 결모양
assemblyStatus: CheckStatus; // 조립상태
lengthDesign: string; // 길이 도면치수 (표시용)
lengthMeasured: string; // 길이 측정값 (입력)
widthDesign: string; // 나비 도면치수 (표시용)
widthMeasured: string; // 나비 측정값 (입력)
gapStandard: string; // 간격 기준치 (표시용)
gapResult: GapResult; // 간격 측정값 (OK/NG 선택)
}
const DEFAULT_ROW_COUNT = 6;
export const ScreenInspectionContent = forwardRef<InspectionContentRef, ScreenInspectionContentProps>(function ScreenInspectionContent({
data: order,
readOnly = false,
workItems,
inspectionDataMap,
schematicImage,
inspectionStandardImage,
}, ref) {
const fullDate = new Date().toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
const today = new Date().toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).replace(/\. /g, '-').replace('.', '');
const documentNo = order.workOrderNo || 'ABC123';
const primaryAssignee = order.assignees?.find(a => a.isPrimary)?.name || order.assignee || '-';
// 행 개수: workItems가 있으면 그 개수, 없으면 기본값
const rowCount = workItems?.length || DEFAULT_ROW_COUNT;
// InspectionData를 InspectionRow로 변환하는 함수
const convertToCheckStatus = (status: 'good' | 'bad' | null | undefined): CheckStatus => {
if (status === 'good') return '양호';
if (status === 'bad') return '불량';
return null;
};
const convertToGapResult = (status: 'ok' | 'ng' | null | undefined): GapResult => {
if (status === 'ok') return 'OK';
if (status === 'ng') return 'NG';
return null;
};
const [rows, setRows] = useState<InspectionRow[]>(() => {
return Array.from({ length: rowCount }, (_, i) => {
const item = workItems?.[i];
const itemData = item && inspectionDataMap?.get(item.id);
return {
id: i + 1,
itemId: item?.id,
itemName: item?.itemName || '',
processStatus: itemData ? convertToCheckStatus(itemData.processingStatus) : null,
sewingStatus: itemData ? convertToCheckStatus(itemData.sewingStatus) : null,
assemblyStatus: itemData ? convertToCheckStatus(itemData.assemblyStatus) : null,
lengthDesign: '7,400',
lengthMeasured: itemData?.length?.toString() || '',
widthDesign: '2,950',
widthMeasured: itemData?.width?.toString() || '',
gapStandard: '400 이하',
gapResult: itemData ? convertToGapResult(itemData.gapStatus) : null,
};
});
});
// workItems나 inspectionDataMap이 변경되면 rows 업데이트
useEffect(() => {
const newRowCount = workItems?.length || DEFAULT_ROW_COUNT;
setRows(Array.from({ length: newRowCount }, (_, i) => {
const item = workItems?.[i];
const itemData = item && inspectionDataMap?.get(item.id);
return {
id: i + 1,
itemId: item?.id,
itemName: item?.itemName || '',
processStatus: itemData ? convertToCheckStatus(itemData.processingStatus) : null,
sewingStatus: itemData ? convertToCheckStatus(itemData.sewingStatus) : null,
assemblyStatus: itemData ? convertToCheckStatus(itemData.assemblyStatus) : null,
lengthDesign: '7,400',
lengthMeasured: itemData?.length?.toString() || '',
widthDesign: '2,950',
widthMeasured: itemData?.width?.toString() || '',
gapStandard: '400 이하',
gapResult: itemData ? convertToGapResult(itemData.gapStatus) : null,
};
}));
}, [workItems, inspectionDataMap]);
const [inadequateContent, setInadequateContent] = useState('');
const handleStatusChange = useCallback((rowId: number, field: 'processStatus' | 'sewingStatus' | 'assemblyStatus', value: CheckStatus) => {
if (readOnly) return;
setRows(prev => prev.map(row =>
row.id === rowId ? { ...row, [field]: value } : row
));
}, [readOnly]);
// 숫자 콤마 포맷
const formatNumberWithComma = (value: string): string => {
const num = value.replace(/[^\d]/g, '');
if (!num) return '';
return Number(num).toLocaleString();
};
const handleInputChange = useCallback((rowId: number, field: 'lengthMeasured' | 'widthMeasured', value: string) => {
if (readOnly) return;
// 숫자만 저장 (콤마 제거)
const numOnly = value.replace(/[^\d]/g, '');
setRows(prev => prev.map(row =>
row.id === rowId ? { ...row, [field]: numOnly } : row
));
}, [readOnly]);
const handleGapChange = useCallback((rowId: number, value: GapResult) => {
if (readOnly) return;
setRows(prev => prev.map(row =>
row.id === rowId ? { ...row, gapResult: value } : row
));
}, [readOnly]);
// 행별 판정 자동 계산
const getRowJudgment = useCallback((row: InspectionRow): '적' | '부' | null => {
const { processStatus, sewingStatus, assemblyStatus, gapResult } = row;
// 하나라도 불량 or NG → 부
if (processStatus === '불량' || sewingStatus === '불량' || assemblyStatus === '불량' || gapResult === 'NG') {
return '부';
}
// 모두 양호 + OK → 적
if (processStatus === '양호' && sewingStatus === '양호' && assemblyStatus === '양호' && gapResult === 'OK') {
return '적';
}
return null;
}, []);
// 종합판정 자동 계산
const overallResult = useMemo(() => {
const judgments = rows.map(getRowJudgment);
if (judgments.some(j => j === '부')) return '불합격';
if (judgments.every(j => j === '적')) return '합격';
return null;
}, [rows, getRowJudgment]);
useImperativeHandle(ref, () => ({
getInspectionData: () => ({
rows: rows.map(row => ({
id: row.id,
processStatus: row.processStatus,
sewingStatus: row.sewingStatus,
assemblyStatus: row.assemblyStatus,
lengthMeasured: row.lengthMeasured,
widthMeasured: row.widthMeasured,
gapResult: row.gapResult,
})),
inadequateContent,
overallResult,
}),
}), [rows, inadequateContent, overallResult]);
// PDF 호환 체크박스 렌더 (양호/불량)
const renderCheckbox = (checked: boolean, onClick: () => void) => (
<span
className={`inline-flex items-center justify-center w-3 h-3 border rounded-sm text-[8px] leading-none cursor-pointer select-none ${
checked ? 'border-gray-600 bg-gray-700 text-white' : 'border-gray-400 bg-white'
}`}
onClick={() => !readOnly && onClick()}
role="checkbox"
aria-checked={checked}
>
{checked ? '✓' : ''}
</span>
);
const renderCheckStatus = (rowId: number, field: 'processStatus' | 'sewingStatus' | 'assemblyStatus', value: CheckStatus) => (
<td className="border border-gray-400 p-1">
<div className="flex flex-col items-center gap-0.5">
<label className="flex items-center gap-0.5 cursor-pointer text-xs whitespace-nowrap">
{renderCheckbox(value === '양호', () => handleStatusChange(rowId, field, value === '양호' ? null : '양호'))}
</label>
<label className="flex items-center gap-0.5 cursor-pointer text-xs whitespace-nowrap">
{renderCheckbox(value === '불량', () => handleStatusChange(rowId, field, value === '불량' ? null : '불량'))}
</label>
</div>
</td>
);
const inputClass = 'w-full text-center border-0 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-xs';
return (
<div className="p-6 bg-white">
{/* ===== 헤더 영역 ===== */}
<div className="flex justify-between items-start mb-6">
<div>
<h1 className="text-2xl font-bold"> ()</h1>
<p className="text-xs text-gray-500 mt-1 whitespace-nowrap">
: {documentNo} | : {fullDate}
</p>
</div>
{/* 결재란 */}
<table className="border-collapse text-sm flex-shrink-0">
<tbody>
<tr>
<td className="border border-gray-400 px-4 py-1 text-center align-middle" rowSpan={3}><br/></td>
<td className="border border-gray-400 px-6 py-1 text-center"></td>
<td className="border border-gray-400 px-6 py-1 text-center"></td>
<td className="border border-gray-400 px-6 py-1 text-center"></td>
<td className="border border-gray-400 px-6 py-1 text-center"></td>
</tr>
<tr>
<td className="border border-gray-400 px-6 py-3 text-center">{primaryAssignee}</td>
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400"></td>
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400"></td>
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400"></td>
</tr>
<tr>
<td className="border border-gray-400 px-6 py-1 text-center whitespace-nowrap"></td>
<td className="border border-gray-400 px-6 py-1 text-center whitespace-nowrap"></td>
<td className="border border-gray-400 px-6 py-1 text-center whitespace-nowrap"></td>
<td className="border border-gray-400 px-6 py-1 text-center whitespace-nowrap"></td>
</tr>
</tbody>
</table>
</div>
{/* ===== 기본 정보 ===== */}
<table className="w-full border-collapse text-xs mb-6">
<tbody>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium w-24"></td>
<td className="border border-gray-400 px-3 py-2"></td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium w-28"> LOT NO</td>
<td className="border border-gray-400 px-3 py-2">{order.lotNo || '-'}</td>
</tr>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2"> </td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{order.items?.length || 0} </td>
</tr>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{order.client || '-'}</td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{today}</td>
</tr>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{order.projectName || '-'}</td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{primaryAssignee}</td>
</tr>
</tbody>
</table>
{/* ===== 중간검사 기준서 ===== */}
<div className="mb-1 font-bold text-sm"> </div>
<table className="w-full border-collapse text-xs mb-6">
<tbody>
<tr>
{/* 도해 영역 */}
<td className="border border-gray-400 p-2 text-center align-middle w-1/4" rowSpan={8}>
{schematicImage ? (
<img src={schematicImage} alt="기준서 도해" className="max-h-40 mx-auto object-contain" />
) : (
<div className="h-40 flex items-center justify-center text-gray-300"> </div>
)}
</td>
{/* 헤더 행 */}
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center" colSpan={2}></th>
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center"></th>
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center"></th>
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center"></th>
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center"></th>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 font-medium" colSpan={2}></td>
<td className="border border-gray-400 px-2 py-1"> </td>
<td className="border border-gray-400 px-2 py-1 text-center"></td>
<td className="border border-gray-400 px-2 py-1 text-center"></td>
<td className="border border-gray-400 px-2 py-1">KS F 4510 5.1</td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 font-medium" colSpan={2}></td>
<td className="border border-gray-400 px-2 py-1"> </td>
<td className="border border-gray-400 px-2 py-1 text-center"></td>
<td className="border border-gray-400 px-2 py-1 text-center"></td>
<td className="border border-gray-400 px-2 py-1"></td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 font-medium" colSpan={2}></td>
<td className="border border-gray-400 px-2 py-1"> </td>
<td className="border border-gray-400 px-2 py-1 text-center"></td>
<td className="border border-gray-400 px-2 py-1 text-center"></td>
<td className="border border-gray-400 px-2 py-1">KS F 4510<br/>n = 1, c = 0</td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 font-medium bg-gray-50" rowSpan={3}><br/>(mm)</td>
<td className="border border-gray-400 px-2 py-1 font-medium"></td>
<td className="border border-gray-400 px-2 py-1"> ± 4</td>
<td className="border border-gray-400 px-2 py-1 text-center"></td>
<td className="border border-gray-400 px-2 py-1 text-center"></td>
<td className="border border-gray-400 px-2 py-1"></td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 font-medium"></td>
<td className="border border-gray-400 px-2 py-1"> + 40</td>
<td className="border border-gray-400 px-2 py-1 text-center"></td>
<td className="border border-gray-400 px-2 py-1 text-center"></td>
<td className="border border-gray-400 px-2 py-1"></td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 font-medium"></td>
<td className="border border-gray-400 px-2 py-1">400 </td>
<td className="border border-gray-400 px-2 py-1 text-center">GONO </td>
<td className="border border-gray-400 px-2 py-1 text-center"></td>
<td className="border border-gray-400 px-2 py-1"></td>
</tr>
</tbody>
</table>
{/* ===== 중간검사 DATA ===== */}
<div className="mb-1 font-bold text-sm"> DATA</div>
<table className="w-full border-collapse text-xs mb-4">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-400 p-1 w-8" rowSpan={2}>No.</th>
<th className="border border-gray-400 p-1 w-16" rowSpan={2}><br/></th>
<th className="border border-gray-400 p-1 w-16" rowSpan={2}><br/></th>
<th className="border border-gray-400 p-1 w-16" rowSpan={2}></th>
<th className="border border-gray-400 p-1" colSpan={2}> (mm)</th>
<th className="border border-gray-400 p-1" colSpan={2}> (mm)</th>
<th className="border border-gray-400 p-1" colSpan={2}> (mm)</th>
<th className="border border-gray-400 p-1 w-14" rowSpan={2}><br/>(/)</th>
</tr>
<tr className="bg-gray-100">
<th className="border border-gray-400 p-1 w-16"></th>
<th className="border border-gray-400 p-1 w-16"></th>
<th className="border border-gray-400 p-1 w-16"></th>
<th className="border border-gray-400 p-1 w-16"></th>
<th className="border border-gray-400 p-1 w-16"></th>
<th className="border border-gray-400 p-1 w-16"></th>
</tr>
</thead>
<tbody>
{rows.map((row) => {
const judgment = getRowJudgment(row);
return (
<tr key={row.id}>
<td className="border border-gray-400 p-1 text-center">{row.id}</td>
{/* 가공상태 - 양호/불량 체크 */}
{renderCheckStatus(row.id, 'processStatus', row.processStatus)}
{/* 재봉상태 - 양호/불량 체크 */}
{renderCheckStatus(row.id, 'sewingStatus', row.sewingStatus)}
{/* 조립상태 - 양호/불량 체크 */}
{renderCheckStatus(row.id, 'assemblyStatus', row.assemblyStatus)}
{/* 길이 - 도면치수 표시 + 측정값 입력 */}
<td className="border border-gray-400 p-1 text-center">{row.lengthDesign}</td>
<td className="border border-gray-400 p-1">
<input type="text" value={formatNumberWithComma(row.lengthMeasured)} onChange={(e) => handleInputChange(row.id, 'lengthMeasured', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
</td>
{/* 나비 - 도면치수 표시 + 측정값 입력 */}
<td className="border border-gray-400 p-1 text-center">{row.widthDesign}</td>
<td className="border border-gray-400 p-1">
<input type="text" value={formatNumberWithComma(row.widthMeasured)} onChange={(e) => handleInputChange(row.id, 'widthMeasured', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
</td>
{/* 간격 - 기준치 표시 + OK/NG 선택 */}
<td className="border border-gray-400 p-1 text-center">{row.gapStandard}</td>
<td className="border border-gray-400 p-1">
<div className="flex flex-col items-center gap-0.5">
<label className="flex items-center gap-0.5 cursor-pointer text-xs">
{renderCheckbox(row.gapResult === 'OK', () => handleGapChange(row.id, row.gapResult === 'OK' ? null : 'OK'))}
OK
</label>
<label className="flex items-center gap-0.5 cursor-pointer text-xs">
{renderCheckbox(row.gapResult === 'NG', () => handleGapChange(row.id, row.gapResult === 'NG' ? null : 'NG'))}
NG
</label>
</div>
</td>
{/* 판정 - 자동 계산 */}
<td className={`border border-gray-400 p-1 text-center font-bold ${
judgment === '적' ? 'text-blue-600' : judgment === '부' ? 'text-red-600' : 'text-gray-300'
}`}>
{judgment || '-'}
</td>
</tr>
);
})}
</tbody>
</table>
{/* ===== 부적합 내용 + 종합판정 ===== */}
<table className="w-full border-collapse text-xs">
<tbody>
<tr>
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium w-24 align-middle text-center"> </td>
<td className="border border-gray-400 px-3 py-2">
<textarea value={inadequateContent} onChange={(e) => !readOnly && setInadequateContent(e.target.value)} disabled={readOnly}
className="w-full border-0 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-xs resize-none" rows={2} />
</td>
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium text-center w-24"></td>
<td className={`border border-gray-400 px-3 py-2 text-center font-bold text-sm w-24 ${
overallResult === '합격' ? 'text-blue-600' : overallResult === '불합격' ? 'text-red-600' : 'text-gray-400'
}`}>
{overallResult || '합격'}
</td>
</tr>
</tbody>
</table>
</div>
);
});