Files
sam-react-prod/src/components/production/WorkOrders/documents/ScreenInspectionContent.tsx
유병철 f344dc7d00 refactor(WEB): 회계/견적/설정/생산 등 전반적 코드 개선 및 공통화 2차
- 회계 모듈 전면 개선: 청구/입금/출금/매입/매출/세금계산서/일반전표/거래처원장 등
- 견적 모듈 금액 포맷/할인/수식/미리보기 등 코드 정리
- 설정 모듈: 계정관리/직급/직책/권한 상세 간소화
- 생산 모듈: 작업지시서/작업자화면/검수 문서 코드 정리
- UniversalListPage 엑셀 다운로드 및 필터 기능 확장
- 대시보드/게시판/수주 등 날짜 유틸 공통화 적용
- claudedocs 문서 인덱스 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 10:45:47 +09:00

311 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
/**
* 스크린 중간검사 성적서 문서 콘텐츠
*
* 검사 항목: 가공상태, 재봉상태, 조립상태, 길이, 나비, 간격(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';
import type { InspectionSetting } from '@/types/process';
import {
type CheckStatus,
type GapResult,
type InspectionContentRef,
convertToCheckStatus,
convertToGapResult,
getFullDate,
getToday,
getOrderInfo,
INPUT_CLASS,
DEFAULT_ROW_COUNT,
InspectionCheckbox,
CheckStatusCell,
InspectionLayout,
InspectionFooter,
InspectionStandardSection,
JudgmentCell,
calculateOverallResult,
} from './inspection-shared';
import { formatNumber } from '@/lib/utils/amount';
export type { InspectionContentRef };
export interface ScreenInspectionContentProps {
data: WorkOrder;
readOnly?: boolean;
inspectionData?: InspectionData;
workItems?: WorkItemData[];
inspectionDataMap?: InspectionDataMap;
inspectionSetting?: InspectionSetting;
}
interface InspectionRow {
id: number;
itemId?: string;
itemName?: string;
processStatus: CheckStatus;
sewingStatus: CheckStatus;
assemblyStatus: CheckStatus;
lengthDesign: string;
lengthMeasured: string;
widthDesign: string;
widthMeasured: string;
gapStandard: string;
gapResult: GapResult;
}
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,
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,
};
}
export const ScreenInspectionContent = forwardRef<InspectionContentRef, ScreenInspectionContentProps>(function ScreenInspectionContent({
data: order,
readOnly = false,
workItems,
inspectionDataMap,
inspectionSetting,
}, ref) {
const schematicImage = inspectionSetting?.schematicImage;
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))
);
const [inadequateContent, setInadequateContent] = useState('');
useEffect(() => {
const newRowCount = workItems?.length || DEFAULT_ROW_COUNT;
setRows(Array.from({ length: newRowCount }, (_, i) => buildRow(i, workItems, inspectionDataMap)));
}, [workItems, inspectionDataMap]);
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 formatNumber(Number(num));
};
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;
if (processStatus === '불량' || sewingStatus === '불량' || assemblyStatus === '불량' || gapResult === 'NG') return '부';
if (processStatus === '양호' && sewingStatus === '양호' && assemblyStatus === '양호' && gapResult === 'OK') 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,
sewingStatus: row.sewingStatus,
assemblyStatus: row.assemblyStatus,
lengthMeasured: row.lengthMeasured,
widthMeasured: row.widthMeasured,
gapResult: row.gapResult,
})),
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"> </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>
{/* 중간검사 기준서 */}
<InspectionStandardSection inspectionSetting={inspectionSetting}>
<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>
</InspectionStandardSection>
{/* 중간검사 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>
<CheckStatusCell value={row.processStatus} onToggle={(v) => handleStatusChange(row.id, 'processStatus', v)} readOnly={readOnly} />
<CheckStatusCell value={row.sewingStatus} onToggle={(v) => handleStatusChange(row.id, 'sewingStatus', 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.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={INPUT_CLASS} 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={INPUT_CLASS} placeholder="-" />
</td>
<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">
<InspectionCheckbox checked={row.gapResult === 'OK'} onClick={() => handleGapChange(row.id, row.gapResult === 'OK' ? null : 'OK')} readOnly={readOnly} />
OK
</label>
<label className="flex items-center gap-0.5 cursor-pointer text-xs">
<InspectionCheckbox checked={row.gapResult === 'NG'} onClick={() => handleGapChange(row.id, row.gapResult === 'NG' ? null : 'NG')} readOnly={readOnly} />
NG
</label>
</div>
</td>
<JudgmentCell judgment={judgment} />
</tr>
);
})}
</tbody>
</table>
<InspectionFooter readOnly={readOnly} overallResult={overallResult} inadequateContent={inadequateContent} onInadequateContentChange={setInadequateContent} />
</InspectionLayout>
);
});