Files
sam-react-prod/src/components/production/WorkOrders/documents/BendingInspectionContent.tsx
유병철 efcc645e24 feat(WEB): 생산/검사 기능 대폭 확장 및 작업자화면 검사입력 추가
생산관리:
- 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>
2026-02-05 21:43:28 +09:00

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>
);
});