- 회계 모듈 전면 개선: 청구/입금/출금/매입/매출/세금계산서/일반전표/거래처원장 등 - 견적 모듈 금액 포맷/할인/수식/미리보기 등 코드 정리 - 설정 모듈: 계정관리/직급/직책/권한 상세 간소화 - 생산 모듈: 작업지시서/작업자화면/검수 문서 코드 정리 - UniversalListPage 엑셀 다운로드 및 필터 기능 확장 - 대시보드/게시판/수주 등 날짜 유틸 공통화 적용 - claudedocs 문서 인덱스 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
311 lines
15 KiB
TypeScript
311 lines
15 KiB
TypeScript
'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>
|
||
);
|
||
}); |