생산관리: - WipProductionModal 기능 개선 - WorkOrderDetail/Edit 확장 (+265줄) - 검사성적서 콘텐츠 5종 대폭 확장 (벤딩/벤딩WIP/스크린/슬랫/슬랫조인트바) - InspectionReportModal 기능 강화 작업자화면: - WorkerScreen 기능 대폭 확장 (+211줄) - WorkItemCard 개선 - InspectionInputModal 신규 추가 (작업자 검사입력) 공정관리: - StepForm 검사항목 설정 기능 추가 - InspectionSettingModal 신규 추가 - InspectionPreviewModal 신규 추가 - process.ts 타입 확장 (+102줄) 자재관리: - StockStatus 상세/목록/타입/목데이터 개선 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
600 lines
28 KiB
TypeScript
600 lines
28 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* 절곡 중간검사 성적서 문서 콘텐츠
|
|
*
|
|
* 기획서 기준:
|
|
* - 헤더: "중간검사성적서 (절곡)" + 결재란
|
|
* - 기본정보: 제품명/슬랫, 규격/절곡, 수주처, 현장명 | 제품LOT NO, 로트크기, 검사일자, 검사자
|
|
* + 제품명/KWE01, 마감유형/소니자감
|
|
* - ■ 중간검사 기준서: 2섹션 (가이드레일류 + 연기차단재)
|
|
* - ■ 중간검사 DATA: 분류, 제품명, 타입, 절곡상태결모양(양호/불량),
|
|
* 길이(도면치수/측정값입력), 너비(도면치수/측정값입력),
|
|
* 간격(포인트/도면치수/측정값입력), 판정(자동)
|
|
* - 부적합 내용 / 종합판정(자동)
|
|
*/
|
|
|
|
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 BendingInspectionContentProps {
|
|
data: WorkOrder;
|
|
readOnly?: boolean;
|
|
inspectionData?: InspectionData;
|
|
/** 작업 아이템 목록 - 행 개수 동적 생성용 */
|
|
workItems?: WorkItemData[];
|
|
/** 아이템별 검사 데이터 맵 */
|
|
inspectionDataMap?: InspectionDataMap;
|
|
/** 기준서 도해 이미지 URL */
|
|
schematicImage?: string;
|
|
/** 검사기준 이미지 URL */
|
|
inspectionStandardImage?: string;
|
|
}
|
|
|
|
type CheckStatus = '양호' | '불량' | null;
|
|
|
|
interface GapPoint {
|
|
point: string; // ①②③④⑤
|
|
designValue: string; // 도면치수
|
|
measured: string; // 측정값 (입력)
|
|
}
|
|
|
|
interface ProductRow {
|
|
id: string;
|
|
category: string;
|
|
productName: string;
|
|
productType: string;
|
|
bendingStatus: CheckStatus;
|
|
lengthDesign: string;
|
|
lengthMeasured: string;
|
|
widthDesign: string;
|
|
widthMeasured: string;
|
|
gapPoints: GapPoint[];
|
|
}
|
|
|
|
/**
|
|
* 절곡 검사성적서 - 가이드레일 타입별 행 구조
|
|
*
|
|
* | 타입 조합 | 가이드레일 행 개수 |
|
|
* |-----------------------|-------------------|
|
|
* | 벽면형/벽면형 (벽벽) | 1행 |
|
|
* | 측면형/측면형 (측측) | 1행 |
|
|
* | 벽면형/측면형 (혼합형) | 2행 (규격이 달라서) |
|
|
*
|
|
* TODO: 실제 구현 시 공정 데이터에서 타입 정보를 받아서
|
|
* INITIAL_PRODUCTS를 동적으로 생성해야 함
|
|
*/
|
|
const INITIAL_PRODUCTS: Omit<ProductRow, 'bendingStatus' | 'lengthMeasured' | 'widthMeasured'>[] = [
|
|
// 현재 목업: 혼합형(벽/측)인 경우 가이드레일 2행
|
|
{
|
|
id: 'guide-rail-wall', category: 'KWE01', productName: '가이드레일', productType: '벽면형',
|
|
lengthDesign: '3000', widthDesign: 'N/A',
|
|
gapPoints: [
|
|
{ point: '①', designValue: '30', measured: '' },
|
|
{ point: '②', designValue: '80', measured: '' },
|
|
{ point: '③', designValue: '45', measured: '' },
|
|
{ point: '④', designValue: '40', measured: '' },
|
|
{ point: '⑤', designValue: '34', measured: '' },
|
|
],
|
|
},
|
|
{
|
|
id: 'guide-rail-side', category: 'KWE01', productName: '가이드레일', productType: '측면형',
|
|
lengthDesign: '3000', widthDesign: 'N/A',
|
|
gapPoints: [
|
|
{ point: '①', designValue: '28', measured: '' },
|
|
{ point: '②', designValue: '75', measured: '' },
|
|
{ point: '③', designValue: '42', measured: '' },
|
|
{ point: '④', designValue: '38', measured: '' },
|
|
{ point: '⑤', designValue: '32', measured: '' },
|
|
],
|
|
},
|
|
{
|
|
id: 'case', category: 'KWE01', productName: '케이스', productType: '500X380',
|
|
lengthDesign: '3000', widthDesign: 'N/A',
|
|
gapPoints: [
|
|
{ point: '①', designValue: '380', measured: '' },
|
|
{ point: '②', designValue: '50', measured: '' },
|
|
{ point: '③', designValue: '240', measured: '' },
|
|
{ point: '④', designValue: '50', measured: '' },
|
|
],
|
|
},
|
|
{
|
|
id: 'bottom-finish', category: 'KWE01', productName: '하단마감재', productType: '60X40',
|
|
lengthDesign: '3000', widthDesign: 'N/A',
|
|
gapPoints: [
|
|
{ point: '②', designValue: '60', measured: '' },
|
|
{ point: '②', designValue: '64', measured: '' },
|
|
],
|
|
},
|
|
{
|
|
id: 'bottom-l-bar', category: 'KWE01', productName: '하단L-BAR', productType: '17X60',
|
|
lengthDesign: '3000', widthDesign: 'N/A',
|
|
gapPoints: [
|
|
{ point: '①', designValue: '17', measured: '' },
|
|
],
|
|
},
|
|
{
|
|
id: 'smoke-w50', category: 'KWE01', productName: '연기차단재', productType: 'W50\n가이드레일용',
|
|
lengthDesign: '3000', widthDesign: '',
|
|
gapPoints: [
|
|
{ point: '①', designValue: '50', measured: '' },
|
|
{ point: '②', designValue: '12', measured: '' },
|
|
],
|
|
},
|
|
{
|
|
id: 'smoke-w80', category: 'KWE01', productName: '연기차단재', productType: 'W80\n케이스용',
|
|
lengthDesign: '3000', widthDesign: '',
|
|
gapPoints: [
|
|
{ point: '①', designValue: '80', measured: '' },
|
|
{ point: '②', designValue: '12', measured: '' },
|
|
],
|
|
},
|
|
];
|
|
|
|
// 상태 변환 함수: 'good'/'bad' → '양호'/'불량'
|
|
const convertToCheckStatus = (status: 'good' | 'bad' | null | undefined): CheckStatus => {
|
|
if (status === 'good') return '양호';
|
|
if (status === 'bad') return '불량';
|
|
return null;
|
|
};
|
|
|
|
export const BendingInspectionContent = forwardRef<InspectionContentRef, BendingInspectionContentProps>(function BendingInspectionContent({
|
|
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 || '-';
|
|
|
|
const [products, setProducts] = useState<ProductRow[]>(() =>
|
|
INITIAL_PRODUCTS.map(p => ({
|
|
...p,
|
|
bendingStatus: null,
|
|
lengthMeasured: '',
|
|
widthMeasured: '',
|
|
gapPoints: p.gapPoints.map(gp => ({ ...gp })),
|
|
}))
|
|
);
|
|
|
|
const [inadequateContent, setInadequateContent] = useState('');
|
|
|
|
// workItems의 첫 번째 아이템 검사 데이터로 절곡상태 적용
|
|
useEffect(() => {
|
|
if (workItems && workItems.length > 0 && inspectionDataMap) {
|
|
const firstItem = workItems[0];
|
|
const itemData = inspectionDataMap.get(firstItem.id);
|
|
if (itemData?.bendingStatus) {
|
|
const bendingStatusValue = convertToCheckStatus(itemData.bendingStatus);
|
|
setProducts(prev => prev.map(p => ({
|
|
...p,
|
|
bendingStatus: bendingStatusValue,
|
|
})));
|
|
}
|
|
}
|
|
}, [workItems, inspectionDataMap]);
|
|
|
|
const handleStatusChange = useCallback((productId: string, value: CheckStatus) => {
|
|
if (readOnly) return;
|
|
setProducts(prev => prev.map(p =>
|
|
p.id === productId ? { ...p, bendingStatus: value } : p
|
|
));
|
|
}, [readOnly]);
|
|
|
|
const handleInputChange = useCallback((productId: string, field: 'lengthMeasured' | 'widthMeasured', value: string) => {
|
|
if (readOnly) return;
|
|
setProducts(prev => prev.map(p =>
|
|
p.id === productId ? { ...p, [field]: value } : p
|
|
));
|
|
}, [readOnly]);
|
|
|
|
const handleGapMeasuredChange = useCallback((productId: string, gapIndex: number, value: string) => {
|
|
if (readOnly) return;
|
|
setProducts(prev => prev.map(p => {
|
|
if (p.id !== productId) return p;
|
|
const newGapPoints = p.gapPoints.map((gp, i) =>
|
|
i === gapIndex ? { ...gp, measured: value } : gp
|
|
);
|
|
return { ...p, gapPoints: newGapPoints };
|
|
}));
|
|
}, [readOnly]);
|
|
|
|
// 행별 판정 자동 계산
|
|
const getProductJudgment = useCallback((product: ProductRow): '적' | '부' | null => {
|
|
if (product.bendingStatus === '불량') return '부';
|
|
if (product.bendingStatus === '양호') return '적';
|
|
return null;
|
|
}, []);
|
|
|
|
// 종합판정 자동 계산
|
|
const overallResult = useMemo(() => {
|
|
const judgments = products.map(getProductJudgment);
|
|
if (judgments.some(j => j === '부')) return '불합격';
|
|
if (judgments.every(j => j === '적')) return '합격';
|
|
return null;
|
|
}, [products, getProductJudgment]);
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
getInspectionData: () => ({
|
|
products: products.map(p => ({
|
|
id: p.id,
|
|
category: p.category,
|
|
productName: p.productName,
|
|
productType: p.productType,
|
|
bendingStatus: p.bendingStatus,
|
|
lengthMeasured: p.lengthMeasured,
|
|
widthMeasured: p.widthMeasured,
|
|
gapPoints: p.gapPoints.map(gp => ({
|
|
point: gp.point,
|
|
designValue: gp.designValue,
|
|
measured: gp.measured,
|
|
})),
|
|
})),
|
|
inadequateContent,
|
|
overallResult,
|
|
}),
|
|
}), [products, 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 inputClass = 'w-full text-center border-0 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-xs';
|
|
|
|
// 전체 행 수 계산 (간격 포인트 수 합계)
|
|
const totalRows = products.reduce((sum, p) => sum + p.gapPoints.length, 0);
|
|
|
|
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>
|
|
<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">KWE01</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">소니자감</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
{/* ===== 중간검사 기준서 (1) 가이드레일/케이스/하단마감재/하단L-BAR ===== */}
|
|
<div className="mb-1 font-bold text-sm">■ 중간검사 기준서</div>
|
|
<table className="w-full table-fixed border-collapse text-xs mb-4">
|
|
<colgroup>
|
|
<col style={{width: '80px'}} />
|
|
<col style={{width: '180px'}} />
|
|
<col style={{width: '58px'}} />
|
|
<col style={{width: '58px'}} />
|
|
<col />
|
|
<col style={{width: '68px'}} />
|
|
<col style={{width: '78px'}} />
|
|
<col style={{width: '110px'}} />
|
|
</colgroup>
|
|
<tbody>
|
|
{/* 도해 3개 (가이드레일 / 케이스 / 하단마감재) */}
|
|
<tr>
|
|
<td className="border border-gray-400 px-2 py-1 font-bold text-center align-middle" rowSpan={6}>
|
|
가이드레일<br/>케이스<br/>하단마감재<br/>하단 L-BAR
|
|
</td>
|
|
<td className="border border-gray-400 p-0" colSpan={7}>
|
|
<div className="grid grid-cols-3 divide-x divide-gray-400">
|
|
<div className="text-center font-medium bg-gray-100 py-1">가이드레일 도해</div>
|
|
<div className="text-center font-medium bg-gray-100 py-1">케이스 도해</div>
|
|
<div className="text-center font-medium bg-gray-100 py-1">하단마감재 도해</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td className="border border-gray-400 p-0" colSpan={7}>
|
|
{schematicImage ? (
|
|
<div className="h-28 flex items-center justify-center p-2">
|
|
<img src={schematicImage} alt="기준서 도해" className="max-h-24 mx-auto object-contain" />
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-3 divide-x divide-gray-400">
|
|
<div className="h-28 flex items-center justify-center text-gray-300 text-xs">IMG</div>
|
|
<div className="h-28 flex items-center justify-center text-gray-300 text-xs">IMG</div>
|
|
<div className="h-28 flex items-center justify-center text-gray-300 text-xs">IMG</div>
|
|
</div>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
{/* 기준서 헤더 */}
|
|
<tr>
|
|
<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" 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 p-2 text-center text-gray-500 align-middle text-xs" rowSpan={3}>
|
|
절곡류 중간검사<br/>상세도면 참조
|
|
</td>
|
|
<td className="border border-gray-400 px-2 py-1 font-medium whitespace-nowrap">겉모양</td>
|
|
<td className="border border-gray-400 px-2 py-1 font-medium whitespace-nowrap">절곡상태</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" rowSpan={3}>n = 1, c = 0</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 bg-gray-50" rowSpan={2}>치수<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" rowSpan={2}>체크검사</td>
|
|
<td className="border border-gray-400 px-2 py-1">KS F 4510 7항<br/>표9</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">도면치수 ± 2</td>
|
|
<td className="border border-gray-400 px-2 py-1">KS F 4510 7항<br/>표9 / 자체규정</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
{/* ===== 중간검사 기준서 (2) 연기차단재 ===== */}
|
|
<table className="w-full table-fixed border-collapse text-xs mb-6">
|
|
<colgroup>
|
|
<col style={{width: '80px'}} />
|
|
<col style={{width: '180px'}} />
|
|
<col style={{width: '52px'}} />
|
|
<col style={{width: '52px'}} />
|
|
<col />
|
|
<col style={{width: '68px'}} />
|
|
<col style={{width: '78px'}} />
|
|
<col style={{width: '110px'}} />
|
|
</colgroup>
|
|
<tbody>
|
|
{/* 헤더 */}
|
|
<tr>
|
|
<td className="border border-gray-400 px-2 py-1 font-bold text-center align-middle" rowSpan={6}>
|
|
연기차단재
|
|
</td>
|
|
<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" 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>
|
|
{/* 겉모양 | 절곡상태 (row 1) */}
|
|
<tr>
|
|
<td className="border border-gray-400 p-2 text-center align-middle" rowSpan={5}>
|
|
{inspectionStandardImage ? (
|
|
<img src={inspectionStandardImage} alt="검사기준 도해" className="max-h-32 mx-auto object-contain" />
|
|
) : (
|
|
<div className="h-32 flex items-center justify-center text-gray-300">도해 이미지 영역</div>
|
|
)}
|
|
</td>
|
|
<td className="border border-gray-400 px-2 py-1 font-medium" rowSpan={2}>겉모양</td>
|
|
<td className="border border-gray-400 px-2 py-1 font-medium" rowSpan={2}>절곡상태</td>
|
|
<td className="border border-gray-400 px-2 py-1" rowSpan={2}>절단이 프레임에서 빠지지 않을것</td>
|
|
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={2}>육안검사</td>
|
|
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={5}>n = 1, c = 0</td>
|
|
<td className="border border-gray-400 px-2 py-1">KS F 4510 5.1항</td>
|
|
</tr>
|
|
{/* 겉모양 | 절곡상태 (row 2 - 관련규정 분리) */}
|
|
<tr>
|
|
<td className="border border-gray-400 px-2 py-1">KS F 4510 7항<br/>표9 인용</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" rowSpan={3}>체크검사</td>
|
|
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={3}>자체규정</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">W50 : 50 ± 5<br/>W80 : 80 ± 5</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">도면치수 ± 2</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-14" rowSpan={2}>분류</th>
|
|
<th className="border border-gray-400 p-1" rowSpan={2}>제품명</th>
|
|
<th className="border border-gray-400 p-1 w-16" rowSpan={2}>타입</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={3}>간격 (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-10">포인트</th>
|
|
<th className="border border-gray-400 p-1 w-14">도면치수</th>
|
|
<th className="border border-gray-400 p-1 w-14">측정값</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{products.map((product) => {
|
|
const judgment = getProductJudgment(product);
|
|
const rowCount = product.gapPoints.length;
|
|
|
|
return product.gapPoints.map((gap, gapIdx) => (
|
|
<tr key={`${product.id}-${gapIdx}`}>
|
|
{/* 첫 번째 간격 행에만 rowSpan 적용 */}
|
|
{gapIdx === 0 && (
|
|
<>
|
|
<td className="border border-gray-400 p-1 text-center font-medium bg-gray-50" rowSpan={rowCount}>{product.category}</td>
|
|
<td className="border border-gray-400 p-1" rowSpan={rowCount}>{product.productName}</td>
|
|
<td className="border border-gray-400 p-1 text-center whitespace-pre-line" rowSpan={rowCount}>{product.productType}</td>
|
|
{/* 절곡상태 - 양호/불량 체크 */}
|
|
<td className="border border-gray-400 p-1" rowSpan={rowCount}>
|
|
<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(product.bendingStatus === '양호', () => handleStatusChange(product.id, product.bendingStatus === '양호' ? null : '양호'))}
|
|
양호
|
|
</label>
|
|
<label className="flex items-center gap-0.5 cursor-pointer text-xs whitespace-nowrap">
|
|
{renderCheckbox(product.bendingStatus === '불량', () => handleStatusChange(product.id, product.bendingStatus === '불량' ? null : '불량'))}
|
|
불량
|
|
</label>
|
|
</div>
|
|
</td>
|
|
{/* 길이 */}
|
|
<td className="border border-gray-400 p-1 text-center" rowSpan={rowCount}>{product.lengthDesign}</td>
|
|
<td className="border border-gray-400 p-1" rowSpan={rowCount}>
|
|
<input type="text" value={product.lengthMeasured} onChange={(e) => handleInputChange(product.id, 'lengthMeasured', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
|
|
</td>
|
|
{/* 너비 */}
|
|
<td className="border border-gray-400 p-1 text-center" rowSpan={rowCount}>{product.widthDesign || 'N/A'}</td>
|
|
<td className="border border-gray-400 p-1" rowSpan={rowCount}>
|
|
<input type="text" value={product.widthMeasured} onChange={(e) => handleInputChange(product.id, 'widthMeasured', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
|
|
</td>
|
|
</>
|
|
)}
|
|
{/* 간격 - 포인트별 개별 행 */}
|
|
<td className="border border-gray-400 p-1 text-center">{gap.point}</td>
|
|
<td className="border border-gray-400 p-1 text-center">{gap.designValue}</td>
|
|
<td className="border border-gray-400 p-1">
|
|
<input type="text" value={gap.measured} onChange={(e) => handleGapMeasuredChange(product.id, gapIdx, e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
|
|
</td>
|
|
{/* 판정 - 자동 (첫 행에만) */}
|
|
{gapIdx === 0 && (
|
|
<td className={`border border-gray-400 p-1 text-center font-bold ${
|
|
judgment === '적' ? 'text-blue-600' : judgment === '부' ? 'text-red-600' : 'text-gray-300'
|
|
}`} rowSpan={rowCount}>
|
|
{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>
|
|
);
|
|
});
|