Files
sam-react-prod/src/components/production/WorkOrders/documents/SlatInspectionContent.tsx
권혁성 a9ae162c90 feat(WEB): Phase 4 중간검사 성적서 API 연동 및 컴포넌트 리팩토링
- Phase 4.1: InspectionReportModal API 연동 (getInspectionReport 서버 액션)
- Phase 4.2: 5개 InspectionContent 공통 코드 추출 (inspection-shared.tsx)
  - 공통 컴포넌트: InspectionLayout, CheckStatusCell, JudgmentCell, InspectionFooter
  - 공통 유틸: convertToCheckStatus, calculateOverallResult, getOrderInfo
  - 총 코드량 2,376줄 → 1,583줄 (33% 감소)
- InspectionInputModal 기본값 null로 수정 (적합 버튼 미선택 상태 시작)
2026-02-09 17:37:49 +09:00

291 lines
14 KiB
TypeScript

'use client';
/**
* 슬랫 중간검사 성적서 문서 콘텐츠
*
* 기획서 기준:
* - 헤더: "중간검사성적서 (슬랫)" + 결재란
* - 기본정보: 제품명/슬랫, 규격/EGI 1.6T, 수주처, 현장명 | 제품LOT NO, 로트크기, 검사일자, 검사자
* - ■ 중간검사 기준서: 도해 + 검사항목/검사기준/검사방법/검사주기/관련규정
* 가공상태, 결모양, 조립상태, 치수(높이/길이)
* - ■ 중간검사 DATA: No, 가공상태결모양(양호/불량), 조립상태결모양(양호/불량),
* ①높이(기준치/측정값입력), ②높이(기준치/측정값입력),
* 길이(엔드락제외)(도면치수/측정값입력), 판정(자동)
* - 부적합 내용 / 종합판정(자동)
*/
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';
import {
type CheckStatus,
type InspectionContentRef,
convertToCheckStatus,
getFullDate,
getToday,
getOrderInfo,
calculateOverallResult,
INPUT_CLASS,
DEFAULT_ROW_COUNT,
CheckStatusCell,
JudgmentCell,
InspectionLayout,
InspectionFooter,
} from './inspection-shared';
export type { InspectionContentRef };
export interface SlatInspectionContentProps {
data: WorkOrder;
readOnly?: boolean;
inspectionData?: InspectionData;
workItems?: WorkItemData[];
inspectionDataMap?: InspectionDataMap;
schematicImage?: string;
inspectionStandardImage?: string;
}
interface InspectionRow {
id: number;
itemId?: string;
itemName?: string;
processStatus: CheckStatus;
assemblyStatus: CheckStatus;
height1Standard: string;
height1Measured: string;
height2Standard: string;
height2Measured: string;
lengthDesign: string;
lengthMeasured: string;
}
function buildRow(i: number, workItems?: WorkItemData[], inspectionDataMap?: InspectionDataMap): InspectionRow {
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,
assemblyStatus: itemData ? convertToCheckStatus(itemData.assemblyStatus) : null,
height1Standard: '16.5 \u00b1 1',
height1Measured: itemData?.height1?.toString() || '',
height2Standard: '14.5 \u00b1 1',
height2Measured: itemData?.height2?.toString() || '',
lengthDesign: '0',
lengthMeasured: itemData?.length?.toString() || '',
};
}
export const SlatInspectionContent = forwardRef<InspectionContentRef, SlatInspectionContentProps>(function SlatInspectionContent({
data: order,
readOnly = false,
workItems,
inspectionDataMap,
schematicImage,
}, ref) {
const fullDate = getFullDate();
const today = getToday();
const { documentNo, primaryAssignee } = getOrderInfo(order);
const rowCount = workItems?.length || DEFAULT_ROW_COUNT;
const [rows, setRows] = useState<InspectionRow[]>(() =>
Array.from({ length: rowCount }, (_, i) => buildRow(i, workItems, inspectionDataMap))
);
useEffect(() => {
const newRowCount = workItems?.length || DEFAULT_ROW_COUNT;
setRows(Array.from({ length: newRowCount }, (_, i) => buildRow(i, workItems, inspectionDataMap)));
}, [workItems, inspectionDataMap]);
const [inadequateContent, setInadequateContent] = useState('');
const handleStatusChange = useCallback((rowId: number, field: 'processStatus' | 'assemblyStatus', value: CheckStatus) => {
if (readOnly) return;
setRows(prev => prev.map(row =>
row.id === rowId ? { ...row, [field]: value } : row
));
}, [readOnly]);
const handleInputChange = useCallback((rowId: number, field: keyof InspectionRow, value: string) => {
if (readOnly) return;
const filtered = value.replace(/[^\d.]/g, '');
setRows(prev => prev.map(row =>
row.id === rowId ? { ...row, [field]: filtered } : row
));
}, [readOnly]);
const getRowJudgment = useCallback((row: InspectionRow): '적' | '부' | null => {
const { processStatus, assemblyStatus } = row;
if (processStatus === '불량' || assemblyStatus === '불량') return '부';
if (processStatus === '양호' && assemblyStatus === '양호') return '적';
return null;
}, []);
const overallResult = useMemo(() => calculateOverallResult(rows.map(getRowJudgment)), [rows, getRowJudgment]);
useImperativeHandle(ref, () => ({
getInspectionData: () => ({
rows: rows.map(row => ({
id: row.id,
processStatus: row.processStatus,
assemblyStatus: row.assemblyStatus,
height1Measured: row.height1Measured,
height2Measured: row.height2Measured,
lengthMeasured: row.lengthMeasured,
})),
inadequateContent,
overallResult,
}),
}), [rows, inadequateContent, overallResult]);
return (
<InspectionLayout title="중간검사성적서 (슬랫)" documentNo={documentNo} fullDate={fullDate} primaryAssignee={primaryAssignee}>
{/* ===== 기본 정보 ===== */}
<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">EGI 1.6T</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/5" rowSpan={7}>
{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={3}></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 text-center" rowSpan={3}></td>
<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" rowSpan={3}></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} rowSpan={2}></td>
<td className="border border-gray-400 px-2 py-1" rowSpan={2}> <br/> <br/> <br/> </td>
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={2}>n = 1, c = 0</td>
<td className="border border-gray-400 px-2 py-1">KS F 4510 9</td>
</tr>
<tr>
<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 bg-gray-50 text-center" rowSpan={3}><br/>(mm)</td>
<td className="border border-gray-400 px-2 py-1 font-medium text-center" rowSpan={2}></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">16.5 ± 1</td>
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={3}></td>
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={3}></td>
<td className="border border-gray-400 px-2 py-1" rowSpan={3}>KS F 4510 7<br/>9</td>
</tr>
<tr>
<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">14.5 ± 1</td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 font-medium 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 text-center">() ± 4</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" 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)<br/><span className="font-normal text-gray-500">( )</span></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>
<CheckStatusCell value={row.processStatus} onToggle={(v) => handleStatusChange(row.id, 'processStatus', v)} readOnly={readOnly} />
<CheckStatusCell value={row.assemblyStatus} onToggle={(v) => handleStatusChange(row.id, 'assemblyStatus', v)} readOnly={readOnly} />
<td className="border border-gray-400 p-1 text-center">{row.height1Standard}</td>
<td className="border border-gray-400 p-1">
<input type="text" value={row.height1Measured} onChange={(e) => handleInputChange(row.id, 'height1Measured', e.target.value)} disabled={readOnly} className={INPUT_CLASS} placeholder="-" />
</td>
<td className="border border-gray-400 p-1 text-center">{row.height2Standard}</td>
<td className="border border-gray-400 p-1">
<input type="text" value={row.height2Measured} onChange={(e) => handleInputChange(row.id, 'height2Measured', e.target.value)} disabled={readOnly} className={INPUT_CLASS} placeholder="-" />
</td>
<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={row.lengthMeasured} onChange={(e) => handleInputChange(row.id, 'lengthMeasured', e.target.value)} disabled={readOnly} className={INPUT_CLASS} placeholder="-" />
</td>
<JudgmentCell judgment={judgment} />
</tr>
);
})}
</tbody>
</table>
{/* ===== 부적합 내용 + 종합판정 ===== */}
<InspectionFooter
readOnly={readOnly}
overallResult={overallResult}
inadequateContent={inadequateContent}
onInadequateContentChange={setInadequateContent}
/>
</InspectionLayout>
);
});