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로 수정 (적합 버튼 미선택 상태 시작)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -19,31 +19,37 @@ 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,
|
||||
InspectionCheckbox,
|
||||
JudgmentCell,
|
||||
InspectionLayout,
|
||||
InspectionFooter,
|
||||
} from './inspection-shared';
|
||||
|
||||
export interface InspectionContentRef {
|
||||
getInspectionData: () => unknown;
|
||||
}
|
||||
export type { InspectionContentRef };
|
||||
|
||||
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; // 측정값 (입력)
|
||||
point: string;
|
||||
designValue: string;
|
||||
measured: string;
|
||||
}
|
||||
|
||||
interface ProductRow {
|
||||
@@ -59,20 +65,7 @@ interface ProductRow {
|
||||
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',
|
||||
@@ -138,13 +131,6 @@ const INITIAL_PRODUCTS: Omit<ProductRow, 'bendingStatus' | 'lengthMeasured' | 'w
|
||||
},
|
||||
];
|
||||
|
||||
// 상태 변환 함수: '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,
|
||||
@@ -153,20 +139,9 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
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 fullDate = getFullDate();
|
||||
const today = getToday();
|
||||
const { documentNo, primaryAssignee } = getOrderInfo(order);
|
||||
|
||||
const [products, setProducts] = useState<ProductRow[]>(() =>
|
||||
INITIAL_PRODUCTS.map(p => ({
|
||||
@@ -180,7 +155,6 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
|
||||
const [inadequateContent, setInadequateContent] = useState('');
|
||||
|
||||
// workItems의 첫 번째 아이템 검사 데이터로 절곡상태 적용
|
||||
useEffect(() => {
|
||||
if (workItems && workItems.length > 0 && inspectionDataMap) {
|
||||
const firstItem = workItems[0];
|
||||
@@ -220,20 +194,13 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
}));
|
||||
}, [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]);
|
||||
const overallResult = useMemo(() => calculateOverallResult(products.map(getProductJudgment)), [products, getProductJudgment]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getInspectionData: () => ({
|
||||
@@ -256,62 +223,8 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
}),
|
||||
}), [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>
|
||||
|
||||
<InspectionLayout title="중간검사성적서 (절곡)" documentNo={documentNo} fullDate={fullDate} primaryAssignee={primaryAssignee}>
|
||||
{/* ===== 기본 정보 ===== */}
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<tbody>
|
||||
@@ -362,7 +275,6 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
<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
|
||||
@@ -390,7 +302,6 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
)}
|
||||
</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>
|
||||
@@ -399,7 +310,6 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
<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/>상세도면 참조
|
||||
@@ -411,7 +321,6 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
<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>
|
||||
@@ -419,7 +328,6 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
<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>
|
||||
@@ -441,7 +349,6 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
<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}>
|
||||
연기차단재
|
||||
@@ -453,7 +360,6 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
<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 ? (
|
||||
@@ -469,11 +375,9 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
<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>
|
||||
@@ -481,12 +385,10 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
<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>
|
||||
@@ -525,50 +427,48 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
|
||||
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 : '양호'))}
|
||||
<InspectionCheckbox
|
||||
checked={product.bendingStatus === '양호'}
|
||||
onClick={() => handleStatusChange(product.id, product.bendingStatus === '양호' ? null : '양호')}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
양호
|
||||
</label>
|
||||
<label className="flex items-center gap-0.5 cursor-pointer text-xs whitespace-nowrap">
|
||||
{renderCheckbox(product.bendingStatus === '불량', () => handleStatusChange(product.id, product.bendingStatus === '불량' ? null : '불량'))}
|
||||
<InspectionCheckbox
|
||||
checked={product.bendingStatus === '불량'}
|
||||
onClick={() => handleStatusChange(product.id, product.bendingStatus === '불량' ? null : '불량')}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
불량
|
||||
</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="-" />
|
||||
<input type="text" value={product.lengthMeasured} onChange={(e) => handleInputChange(product.id, 'lengthMeasured', e.target.value)} disabled={readOnly} className={INPUT_CLASS} 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="-" />
|
||||
<input type="text" value={product.widthMeasured} onChange={(e) => handleInputChange(product.id, 'widthMeasured', e.target.value)} disabled={readOnly} className={INPUT_CLASS} 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="-" />
|
||||
<input type="text" value={gap.measured} onChange={(e) => handleGapMeasuredChange(product.id, gapIdx, e.target.value)} disabled={readOnly} className={INPUT_CLASS} 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>
|
||||
<JudgmentCell judgment={judgment} rowSpan={rowCount} />
|
||||
)}
|
||||
</tr>
|
||||
));
|
||||
@@ -577,23 +477,12 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
</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>
|
||||
<InspectionFooter
|
||||
readOnly={readOnly}
|
||||
overallResult={overallResult}
|
||||
inadequateContent={inadequateContent}
|
||||
onInadequateContentChange={setInadequateContent}
|
||||
/>
|
||||
</InspectionLayout>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -17,49 +17,66 @@ 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 interface InspectionContentRef {
|
||||
getInspectionData: () => unknown;
|
||||
}
|
||||
export type { InspectionContentRef };
|
||||
|
||||
export interface BendingWipInspectionContentProps {
|
||||
data: WorkOrder;
|
||||
readOnly?: boolean;
|
||||
inspectionData?: InspectionData;
|
||||
/** 작업 아이템 목록 - 행 개수 동적 생성용 */
|
||||
workItems?: WorkItemData[];
|
||||
/** 아이템별 검사 데이터 맵 */
|
||||
inspectionDataMap?: InspectionDataMap;
|
||||
/** 기준서 도해 이미지 URL */
|
||||
schematicImage?: string;
|
||||
/** 검사기준 이미지 URL */
|
||||
inspectionStandardImage?: string;
|
||||
}
|
||||
|
||||
type CheckStatus = '양호' | '불량' | null;
|
||||
|
||||
// 상태 변환 함수: 'good'/'bad' → '양호'/'불량'
|
||||
const convertToCheckStatus = (status: 'good' | 'bad' | null | undefined): CheckStatus => {
|
||||
if (status === 'good') return '양호';
|
||||
if (status === 'bad') return '불량';
|
||||
return null;
|
||||
};
|
||||
|
||||
interface InspectionRow {
|
||||
id: number;
|
||||
itemId?: string; // 작업 아이템 ID (연동용)
|
||||
productName: string; // 제품명
|
||||
processStatus: CheckStatus; // 절곡상태
|
||||
lengthDesign: string; // 길이 도면치수
|
||||
lengthMeasured: string; // 길이 측정값
|
||||
widthDesign: string; // 너비 도면치수
|
||||
widthMeasured: string; // 너비 측정값
|
||||
spacingPoint: string; // 너비 포인트
|
||||
spacingDesign: string; // 간격 도면치수
|
||||
spacingMeasured: string; // 간격 측정값
|
||||
itemId?: string;
|
||||
productName: string;
|
||||
processStatus: CheckStatus;
|
||||
lengthDesign: string;
|
||||
lengthMeasured: string;
|
||||
widthDesign: string;
|
||||
widthMeasured: string;
|
||||
spacingPoint: string;
|
||||
spacingDesign: string;
|
||||
spacingMeasured: string;
|
||||
}
|
||||
|
||||
const DEFAULT_ROW_COUNT = 6;
|
||||
function buildRow(i: number, order: WorkOrder, workItems?: WorkItemData[], inspectionDataMap?: InspectionDataMap): InspectionRow {
|
||||
const item = workItems?.[i];
|
||||
const orderItem = order.items?.[i];
|
||||
const itemData = item && inspectionDataMap?.get(item.id);
|
||||
return {
|
||||
id: i + 1,
|
||||
itemId: item?.id,
|
||||
productName: item?.itemName || orderItem?.productName || '',
|
||||
processStatus: itemData ? convertToCheckStatus(itemData.bendingStatus) : null,
|
||||
lengthDesign: '4000',
|
||||
lengthMeasured: '',
|
||||
widthDesign: 'N/A',
|
||||
widthMeasured: 'N/A',
|
||||
spacingPoint: '',
|
||||
spacingDesign: '380',
|
||||
spacingMeasured: '',
|
||||
};
|
||||
}
|
||||
|
||||
export const BendingWipInspectionContent = forwardRef<InspectionContentRef, BendingWipInspectionContentProps>(function BendingWipInspectionContent({
|
||||
data: order,
|
||||
@@ -67,71 +84,22 @@ export const BendingWipInspectionContent = forwardRef<InspectionContentRef, Bend
|
||||
workItems,
|
||||
inspectionDataMap,
|
||||
schematicImage,
|
||||
inspectionStandardImage,
|
||||
}, ref) {
|
||||
const fullDate = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
const fullDate = getFullDate();
|
||||
const today = getToday();
|
||||
const { documentNo, primaryAssignee } = getOrderInfo(order);
|
||||
|
||||
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 || '-';
|
||||
|
||||
// workItems 기반 행 개수 결정 (workItems가 있으면 그 개수, 없으면 order.items 또는 기본값)
|
||||
const rowCount = workItems?.length || order.items?.length || DEFAULT_ROW_COUNT;
|
||||
|
||||
// 아이템 기반 초기 행 생성
|
||||
const [rows, setRows] = useState<InspectionRow[]>(() => {
|
||||
return Array.from({ length: rowCount }, (_, i) => {
|
||||
const item = workItems?.[i];
|
||||
const orderItem = order.items?.[i];
|
||||
const itemData = item && inspectionDataMap?.get(item.id);
|
||||
return {
|
||||
id: i + 1,
|
||||
itemId: item?.id,
|
||||
productName: item?.itemName || orderItem?.productName || '',
|
||||
processStatus: itemData ? convertToCheckStatus(itemData.bendingStatus) : null,
|
||||
lengthDesign: '4000',
|
||||
lengthMeasured: '',
|
||||
widthDesign: 'N/A',
|
||||
widthMeasured: 'N/A',
|
||||
spacingPoint: '',
|
||||
spacingDesign: '380',
|
||||
spacingMeasured: '',
|
||||
};
|
||||
});
|
||||
});
|
||||
const [rows, setRows] = useState<InspectionRow[]>(() =>
|
||||
Array.from({ length: rowCount }, (_, i) => buildRow(i, order, workItems, inspectionDataMap))
|
||||
);
|
||||
|
||||
const [inadequateContent, setInadequateContent] = useState('');
|
||||
|
||||
// workItems 또는 inspectionDataMap 변경 시 행 업데이트
|
||||
useEffect(() => {
|
||||
const newRowCount = workItems?.length || order.items?.length || DEFAULT_ROW_COUNT;
|
||||
setRows(Array.from({ length: newRowCount }, (_, i) => {
|
||||
const item = workItems?.[i];
|
||||
const orderItem = order.items?.[i];
|
||||
const itemData = item && inspectionDataMap?.get(item.id);
|
||||
return {
|
||||
id: i + 1,
|
||||
itemId: item?.id,
|
||||
productName: item?.itemName || orderItem?.productName || '',
|
||||
processStatus: itemData ? convertToCheckStatus(itemData.bendingStatus) : null,
|
||||
lengthDesign: '4000',
|
||||
lengthMeasured: '',
|
||||
widthDesign: 'N/A',
|
||||
widthMeasured: 'N/A',
|
||||
spacingPoint: '',
|
||||
spacingDesign: '380',
|
||||
spacingMeasured: '',
|
||||
};
|
||||
}));
|
||||
setRows(Array.from({ length: newRowCount }, (_, i) => buildRow(i, order, workItems, inspectionDataMap)));
|
||||
}, [workItems, inspectionDataMap, order.items]);
|
||||
|
||||
const handleStatusChange = useCallback((rowId: number, value: CheckStatus) => {
|
||||
@@ -156,20 +124,13 @@ export const BendingWipInspectionContent = forwardRef<InspectionContentRef, Bend
|
||||
));
|
||||
}, [readOnly]);
|
||||
|
||||
// 행별 판정 자동 계산
|
||||
const getRowJudgment = useCallback((row: InspectionRow): '적' | '부' | null => {
|
||||
if (row.processStatus === '불량') return '부';
|
||||
if (row.processStatus === '양호') return '적';
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
// 종합판정 자동 계산
|
||||
const overallResult = useMemo(() => {
|
||||
const judgments = rows.map(getRowJudgment);
|
||||
if (judgments.some(j => j === '부')) return '불합격';
|
||||
if (judgments.every(j => j === '적')) return '합격';
|
||||
return null;
|
||||
}, [rows, getRowJudgment]);
|
||||
const overallResult = useMemo(() => calculateOverallResult(rows.map(getRowJudgment)), [rows, getRowJudgment]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getInspectionData: () => ({
|
||||
@@ -187,59 +148,8 @@ export const BendingWipInspectionContent = forwardRef<InspectionContentRef, Bend
|
||||
}),
|
||||
}), [rows, 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';
|
||||
|
||||
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>
|
||||
|
||||
<InspectionLayout title="절곡품 재고생산 작업일지 중간검사성적서" documentNo={documentNo} fullDate={fullDate} primaryAssignee={primaryAssignee}>
|
||||
{/* ===== 기본 정보 ===== */}
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<tbody>
|
||||
@@ -284,7 +194,6 @@ export const BendingWipInspectionContent = forwardRef<InspectionContentRef, Bend
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
{/* 도해 영역 - 넓게 */}
|
||||
<td className="border border-gray-400 p-3 text-center align-middle" rowSpan={4}>
|
||||
<div className="text-xs font-medium text-gray-500 mb-2 text-left">도해</div>
|
||||
{schematicImage ? (
|
||||
@@ -293,14 +202,12 @@ export const BendingWipInspectionContent = forwardRef<InspectionContentRef, Bend
|
||||
<div className="h-32 border border-gray-300 rounded flex items-center justify-center text-gray-300 text-sm">IMG</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">겉모양</td>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium">절곡상태</td>
|
||||
@@ -309,7 +216,6 @@ export const BendingWipInspectionContent = forwardRef<InspectionContentRef, Bend
|
||||
<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>
|
||||
@@ -317,7 +223,6 @@ export const BendingWipInspectionContent = forwardRef<InspectionContentRef, Bend
|
||||
<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>
|
||||
@@ -355,58 +260,26 @@ export const BendingWipInspectionContent = forwardRef<InspectionContentRef, Bend
|
||||
return (
|
||||
<tr key={row.id}>
|
||||
<td className="border border-gray-400 p-1 text-center">{row.id}</td>
|
||||
{/* 제품명 */}
|
||||
<td className="border border-gray-400 p-1">
|
||||
<input
|
||||
type="text"
|
||||
value={row.productName}
|
||||
onChange={(e) => handleInputChange(row.id, 'productName', e.target.value)}
|
||||
disabled={readOnly}
|
||||
className={inputClass}
|
||||
placeholder="-"
|
||||
/>
|
||||
<input type="text" value={row.productName} onChange={(e) => handleInputChange(row.id, 'productName', e.target.value)} disabled={readOnly} className={INPUT_CLASS} placeholder="-" />
|
||||
</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 whitespace-nowrap">
|
||||
{renderCheckbox(row.processStatus === '양호', () => handleStatusChange(row.id, row.processStatus === '양호' ? null : '양호'))}
|
||||
양호
|
||||
</label>
|
||||
<label className="flex items-center gap-0.5 cursor-pointer text-xs whitespace-nowrap">
|
||||
{renderCheckbox(row.processStatus === '불량', () => handleStatusChange(row.id, row.processStatus === '불량' ? null : '불량'))}
|
||||
불량
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
{/* 길이 - 도면치수 */}
|
||||
<CheckStatusCell value={row.processStatus} onToggle={(v) => handleStatusChange(row.id, 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={row.lengthMeasured} onChange={(e) => handleNumericInput(row.id, 'lengthMeasured', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
|
||||
<input type="text" value={row.lengthMeasured} onChange={(e) => handleNumericInput(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={row.widthMeasured} onChange={(e) => handleInputChange(row.id, 'widthMeasured', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
|
||||
<input type="text" value={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">
|
||||
<input type="text" value={row.spacingPoint} onChange={(e) => handleInputChange(row.id, 'spacingPoint', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
|
||||
<input type="text" value={row.spacingPoint} onChange={(e) => handleInputChange(row.id, 'spacingPoint', e.target.value)} disabled={readOnly} className={INPUT_CLASS} placeholder="-" />
|
||||
</td>
|
||||
{/* 간격 - 도면치수 */}
|
||||
<td className="border border-gray-400 p-1 text-center">{row.spacingDesign}</td>
|
||||
{/* 간격 - 측정값 */}
|
||||
<td className="border border-gray-400 p-1">
|
||||
<input type="text" value={row.spacingMeasured} onChange={(e) => handleNumericInput(row.id, 'spacingMeasured', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
|
||||
</td>
|
||||
{/* 판정 - 자동 계산 */}
|
||||
<td className={`border border-gray-400 p-1 text-center font-bold ${
|
||||
judgment === '적' ? 'text-blue-600' : judgment === '부' ? 'text-red-600' : 'text-gray-300'
|
||||
}`}>
|
||||
{judgment || '-'}
|
||||
<input type="text" value={row.spacingMeasured} onChange={(e) => handleNumericInput(row.id, 'spacingMeasured', e.target.value)} disabled={readOnly} className={INPUT_CLASS} placeholder="-" />
|
||||
</td>
|
||||
<JudgmentCell judgment={judgment} />
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
@@ -414,23 +287,12 @@ export const BendingWipInspectionContent = forwardRef<InspectionContentRef, Bend
|
||||
</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>
|
||||
<InspectionFooter
|
||||
readOnly={readOnly}
|
||||
overallResult={overallResult}
|
||||
inadequateContent={inadequateContent}
|
||||
onInadequateContentChange={setInadequateContent}
|
||||
/>
|
||||
</InspectionLayout>
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,8 @@
|
||||
* - screen: ScreenInspectionContent
|
||||
* - slat: SlatInspectionContent
|
||||
* - bending: BendingInspectionContent
|
||||
*
|
||||
* Phase 4: API 연동 - getInspectionReport로 검사 데이터 자체 로딩
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
@@ -14,8 +16,9 @@ import { Loader2, Save } from 'lucide-react';
|
||||
import { DocumentViewer } from '@/components/document-system';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from 'sonner';
|
||||
import { getWorkOrderById, saveInspectionData } from '../actions';
|
||||
import { getWorkOrderById, saveInspectionData, getInspectionReport } from '../actions';
|
||||
import type { WorkOrder, ProcessType } from '../types';
|
||||
import type { InspectionReportData } from '../actions';
|
||||
import { ScreenInspectionContent } from './ScreenInspectionContent';
|
||||
import { SlatInspectionContent } from './SlatInspectionContent';
|
||||
import { BendingInspectionContent } from './BendingInspectionContent';
|
||||
@@ -52,6 +55,49 @@ interface InspectionReportModalProps {
|
||||
inspectionSetting?: InspectionSetting;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 응답의 inspection_data를 InspectionData 형식으로 변환
|
||||
*/
|
||||
function toInspectionData(raw: Record<string, unknown>): InspectionData {
|
||||
return raw as unknown as InspectionData;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 응답의 items를 WorkItemData[] + InspectionDataMap으로 변환
|
||||
*/
|
||||
function buildFromReportItems(
|
||||
items: InspectionReportData['items'],
|
||||
processType: ProcessType
|
||||
): { workItems: WorkItemData[]; inspectionDataMap: InspectionDataMap } {
|
||||
const workItems: WorkItemData[] = [];
|
||||
const inspectionDataMap: InspectionDataMap = new Map();
|
||||
|
||||
for (const item of items) {
|
||||
const syntheticId = `report-item-${item.id}`;
|
||||
|
||||
workItems.push({
|
||||
id: syntheticId,
|
||||
apiItemId: item.id,
|
||||
itemNo: item.sort_order,
|
||||
itemCode: '',
|
||||
itemName: item.item_name,
|
||||
floor: '',
|
||||
code: '',
|
||||
width: 0,
|
||||
height: 0,
|
||||
quantity: item.quantity,
|
||||
processType: processType === 'bending_wip' ? 'bending' : processType,
|
||||
steps: [],
|
||||
});
|
||||
|
||||
if (item.inspection_data) {
|
||||
inspectionDataMap.set(syntheticId, toInspectionData(item.inspection_data));
|
||||
}
|
||||
}
|
||||
|
||||
return { workItems, inspectionDataMap };
|
||||
}
|
||||
|
||||
export function InspectionReportModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -60,8 +106,8 @@ export function InspectionReportModal({
|
||||
readOnly = true,
|
||||
isJointBar = false,
|
||||
inspectionData,
|
||||
workItems,
|
||||
inspectionDataMap,
|
||||
workItems: propWorkItems,
|
||||
inspectionDataMap: propInspectionDataMap,
|
||||
inspectionSetting,
|
||||
}: InspectionReportModalProps) {
|
||||
const [order, setOrder] = useState<WorkOrder | null>(null);
|
||||
@@ -70,6 +116,15 @@ export function InspectionReportModal({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const contentRef = useRef<InspectionContentRef>(null);
|
||||
|
||||
// API에서 로딩된 검사 데이터 (props보다 우선)
|
||||
const [apiWorkItems, setApiWorkItems] = useState<WorkItemData[] | null>(null);
|
||||
const [apiInspectionDataMap, setApiInspectionDataMap] = useState<InspectionDataMap | null>(null);
|
||||
const [reportSummary, setReportSummary] = useState<InspectionReportData['summary'] | null>(null);
|
||||
|
||||
// 최종 사용할 데이터: API 데이터 우선, 없으면 props fallback
|
||||
const effectiveWorkItems = apiWorkItems || propWorkItems;
|
||||
const effectiveInspectionDataMap = apiInspectionDataMap || propInspectionDataMap;
|
||||
|
||||
// 목업 WorkOrder 생성
|
||||
const createMockOrder = (id: string, pType: ProcessType): WorkOrder => ({
|
||||
id,
|
||||
@@ -110,6 +165,9 @@ export function InspectionReportModal({
|
||||
// 목업 ID인 경우 API 호출 생략
|
||||
if (workOrderId.startsWith('mock-')) {
|
||||
setOrder(createMockOrder(workOrderId, processType));
|
||||
setApiWorkItems(null);
|
||||
setApiInspectionDataMap(null);
|
||||
setReportSummary(null);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
@@ -117,12 +175,42 @@ export function InspectionReportModal({
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
getWorkOrderById(workOrderId)
|
||||
.then((result) => {
|
||||
if (result.success && result.data) {
|
||||
setOrder(result.data);
|
||||
// 작업지시 기본정보 + 검사 성적서 데이터 동시 로딩
|
||||
Promise.all([
|
||||
getWorkOrderById(workOrderId),
|
||||
getInspectionReport(workOrderId),
|
||||
])
|
||||
.then(([orderResult, reportResult]) => {
|
||||
// 1) WorkOrder 기본정보
|
||||
if (orderResult.success && orderResult.data) {
|
||||
const orderData = orderResult.data;
|
||||
// API 성적서 응답에서 수주 정보 보강
|
||||
if (reportResult.success && reportResult.data?.order) {
|
||||
const reportOrder = reportResult.data.order;
|
||||
if (reportOrder.client_name && !orderData.client) {
|
||||
orderData.client = reportOrder.client_name;
|
||||
}
|
||||
if (reportOrder.site_name && !orderData.projectName) {
|
||||
orderData.projectName = reportOrder.site_name;
|
||||
}
|
||||
}
|
||||
setOrder(orderData);
|
||||
} else {
|
||||
setError(result.error || '데이터를 불러올 수 없습니다.');
|
||||
setError(orderResult.error || '데이터를 불러올 수 없습니다.');
|
||||
}
|
||||
|
||||
// 2) 검사 성적서 데이터 → workItems + inspectionDataMap 구성
|
||||
if (reportResult.success && reportResult.data) {
|
||||
const { workItems: apiItems, inspectionDataMap: apiMap } =
|
||||
buildFromReportItems(reportResult.data.items, processType);
|
||||
setApiWorkItems(apiItems);
|
||||
setApiInspectionDataMap(apiMap);
|
||||
setReportSummary(reportResult.data.summary);
|
||||
} else {
|
||||
// 성적서 API 실패해도 기본 WorkOrder는 표시 (fallback to props)
|
||||
setApiWorkItems(null);
|
||||
setApiInspectionDataMap(null);
|
||||
setReportSummary(null);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -133,6 +221,9 @@ export function InspectionReportModal({
|
||||
});
|
||||
} else if (!open) {
|
||||
setOrder(null);
|
||||
setApiWorkItems(null);
|
||||
setApiInspectionDataMap(null);
|
||||
setReportSummary(null);
|
||||
setError(null);
|
||||
}
|
||||
}, [open, workOrderId, processType]);
|
||||
@@ -169,14 +260,14 @@ export function InspectionReportModal({
|
||||
const renderContent = () => {
|
||||
if (!order) return null;
|
||||
|
||||
// 공통 props
|
||||
// 공통 props - API 데이터 우선, props fallback
|
||||
const commonProps = {
|
||||
ref: contentRef,
|
||||
data: order,
|
||||
readOnly,
|
||||
inspectionData,
|
||||
workItems,
|
||||
inspectionDataMap,
|
||||
workItems: effectiveWorkItems,
|
||||
inspectionDataMap: effectiveInspectionDataMap,
|
||||
// 중간검사 설정에서 등록한 이미지
|
||||
schematicImage: inspectionSetting?.schematicImage,
|
||||
inspectionStandardImage: inspectionSetting?.inspectionStandardImage,
|
||||
@@ -211,6 +302,11 @@ export function InspectionReportModal({
|
||||
</Button>
|
||||
) : undefined;
|
||||
|
||||
// 검사 진행 상태 표시 (summary 있을 때)
|
||||
const summaryInfo = reportSummary && reportSummary.total_items > 0
|
||||
? `검사 ${reportSummary.inspected_items}/${reportSummary.total_items}건`
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<DocumentViewer
|
||||
title={modalTitle}
|
||||
@@ -229,8 +325,20 @@ export function InspectionReportModal({
|
||||
<p className="text-muted-foreground">{error || '데이터를 불러올 수 없습니다.'}</p>
|
||||
</div>
|
||||
) : (
|
||||
renderContent()
|
||||
<>
|
||||
{summaryInfo && (
|
||||
<div className="px-6 pt-2 text-xs text-gray-500 bg-white">
|
||||
{summaryInfo}
|
||||
{reportSummary!.failed_items > 0 && (
|
||||
<span className="ml-2 text-red-500">
|
||||
(부적합 {reportSummary!.failed_items}건)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{renderContent()}
|
||||
</>
|
||||
)}
|
||||
</DocumentViewer>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,7 @@
|
||||
/**
|
||||
* 스크린 중간검사 성적서 문서 콘텐츠
|
||||
*
|
||||
* 기획서 기준:
|
||||
* - 헤더: "중간검사성적서 (스크린)" + 결재란
|
||||
* - 기본정보: 제품명/스크린, 규격/와이어 글라스 코팅직물, 수주처, 현장명 | 제품LOT NO, 로트크기, 검사일자, 검사자
|
||||
* - ■ 중간검사 기준서: 도해 + 검사항목/검사기준/검사방법/검사주기/관련규정
|
||||
* 가공상태, 재봉상태, 조립상태, 치수(길이/높이/간격)
|
||||
* - ■ 중간검사 DATA: No, 가공상태결모양(양호/불량), 재봉상태결모양(양호/불량), 조립상태(양호/불량),
|
||||
* 길이(도면치수/측정값입력), 나비(도면치수/측정값입력), 간격(기준치/OK·NG선택), 판정(자동)
|
||||
* - 부적합 내용 / 종합판정(자동)
|
||||
* 검사 항목: 가공상태, 재봉상태, 조립상태, 길이, 나비, 간격(OK/NG)
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo, forwardRef, useImperativeHandle, useEffect } from 'react';
|
||||
@@ -18,44 +11,70 @@ 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 GapResult,
|
||||
type InspectionContentRef,
|
||||
convertToCheckStatus,
|
||||
convertToGapResult,
|
||||
getFullDate,
|
||||
getToday,
|
||||
getOrderInfo,
|
||||
INPUT_CLASS,
|
||||
DEFAULT_ROW_COUNT,
|
||||
InspectionCheckbox,
|
||||
CheckStatusCell,
|
||||
InspectionLayout,
|
||||
InspectionFooter,
|
||||
JudgmentCell,
|
||||
calculateOverallResult,
|
||||
} from './inspection-shared';
|
||||
|
||||
export interface InspectionContentRef {
|
||||
getInspectionData: () => unknown;
|
||||
}
|
||||
export type { InspectionContentRef };
|
||||
|
||||
export interface ScreenInspectionContentProps {
|
||||
data: WorkOrder;
|
||||
readOnly?: boolean;
|
||||
inspectionData?: InspectionData;
|
||||
/** 작업 아이템 목록 - 행 개수 동적 생성용 */
|
||||
workItems?: WorkItemData[];
|
||||
/** 아이템별 검사 데이터 맵 */
|
||||
inspectionDataMap?: InspectionDataMap;
|
||||
/** 기준서 도해 이미지 URL */
|
||||
schematicImage?: string;
|
||||
/** 검사기준 이미지 URL */
|
||||
inspectionStandardImage?: string;
|
||||
}
|
||||
|
||||
type CheckStatus = '양호' | '불량' | null;
|
||||
type GapResult = 'OK' | 'NG' | null;
|
||||
|
||||
interface InspectionRow {
|
||||
id: number;
|
||||
itemId?: string; // 작업 아이템 ID
|
||||
itemName?: string; // 작업 아이템 이름
|
||||
processStatus: CheckStatus; // 가공상태 결모양
|
||||
sewingStatus: CheckStatus; // 재봉상태 결모양
|
||||
assemblyStatus: CheckStatus; // 조립상태
|
||||
lengthDesign: string; // 길이 도면치수 (표시용)
|
||||
lengthMeasured: string; // 길이 측정값 (입력)
|
||||
widthDesign: string; // 나비 도면치수 (표시용)
|
||||
widthMeasured: string; // 나비 측정값 (입력)
|
||||
gapStandard: string; // 간격 기준치 (표시용)
|
||||
gapResult: GapResult; // 간격 측정값 (OK/NG 선택)
|
||||
itemId?: string;
|
||||
itemName?: string;
|
||||
processStatus: CheckStatus;
|
||||
sewingStatus: CheckStatus;
|
||||
assemblyStatus: CheckStatus;
|
||||
lengthDesign: string;
|
||||
lengthMeasured: string;
|
||||
widthDesign: string;
|
||||
widthMeasured: string;
|
||||
gapStandard: string;
|
||||
gapResult: GapResult;
|
||||
}
|
||||
|
||||
const DEFAULT_ROW_COUNT = 6;
|
||||
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,
|
||||
@@ -63,95 +82,27 @@ export const ScreenInspectionContent = forwardRef<InspectionContentRef, ScreenIn
|
||||
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 || '-';
|
||||
|
||||
// 행 개수: workItems가 있으면 그 개수, 없으면 기본값
|
||||
const fullDate = getFullDate();
|
||||
const today = getToday();
|
||||
const { documentNo, primaryAssignee } = getOrderInfo(order);
|
||||
const rowCount = workItems?.length || DEFAULT_ROW_COUNT;
|
||||
|
||||
// InspectionData를 InspectionRow로 변환하는 함수
|
||||
const convertToCheckStatus = (status: 'good' | 'bad' | null | undefined): CheckStatus => {
|
||||
if (status === 'good') return '양호';
|
||||
if (status === 'bad') return '불량';
|
||||
return null;
|
||||
};
|
||||
const [rows, setRows] = useState<InspectionRow[]>(() =>
|
||||
Array.from({ length: rowCount }, (_, i) => buildRow(i, workItems, inspectionDataMap))
|
||||
);
|
||||
const [inadequateContent, setInadequateContent] = useState('');
|
||||
|
||||
const convertToGapResult = (status: 'ok' | 'ng' | null | undefined): GapResult => {
|
||||
if (status === 'ok') return 'OK';
|
||||
if (status === 'ng') return 'NG';
|
||||
return null;
|
||||
};
|
||||
|
||||
const [rows, setRows] = useState<InspectionRow[]>(() => {
|
||||
return Array.from({ length: rowCount }, (_, i) => {
|
||||
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,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// workItems나 inspectionDataMap이 변경되면 rows 업데이트
|
||||
useEffect(() => {
|
||||
const newRowCount = workItems?.length || DEFAULT_ROW_COUNT;
|
||||
setRows(Array.from({ length: newRowCount }, (_, i) => {
|
||||
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,
|
||||
};
|
||||
}));
|
||||
setRows(Array.from({ length: newRowCount }, (_, i) => buildRow(i, workItems, inspectionDataMap)));
|
||||
}, [workItems, inspectionDataMap]);
|
||||
|
||||
const [inadequateContent, setInadequateContent] = useState('');
|
||||
|
||||
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
|
||||
));
|
||||
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 '';
|
||||
@@ -160,41 +111,23 @@ export const ScreenInspectionContent = forwardRef<InspectionContentRef, ScreenIn
|
||||
|
||||
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
|
||||
));
|
||||
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
|
||||
));
|
||||
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;
|
||||
// 하나라도 불량 or NG → 부
|
||||
if (processStatus === '불량' || sewingStatus === '불량' || assemblyStatus === '불량' || gapResult === 'NG') {
|
||||
return '부';
|
||||
}
|
||||
// 모두 양호 + OK → 적
|
||||
if (processStatus === '양호' && sewingStatus === '양호' && assemblyStatus === '양호' && gapResult === 'OK') {
|
||||
return '적';
|
||||
}
|
||||
if (processStatus === '불량' || sewingStatus === '불량' || assemblyStatus === '불량' || gapResult === 'NG') return '부';
|
||||
if (processStatus === '양호' && sewingStatus === '양호' && assemblyStatus === '양호' && gapResult === 'OK') return '적';
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
// 종합판정 자동 계산
|
||||
const overallResult = useMemo(() => {
|
||||
const judgments = rows.map(getRowJudgment);
|
||||
if (judgments.some(j => j === '부')) return '불합격';
|
||||
if (judgments.every(j => j === '적')) return '합격';
|
||||
return null;
|
||||
}, [rows, getRowJudgment]);
|
||||
const overallResult = useMemo(() => calculateOverallResult(rows.map(getRowJudgment)), [rows, getRowJudgment]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getInspectionData: () => ({
|
||||
@@ -212,75 +145,9 @@ export const ScreenInspectionContent = forwardRef<InspectionContentRef, ScreenIn
|
||||
}),
|
||||
}), [rows, 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 renderCheckStatus = (rowId: number, field: 'processStatus' | 'sewingStatus' | 'assemblyStatus', value: CheckStatus) => (
|
||||
<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 whitespace-nowrap">
|
||||
{renderCheckbox(value === '양호', () => handleStatusChange(rowId, field, value === '양호' ? null : '양호'))}
|
||||
양호
|
||||
</label>
|
||||
<label className="flex items-center gap-0.5 cursor-pointer text-xs whitespace-nowrap">
|
||||
{renderCheckbox(value === '불량', () => handleStatusChange(rowId, field, value === '불량' ? null : '불량'))}
|
||||
불량
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
|
||||
const inputClass = 'w-full text-center border-0 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-xs';
|
||||
|
||||
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>
|
||||
|
||||
{/* ===== 기본 정보 ===== */}
|
||||
<InspectionLayout title="중간검사성적서 (스크린)" documentNo={documentNo} fullDate={fullDate} primaryAssignee={primaryAssignee}>
|
||||
{/* 기본 정보 */}
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -310,12 +177,11 @@ export const ScreenInspectionContent = forwardRef<InspectionContentRef, ScreenIn
|
||||
</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/4" rowSpan={8}>
|
||||
{schematicImage ? (
|
||||
<img src={schematicImage} alt="기준서 도해" className="max-h-40 mx-auto object-contain" />
|
||||
@@ -323,7 +189,6 @@ export const ScreenInspectionContent = forwardRef<InspectionContentRef, ScreenIn
|
||||
<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>
|
||||
@@ -376,7 +241,7 @@ export const ScreenInspectionContent = forwardRef<InspectionContentRef, ScreenIn
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 중간검사 DATA ===== */}
|
||||
{/* 중간검사 DATA */}
|
||||
<div className="mb-1 font-bold text-sm">■ 중간검사 DATA</div>
|
||||
<table className="w-full border-collapse text-xs mb-4">
|
||||
<thead>
|
||||
@@ -405,66 +270,38 @@ export const ScreenInspectionContent = forwardRef<InspectionContentRef, ScreenIn
|
||||
return (
|
||||
<tr key={row.id}>
|
||||
<td className="border border-gray-400 p-1 text-center">{row.id}</td>
|
||||
{/* 가공상태 - 양호/불량 체크 */}
|
||||
{renderCheckStatus(row.id, 'processStatus', row.processStatus)}
|
||||
{/* 재봉상태 - 양호/불량 체크 */}
|
||||
{renderCheckStatus(row.id, 'sewingStatus', row.sewingStatus)}
|
||||
{/* 조립상태 - 양호/불량 체크 */}
|
||||
{renderCheckStatus(row.id, 'assemblyStatus', row.assemblyStatus)}
|
||||
{/* 길이 - 도면치수 표시 + 측정값 입력 */}
|
||||
<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={inputClass} placeholder="-" />
|
||||
<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={inputClass} placeholder="-" />
|
||||
<input type="text" value={formatNumberWithComma(row.widthMeasured)} onChange={(e) => handleInputChange(row.id, 'widthMeasured', e.target.value)} disabled={readOnly} className={INPUT_CLASS} placeholder="-" />
|
||||
</td>
|
||||
{/* 간격 - 기준치 표시 + OK/NG 선택 */}
|
||||
<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">
|
||||
{renderCheckbox(row.gapResult === 'OK', () => handleGapChange(row.id, row.gapResult === 'OK' ? null : 'OK'))}
|
||||
<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">
|
||||
{renderCheckbox(row.gapResult === 'NG', () => handleGapChange(row.id, row.gapResult === 'NG' ? null : 'NG'))}
|
||||
<InspectionCheckbox checked={row.gapResult === 'NG'} onClick={() => handleGapChange(row.id, row.gapResult === 'NG' ? null : 'NG')} readOnly={readOnly} />
|
||||
NG
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
{/* 판정 - 자동 계산 */}
|
||||
<td className={`border border-gray-400 p-1 text-center font-bold ${
|
||||
judgment === '적' ? 'text-blue-600' : judgment === '부' ? 'text-red-600' : 'text-gray-300'
|
||||
}`}>
|
||||
{judgment || '-'}
|
||||
</td>
|
||||
<JudgmentCell judgment={judgment} />
|
||||
</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>
|
||||
<InspectionFooter readOnly={readOnly} overallResult={overallResult} inadequateContent={inadequateContent} onInadequateContentChange={setInadequateContent} />
|
||||
</InspectionLayout>
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -19,42 +19,65 @@ 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 interface InspectionContentRef {
|
||||
getInspectionData: () => unknown;
|
||||
}
|
||||
export type { InspectionContentRef };
|
||||
|
||||
export interface SlatInspectionContentProps {
|
||||
data: WorkOrder;
|
||||
readOnly?: boolean;
|
||||
inspectionData?: InspectionData;
|
||||
/** 작업 아이템 목록 - 행 개수 동적 생성용 */
|
||||
workItems?: WorkItemData[];
|
||||
/** 아이템별 검사 데이터 맵 */
|
||||
inspectionDataMap?: InspectionDataMap;
|
||||
/** 기준서 도해 이미지 URL */
|
||||
schematicImage?: string;
|
||||
/** 검사기준 이미지 URL */
|
||||
inspectionStandardImage?: string;
|
||||
}
|
||||
|
||||
type CheckStatus = '양호' | '불량' | null;
|
||||
|
||||
interface InspectionRow {
|
||||
id: number;
|
||||
itemId?: string; // 작업 아이템 ID
|
||||
itemName?: string; // 작업 아이템 이름
|
||||
processStatus: CheckStatus; // 가공상태 결모양
|
||||
assemblyStatus: CheckStatus; // 조립상태 결모양
|
||||
height1Standard: string; // ① 높이 기준치 (표시용)
|
||||
height1Measured: string; // ① 높이 측정값 (입력)
|
||||
height2Standard: string; // ② 높이 기준치 (표시용)
|
||||
height2Measured: string; // ② 높이 측정값 (입력)
|
||||
lengthDesign: string; // 길이 도면치수 (입력)
|
||||
lengthMeasured: string; // 길이 측정값 (입력)
|
||||
itemId?: string;
|
||||
itemName?: string;
|
||||
processStatus: CheckStatus;
|
||||
assemblyStatus: CheckStatus;
|
||||
height1Standard: string;
|
||||
height1Measured: string;
|
||||
height2Standard: string;
|
||||
height2Measured: string;
|
||||
lengthDesign: string;
|
||||
lengthMeasured: string;
|
||||
}
|
||||
|
||||
const DEFAULT_ROW_COUNT = 6;
|
||||
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,
|
||||
@@ -62,75 +85,20 @@ export const SlatInspectionContent = forwardRef<InspectionContentRef, SlatInspec
|
||||
workItems,
|
||||
inspectionDataMap,
|
||||
schematicImage,
|
||||
inspectionStandardImage,
|
||||
}, ref) {
|
||||
const fullDate = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
const fullDate = getFullDate();
|
||||
const today = getToday();
|
||||
const { documentNo, primaryAssignee } = getOrderInfo(order);
|
||||
|
||||
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 || '-';
|
||||
|
||||
// 행 개수: workItems가 있으면 그 개수, 없으면 기본값
|
||||
const rowCount = workItems?.length || DEFAULT_ROW_COUNT;
|
||||
|
||||
// InspectionData를 InspectionRow로 변환하는 함수
|
||||
const convertToCheckStatus = (status: 'good' | 'bad' | null | undefined): CheckStatus => {
|
||||
if (status === 'good') return '양호';
|
||||
if (status === 'bad') return '불량';
|
||||
return null;
|
||||
};
|
||||
const [rows, setRows] = useState<InspectionRow[]>(() =>
|
||||
Array.from({ length: rowCount }, (_, i) => buildRow(i, workItems, inspectionDataMap))
|
||||
);
|
||||
|
||||
const [rows, setRows] = useState<InspectionRow[]>(() => {
|
||||
return Array.from({ length: rowCount }, (_, i) => {
|
||||
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 ± 1',
|
||||
height1Measured: itemData?.height1?.toString() || '',
|
||||
height2Standard: '14.5 ± 1',
|
||||
height2Measured: itemData?.height2?.toString() || '',
|
||||
lengthDesign: '0',
|
||||
lengthMeasured: itemData?.length?.toString() || '',
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// workItems나 inspectionDataMap이 변경되면 rows 업데이트
|
||||
useEffect(() => {
|
||||
const newRowCount = workItems?.length || DEFAULT_ROW_COUNT;
|
||||
setRows(Array.from({ length: newRowCount }, (_, i) => {
|
||||
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 ± 1',
|
||||
height1Measured: itemData?.height1?.toString() || '',
|
||||
height2Standard: '14.5 ± 1',
|
||||
height2Measured: itemData?.height2?.toString() || '',
|
||||
lengthDesign: '0',
|
||||
lengthMeasured: itemData?.length?.toString() || '',
|
||||
};
|
||||
}));
|
||||
setRows(Array.from({ length: newRowCount }, (_, i) => buildRow(i, workItems, inspectionDataMap)));
|
||||
}, [workItems, inspectionDataMap]);
|
||||
|
||||
const [inadequateContent, setInadequateContent] = useState('');
|
||||
@@ -144,14 +112,12 @@ export const SlatInspectionContent = forwardRef<InspectionContentRef, SlatInspec
|
||||
|
||||
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 '부';
|
||||
@@ -159,13 +125,7 @@ export const SlatInspectionContent = forwardRef<InspectionContentRef, SlatInspec
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
// 종합판정 자동 계산
|
||||
const overallResult = useMemo(() => {
|
||||
const judgments = rows.map(getRowJudgment);
|
||||
if (judgments.some(j => j === '부')) return '불합격';
|
||||
if (judgments.every(j => j === '적')) return '합격';
|
||||
return null;
|
||||
}, [rows, getRowJudgment]);
|
||||
const overallResult = useMemo(() => calculateOverallResult(rows.map(getRowJudgment)), [rows, getRowJudgment]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getInspectionData: () => ({
|
||||
@@ -182,74 +142,8 @@ export const SlatInspectionContent = forwardRef<InspectionContentRef, SlatInspec
|
||||
}),
|
||||
}), [rows, 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 renderCheckStatus = (rowId: number, field: 'processStatus' | 'assemblyStatus', value: CheckStatus) => (
|
||||
<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 whitespace-nowrap">
|
||||
{renderCheckbox(value === '양호', () => handleStatusChange(rowId, field, value === '양호' ? null : '양호'))}
|
||||
양호
|
||||
</label>
|
||||
<label className="flex items-center gap-0.5 cursor-pointer text-xs whitespace-nowrap">
|
||||
{renderCheckbox(value === '불량', () => handleStatusChange(rowId, field, value === '불량' ? null : '불량'))}
|
||||
불량
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
|
||||
const inputClass = 'w-full text-center border-0 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-xs';
|
||||
|
||||
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>
|
||||
|
||||
<InspectionLayout title="중간검사성적서 (슬랫)" documentNo={documentNo} fullDate={fullDate} primaryAssignee={primaryAssignee}>
|
||||
{/* ===== 기본 정보 ===== */}
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<tbody>
|
||||
@@ -285,7 +179,6 @@ export const SlatInspectionContent = forwardRef<InspectionContentRef, SlatInspec
|
||||
<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" />
|
||||
@@ -293,14 +186,12 @@ export const SlatInspectionContent = forwardRef<InspectionContentRef, SlatInspec
|
||||
<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>
|
||||
@@ -309,18 +200,15 @@ export const SlatInspectionContent = forwardRef<InspectionContentRef, SlatInspec
|
||||
<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>
|
||||
@@ -330,12 +218,10 @@ export const SlatInspectionContent = forwardRef<InspectionContentRef, SlatInspec
|
||||
<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>
|
||||
@@ -372,31 +258,21 @@ export const SlatInspectionContent = forwardRef<InspectionContentRef, SlatInspec
|
||||
return (
|
||||
<tr key={row.id}>
|
||||
<td className="border border-gray-400 p-1 text-center">{row.id}</td>
|
||||
{/* 가공상태 - 양호/불량 체크 */}
|
||||
{renderCheckStatus(row.id, 'processStatus', row.processStatus)}
|
||||
{/* 조립상태 - 양호/불량 체크 */}
|
||||
{renderCheckStatus(row.id, 'assemblyStatus', row.assemblyStatus)}
|
||||
{/* ① 높이 - 기준치 표시 + 측정값 입력 */}
|
||||
<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={inputClass} placeholder="-" />
|
||||
<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={inputClass} placeholder="-" />
|
||||
<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={inputClass} placeholder="-" />
|
||||
</td>
|
||||
{/* 판정 - 자동 계산 */}
|
||||
<td className={`border border-gray-400 p-1 text-center font-bold ${
|
||||
judgment === '적' ? 'text-blue-600' : judgment === '부' ? 'text-red-600' : 'text-gray-300'
|
||||
}`}>
|
||||
{judgment || '-'}
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
@@ -404,23 +280,12 @@ export const SlatInspectionContent = forwardRef<InspectionContentRef, SlatInspec
|
||||
</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>
|
||||
<InspectionFooter
|
||||
readOnly={readOnly}
|
||||
overallResult={overallResult}
|
||||
inadequateContent={inadequateContent}
|
||||
onInadequateContentChange={setInadequateContent}
|
||||
/>
|
||||
</InspectionLayout>
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -18,51 +18,69 @@ 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 interface InspectionContentRef {
|
||||
getInspectionData: () => unknown;
|
||||
}
|
||||
export type { InspectionContentRef };
|
||||
|
||||
export interface SlatJointBarInspectionContentProps {
|
||||
data: WorkOrder;
|
||||
readOnly?: boolean;
|
||||
inspectionData?: InspectionData;
|
||||
/** 작업 아이템 목록 - 행 개수 동적 생성용 */
|
||||
workItems?: WorkItemData[];
|
||||
/** 아이템별 검사 데이터 맵 */
|
||||
inspectionDataMap?: InspectionDataMap;
|
||||
/** 기준서 도해 이미지 URL */
|
||||
schematicImage?: string;
|
||||
/** 검사기준 이미지 URL */
|
||||
inspectionStandardImage?: string;
|
||||
}
|
||||
|
||||
type CheckStatus = '양호' | '불량' | null;
|
||||
|
||||
interface InspectionRow {
|
||||
id: number;
|
||||
itemId?: string; // 작업 아이템 ID (연동용)
|
||||
itemName?: string; // 작업 아이템명 (연동용)
|
||||
processStatus: CheckStatus; // 가공상태
|
||||
assemblyStatus: CheckStatus; // 조립상태
|
||||
height1Standard: string; // ①높이 기준치
|
||||
height1Measured: string; // ①높이 측정값
|
||||
height2Standard: string; // ②높이 기준치
|
||||
height2Measured: string; // ②높이 측정값
|
||||
lengthDesign: string; // 길이 도면치수
|
||||
lengthMeasured: string; // 길이 측정값
|
||||
intervalStandard: string; // 간격 기준치
|
||||
intervalMeasured: string; // 간격 측정값
|
||||
itemId?: string;
|
||||
itemName?: string;
|
||||
processStatus: CheckStatus;
|
||||
assemblyStatus: CheckStatus;
|
||||
height1Standard: string;
|
||||
height1Measured: string;
|
||||
height2Standard: string;
|
||||
height2Measured: string;
|
||||
lengthDesign: string;
|
||||
lengthMeasured: string;
|
||||
intervalStandard: string;
|
||||
intervalMeasured: string;
|
||||
}
|
||||
|
||||
const DEFAULT_ROW_COUNT = 6;
|
||||
|
||||
// 상태 변환 함수: 'good'/'bad' → '양호'/'불량'
|
||||
const convertToCheckStatus = (status: 'good' | 'bad' | null | undefined): CheckStatus => {
|
||||
if (status === 'good') return '양호';
|
||||
if (status === 'bad') return '불량';
|
||||
return null;
|
||||
};
|
||||
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: '43.1 \u00b1 0.5',
|
||||
height1Measured: '',
|
||||
height2Standard: '14.5 \u00b1 1',
|
||||
height2Measured: '',
|
||||
lengthDesign: '',
|
||||
lengthMeasured: '',
|
||||
intervalStandard: '150 \u00b1 4',
|
||||
intervalMeasured: '',
|
||||
};
|
||||
}
|
||||
|
||||
export const SlatJointBarInspectionContent = forwardRef<InspectionContentRef, SlatJointBarInspectionContentProps>(function SlatJointBarInspectionContent({
|
||||
data: order,
|
||||
@@ -70,72 +88,22 @@ export const SlatJointBarInspectionContent = forwardRef<InspectionContentRef, Sl
|
||||
workItems,
|
||||
inspectionDataMap,
|
||||
schematicImage,
|
||||
inspectionStandardImage,
|
||||
}, ref) {
|
||||
const fullDate = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
const fullDate = getFullDate();
|
||||
const today = getToday();
|
||||
const { documentNo, primaryAssignee } = getOrderInfo(order);
|
||||
|
||||
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 || '-';
|
||||
|
||||
// workItems 기반 행 개수 결정
|
||||
const rowCount = workItems?.length || DEFAULT_ROW_COUNT;
|
||||
|
||||
const [rows, setRows] = useState<InspectionRow[]>(() =>
|
||||
Array.from({ length: rowCount }, (_, i) => {
|
||||
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: '43.1 ± 0.5',
|
||||
height1Measured: '',
|
||||
height2Standard: '14.5 ± 1',
|
||||
height2Measured: '',
|
||||
lengthDesign: '',
|
||||
lengthMeasured: '',
|
||||
intervalStandard: '150 ± 4',
|
||||
intervalMeasured: '',
|
||||
};
|
||||
})
|
||||
Array.from({ length: rowCount }, (_, i) => buildRow(i, workItems, inspectionDataMap))
|
||||
);
|
||||
|
||||
const [inadequateContent, setInadequateContent] = useState('');
|
||||
|
||||
// workItems 또는 inspectionDataMap 변경 시 행 업데이트
|
||||
useEffect(() => {
|
||||
const newRowCount = workItems?.length || DEFAULT_ROW_COUNT;
|
||||
setRows(Array.from({ length: newRowCount }, (_, i) => {
|
||||
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: '43.1 ± 0.5',
|
||||
height1Measured: '',
|
||||
height2Standard: '14.5 ± 1',
|
||||
height2Measured: '',
|
||||
lengthDesign: '',
|
||||
lengthMeasured: '',
|
||||
intervalStandard: '150 ± 4',
|
||||
intervalMeasured: '',
|
||||
};
|
||||
}));
|
||||
setRows(Array.from({ length: newRowCount }, (_, i) => buildRow(i, workItems, inspectionDataMap)));
|
||||
}, [workItems, inspectionDataMap]);
|
||||
|
||||
const handleStatusChange = useCallback((rowId: number, field: 'processStatus' | 'assemblyStatus', value: CheckStatus) => {
|
||||
@@ -153,7 +121,6 @@ export const SlatJointBarInspectionContent = forwardRef<InspectionContentRef, Sl
|
||||
));
|
||||
}, [readOnly]);
|
||||
|
||||
// 행별 판정 자동 계산
|
||||
const getRowJudgment = useCallback((row: InspectionRow): '적' | '부' | null => {
|
||||
const { processStatus, assemblyStatus } = row;
|
||||
if (processStatus === '불량' || assemblyStatus === '불량') return '부';
|
||||
@@ -161,13 +128,7 @@ export const SlatJointBarInspectionContent = forwardRef<InspectionContentRef, Sl
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
// 종합판정 자동 계산
|
||||
const overallResult = useMemo(() => {
|
||||
const judgments = rows.map(getRowJudgment);
|
||||
if (judgments.some(j => j === '부')) return '불합격';
|
||||
if (judgments.every(j => j === '적')) return '합격';
|
||||
return null;
|
||||
}, [rows, getRowJudgment]);
|
||||
const overallResult = useMemo(() => calculateOverallResult(rows.map(getRowJudgment)), [rows, getRowJudgment]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getInspectionData: () => ({
|
||||
@@ -185,74 +146,8 @@ export const SlatJointBarInspectionContent = forwardRef<InspectionContentRef, Sl
|
||||
}),
|
||||
}), [rows, 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 renderCheckStatus = (rowId: number, field: 'processStatus' | 'assemblyStatus', value: CheckStatus) => (
|
||||
<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 whitespace-nowrap">
|
||||
{renderCheckbox(value === '양호', () => handleStatusChange(rowId, field, value === '양호' ? null : '양호'))}
|
||||
양호
|
||||
</label>
|
||||
<label className="flex items-center gap-0.5 cursor-pointer text-xs whitespace-nowrap">
|
||||
{renderCheckbox(value === '불량', () => handleStatusChange(rowId, field, value === '불량' ? null : '불량'))}
|
||||
불량
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
|
||||
const inputClass = 'w-full text-center border-0 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-xs';
|
||||
|
||||
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>
|
||||
|
||||
<InspectionLayout title="중간검사성적서 (조인트바)" documentNo={documentNo} fullDate={fullDate} primaryAssignee={primaryAssignee}>
|
||||
{/* ===== 기본 정보 ===== */}
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<tbody>
|
||||
@@ -288,7 +183,6 @@ export const SlatJointBarInspectionContent = forwardRef<InspectionContentRef, Sl
|
||||
<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" />
|
||||
@@ -296,14 +190,12 @@ export const SlatJointBarInspectionContent = forwardRef<InspectionContentRef, Sl
|
||||
<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 text-center" rowSpan={3}>결모양</td>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium">가공상태</td>
|
||||
@@ -312,7 +204,6 @@ export const SlatJointBarInspectionContent = forwardRef<InspectionContentRef, Sl
|
||||
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={7}>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" rowSpan={2}>조립상태</td>
|
||||
<td className="border border-gray-400 px-2 py-1">엔드락이 용접에 의해<br/>견고하게 조립되어야 함</td>
|
||||
@@ -322,7 +213,6 @@ export const SlatJointBarInspectionContent = forwardRef<InspectionContentRef, Sl
|
||||
<td className="border border-gray-400 px-2 py-1">용접부위에 락카도색이<br/>되어야 함</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 bg-gray-50 text-center" rowSpan={4}>치수<br/>(mm)</td>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium">① 높이</td>
|
||||
@@ -330,17 +220,14 @@ export const SlatJointBarInspectionContent = forwardRef<InspectionContentRef, Sl
|
||||
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={4}>체크검사</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 font-medium">② 높이</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">길이</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">도면치수 ± 4</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 text-center">150 ± 4</td>
|
||||
@@ -381,36 +268,25 @@ export const SlatJointBarInspectionContent = forwardRef<InspectionContentRef, Sl
|
||||
return (
|
||||
<tr key={row.id}>
|
||||
<td className="border border-gray-400 p-1 text-center">{row.id}</td>
|
||||
{/* 가공상태 */}
|
||||
{renderCheckStatus(row.id, 'processStatus', row.processStatus)}
|
||||
{/* 조립상태 */}
|
||||
{renderCheckStatus(row.id, 'assemblyStatus', row.assemblyStatus)}
|
||||
{/* ① 높이 */}
|
||||
<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={inputClass} placeholder="-" />
|
||||
<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={inputClass} placeholder="-" />
|
||||
<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={inputClass} placeholder="-" />
|
||||
<input type="text" value={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.intervalStandard}</td>
|
||||
<td className="border border-gray-400 p-1">
|
||||
<input type="text" value={row.intervalMeasured} onChange={(e) => handleInputChange(row.id, 'intervalMeasured', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
|
||||
</td>
|
||||
{/* 판정 - 자동 계산 */}
|
||||
<td className={`border border-gray-400 p-1 text-center font-bold ${
|
||||
judgment === '적' ? 'text-blue-600' : judgment === '부' ? 'text-red-600' : 'text-gray-300'
|
||||
}`}>
|
||||
{judgment || '-'}
|
||||
<input type="text" value={row.intervalMeasured} onChange={(e) => handleInputChange(row.id, 'intervalMeasured', e.target.value)} disabled={readOnly} className={INPUT_CLASS} placeholder="-" />
|
||||
</td>
|
||||
<JudgmentCell judgment={judgment} />
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
@@ -418,23 +294,12 @@ export const SlatJointBarInspectionContent = forwardRef<InspectionContentRef, Sl
|
||||
</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>
|
||||
<InspectionFooter
|
||||
readOnly={readOnly}
|
||||
overallResult={overallResult}
|
||||
inadequateContent={inadequateContent}
|
||||
onInadequateContentChange={setInadequateContent}
|
||||
/>
|
||||
</InspectionLayout>
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,253 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 중간검사 성적서 공통 유틸리티 및 컴포넌트
|
||||
*
|
||||
* 5개 InspectionContent 컴포넌트에서 공유하는 코드:
|
||||
* - 타입 정의 (CheckStatus, InspectionContentRef 등)
|
||||
* - 상태 변환 함수 (convertToCheckStatus, convertToGapResult)
|
||||
* - UI 컴포넌트 (renderCheckbox, ApprovalTable, InspectionFooter)
|
||||
* - 날짜 유틸, 공통 스타일
|
||||
*/
|
||||
|
||||
import { ReactNode, useState } from 'react';
|
||||
import type { WorkOrder } from '../types';
|
||||
|
||||
// ===== 공통 타입 =====
|
||||
|
||||
export type CheckStatus = '양호' | '불량' | null;
|
||||
export type GapResult = 'OK' | 'NG' | null;
|
||||
|
||||
export interface InspectionContentRef {
|
||||
getInspectionData: () => unknown;
|
||||
}
|
||||
|
||||
// ===== 상태 변환 함수 =====
|
||||
|
||||
export const convertToCheckStatus = (status: 'good' | 'bad' | null | undefined): CheckStatus => {
|
||||
if (status === 'good') return '양호';
|
||||
if (status === 'bad') return '불량';
|
||||
return null;
|
||||
};
|
||||
|
||||
export const convertToGapResult = (status: 'ok' | 'ng' | null | undefined): GapResult => {
|
||||
if (status === 'ok') return 'OK';
|
||||
if (status === 'ng') return 'NG';
|
||||
return null;
|
||||
};
|
||||
|
||||
// ===== 날짜 유틸 =====
|
||||
|
||||
export function getFullDate(): string {
|
||||
return new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
export function getToday(): string {
|
||||
return new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).replace(/\. /g, '-').replace('.', '');
|
||||
}
|
||||
|
||||
// ===== 공통 스타일 =====
|
||||
|
||||
export const INPUT_CLASS = 'w-full text-center border-0 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-xs';
|
||||
|
||||
export const DEFAULT_ROW_COUNT = 6;
|
||||
|
||||
// ===== 공통 order 정보 추출 =====
|
||||
|
||||
export function getOrderInfo(order: WorkOrder) {
|
||||
return {
|
||||
documentNo: order.workOrderNo || 'ABC123',
|
||||
primaryAssignee: order.assignees?.find(a => a.isPrimary)?.name || order.assignee || '-',
|
||||
};
|
||||
}
|
||||
|
||||
// ===== PDF 호환 체크박스 =====
|
||||
|
||||
export function InspectionCheckbox({
|
||||
checked,
|
||||
onClick,
|
||||
readOnly = false,
|
||||
}: {
|
||||
checked: boolean;
|
||||
onClick: () => void;
|
||||
readOnly?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 양호/불량 체크 셀 =====
|
||||
|
||||
export function CheckStatusCell({
|
||||
value,
|
||||
onToggle,
|
||||
readOnly = false,
|
||||
rowSpan,
|
||||
}: {
|
||||
value: CheckStatus;
|
||||
onToggle: (v: CheckStatus) => void;
|
||||
readOnly?: boolean;
|
||||
rowSpan?: number;
|
||||
}) {
|
||||
return (
|
||||
<td className="border border-gray-400 p-1" rowSpan={rowSpan}>
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<label className="flex items-center gap-0.5 cursor-pointer text-xs whitespace-nowrap">
|
||||
<InspectionCheckbox
|
||||
checked={value === '양호'}
|
||||
onClick={() => onToggle(value === '양호' ? null : '양호')}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
양호
|
||||
</label>
|
||||
<label className="flex items-center gap-0.5 cursor-pointer text-xs whitespace-nowrap">
|
||||
<InspectionCheckbox
|
||||
checked={value === '불량'}
|
||||
onClick={() => onToggle(value === '불량' ? null : '불량')}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
불량
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 결재란 =====
|
||||
|
||||
export function ApprovalTable({ primaryAssignee }: { primaryAssignee: string }) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 부적합 내용 + 종합판정 하단 =====
|
||||
|
||||
export function InspectionFooter({
|
||||
readOnly = false,
|
||||
overallResult,
|
||||
inadequateContent,
|
||||
onInadequateContentChange,
|
||||
}: {
|
||||
readOnly?: boolean;
|
||||
overallResult: '합격' | '불합격' | null;
|
||||
inadequateContent: string;
|
||||
onInadequateContentChange: (v: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<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 && onInadequateContentChange(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>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 기본 레이아웃 래퍼 =====
|
||||
|
||||
export function InspectionLayout({
|
||||
title,
|
||||
documentNo,
|
||||
fullDate,
|
||||
primaryAssignee,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
documentNo: string;
|
||||
fullDate: string;
|
||||
primaryAssignee: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="p-6 bg-white">
|
||||
{/* 헤더 영역 */}
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{title}</h1>
|
||||
<p className="text-xs text-gray-500 mt-1 whitespace-nowrap">
|
||||
문서번호: {documentNo} | 작성일자: {fullDate}
|
||||
</p>
|
||||
</div>
|
||||
<ApprovalTable primaryAssignee={primaryAssignee} />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 판정 셀 =====
|
||||
|
||||
export function JudgmentCell({ judgment, rowSpan }: { judgment: '적' | '부' | null; rowSpan?: number }) {
|
||||
return (
|
||||
<td className={`border border-gray-400 p-1 text-center font-bold ${
|
||||
judgment === '적' ? 'text-blue-600' : judgment === '부' ? 'text-red-600' : 'text-gray-300'
|
||||
}`} rowSpan={rowSpan}>
|
||||
{judgment || '-'}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 종합판정 계산 =====
|
||||
|
||||
export function calculateOverallResult(judgments: ('적' | '부' | null)[]): '합격' | '불합격' | null {
|
||||
if (judgments.some(j => j === '부')) return '불합격';
|
||||
if (judgments.every(j => j === '적')) return '합격';
|
||||
return null;
|
||||
}
|
||||
@@ -225,49 +225,49 @@ export function InspectionInputModal({
|
||||
return;
|
||||
}
|
||||
|
||||
// 공정별 기본값 설정 - 모두 양호/OK/적합 상태로 초기화
|
||||
// 공정별 기본값 설정 - 모두 미선택(null) 상태로 초기화
|
||||
const baseData: InspectionData = {
|
||||
productName,
|
||||
specification,
|
||||
judgment: 'pass', // 기본값: 적합
|
||||
judgment: null,
|
||||
nonConformingContent: '',
|
||||
};
|
||||
|
||||
// 공정별 추가 기본값 설정
|
||||
// 공정별 추가 기본값 설정 (모두 null)
|
||||
switch (processType) {
|
||||
case 'screen':
|
||||
setFormData({
|
||||
...baseData,
|
||||
processingStatus: 'good', // 가공상태: 양호
|
||||
sewingStatus: 'good', // 재봉상태: 양호
|
||||
assemblyStatus: 'good', // 조립상태: 양호
|
||||
gapStatus: 'ok', // 간격: OK
|
||||
processingStatus: null,
|
||||
sewingStatus: null,
|
||||
assemblyStatus: null,
|
||||
gapStatus: null,
|
||||
});
|
||||
break;
|
||||
case 'slat':
|
||||
setFormData({
|
||||
...baseData,
|
||||
processingStatus: 'good', // 가공상태: 양호
|
||||
assemblyStatus: 'good', // 조립상태: 양호
|
||||
processingStatus: null,
|
||||
assemblyStatus: null,
|
||||
});
|
||||
break;
|
||||
case 'slat_jointbar':
|
||||
setFormData({
|
||||
...baseData,
|
||||
processingStatus: 'good', // 가공상태: 양호
|
||||
assemblyStatus: 'good', // 조립상태: 양호
|
||||
processingStatus: null,
|
||||
assemblyStatus: null,
|
||||
});
|
||||
break;
|
||||
case 'bending':
|
||||
setFormData({
|
||||
...baseData,
|
||||
bendingStatus: 'good', // 절곡상태: 양호
|
||||
bendingStatus: null,
|
||||
});
|
||||
break;
|
||||
case 'bending_wip':
|
||||
setFormData({
|
||||
...baseData,
|
||||
bendingStatus: 'good', // 절곡상태: 양호
|
||||
bendingStatus: null,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user