feat(WEB): 자재/출고/생산/품질/단가 기능 대폭 개선 및 신규 페이지 추가

자재관리:
- 입고관리 재고조정 다이얼로그 신규 추가, 상세/목록 기능 확장
- 재고현황 컴포넌트 리팩토링

출고관리:
- 출하관리 생성/수정/목록/상세 개선
- 차량배차관리 상세/수정/목록 기능 보강

생산관리:
- 작업지시서 WIP 생산 모달 신규 추가
- 벤딩WIP/슬랫조인트바 검사 콘텐츠 신규 추가
- 작업자화면 기능 대폭 확장 (카드/목록 개선)
- 검사성적서 모달 개선

품질관리:
- 실적보고서 관리 페이지 신규 추가
- 검사관리 문서/타입/목데이터 개선

단가관리:
- 단가배포 페이지 및 컴포넌트 신규 추가
- 단가표 관리 페이지 및 컴포넌트 신규 추가

공통:
- 권한 시스템 추가 개선 (PermissionContext, usePermission, PermissionGuard)
- 메뉴 폴링 훅 개선, 레이아웃 수정
- 모바일 줌/패닝 CSS 수정
- locale 유틸 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-04 12:46:19 +09:00
parent 17c16028b1
commit c1b63b850a
70 changed files with 6832 additions and 384 deletions

View File

@@ -0,0 +1,375 @@
'use client';
/**
* 절곡 재공품 통합 문서 (작업일지 & 중간검사성적서)
*
* 기획서 기준:
* - 제목: "절곡품 재고생산 작업일지 중간검사성적서"
* - 결재란: 작성/승인/승인/승인
* - 기본 정보: 제품명, 규격, 길이, 판고 LOT NO / 생산 LOT NO, 수량, 검사일자, 검사자
* - ■ 중간검사 기준서 KDPS-20: 도해 IMG + 검사항목(겉모양/절곡상태, 치수/길이/폭/간격)
* - ■ 중간검사 DATA: No, 제품명, 절곡상태(양호/불량), 길이(mm), 너비(mm)+포인트, 간격(mm), 판정
* - 부적합 내용 / 종합판정 (자동)
*/
import { useState, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react';
import type { WorkOrder } from '../types';
export interface InspectionContentRef {
getInspectionData: () => unknown;
}
interface BendingWipInspectionContentProps {
data: WorkOrder;
readOnly?: boolean;
}
type CheckStatus = '양호' | '불량' | null;
interface InspectionRow {
id: number;
productName: string; // 제품명
processStatus: CheckStatus; // 절곡상태
lengthDesign: string; // 길이 도면치수
lengthMeasured: string; // 길이 측정값
widthDesign: string; // 너비 도면치수
widthMeasured: string; // 너비 측정값
spacingPoint: string; // 너비 포인트
spacingDesign: string; // 간격 도면치수
spacingMeasured: string; // 간격 측정값
}
const DEFAULT_ROW_COUNT = 6;
export const BendingWipInspectionContent = forwardRef<InspectionContentRef, BendingWipInspectionContentProps>(function BendingWipInspectionContent({ data: order, readOnly = false }, 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 [rows, setRows] = useState<InspectionRow[]>(() => {
const items = order.items || [];
const count = Math.max(items.length, DEFAULT_ROW_COUNT);
return Array.from({ length: count }, (_, i) => ({
id: i + 1,
productName: items[i]?.productName || '',
processStatus: null,
lengthDesign: '4000',
lengthMeasured: '',
widthDesign: 'N/A',
widthMeasured: 'N/A',
spacingPoint: '',
spacingDesign: '380',
spacingMeasured: '',
}));
});
const [inadequateContent, setInadequateContent] = useState('');
const handleStatusChange = useCallback((rowId: number, value: CheckStatus) => {
if (readOnly) return;
setRows(prev => prev.map(row =>
row.id === rowId ? { ...row, processStatus: value } : row
));
}, [readOnly]);
const handleInputChange = useCallback((rowId: number, field: keyof InspectionRow, value: string) => {
if (readOnly) return;
setRows(prev => prev.map(row =>
row.id === rowId ? { ...row, [field]: value } : row
));
}, [readOnly]);
const handleNumericInput = 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 => {
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]);
useImperativeHandle(ref, () => ({
getInspectionData: () => ({
rows: rows.map(row => ({
id: row.id,
productName: row.productName,
processStatus: row.processStatus,
lengthMeasured: row.lengthMeasured,
widthMeasured: row.widthMeasured,
spacingPoint: row.spacingPoint,
spacingMeasured: row.spacingMeasured,
})),
inadequateContent,
overallResult,
}),
}), [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>
{/* ===== 기본 정보 ===== */}
<table className="w-full border-collapse text-xs mb-6">
<tbody>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium w-24"></td>
<td className="border border-gray-400 px-3 py-2">{order.items?.[0]?.productName || '가이드레일'}</td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium w-28"> LOT NO</td>
<td className="border border-gray-400 px-3 py-2">{order.lotNo || '-'}</td>
</tr>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{order.items?.[0]?.specification || 'EGI 1.6T'}</td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"> LOT NO</td>
<td className="border border-gray-400 px-3 py-2">{order.workOrderNo || '-'}</td>
</tr>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">3,000 mm</td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{order.items?.reduce((sum, item) => sum + item.quantity, 0) || 0} EA</td>
</tr>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{today}</td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{primaryAssignee}</td>
</tr>
</tbody>
</table>
{/* ===== 중간검사 기준서 KDPS-20 ===== */}
<div className="mb-1 font-bold text-sm"> KDPS-20</div>
<table className="w-full table-fixed border-collapse text-xs mb-6">
<colgroup>
<col style={{width: '200px'}} />
<col style={{width: '52px'}} />
<col style={{width: '58px'}} />
<col />
<col style={{width: '68px'}} />
<col style={{width: '78px'}} />
<col style={{width: '120px'}} />
</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>
<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>
<td className="border border-gray-400 px-2 py-1"> </td>
<td className="border border-gray-400 px-2 py-1 text-center"></td>
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={3}>n = 1, c = 0</td>
<td className="border border-gray-400 px-2 py-1">KS F 4510 5.1</td>
</tr>
{/* 치수 > 길이 */}
<tr>
<td className="border border-gray-400 px-2 py-1 font-medium bg-gray-50" rowSpan={2}><br/>(mm)</td>
<td className="border border-gray-400 px-2 py-1 font-medium"></td>
<td className="border border-gray-400 px-2 py-1"> ± 4</td>
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={2}></td>
<td className="border border-gray-400 px-2 py-1">KS F 4510 7<br/>9</td>
</tr>
{/* 치수 > 간격 */}
<tr>
<td className="border border-gray-400 px-2 py-1 font-medium"></td>
<td className="border border-gray-400 px-2 py-1"> ± 2</td>
<td className="border border-gray-400 px-2 py-1">KS F 4510 7<br/>9 / </td>
</tr>
</tbody>
</table>
{/* ===== 중간검사 DATA ===== */}
<div className="mb-1 font-bold text-sm"> DATA</div>
<table className="w-full border-collapse text-xs mb-4">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-400 p-1 w-8" rowSpan={2}>No.</th>
<th className="border border-gray-400 p-1 w-20" rowSpan={2}></th>
<th className="border border-gray-400 p-1 w-16" rowSpan={2}><br/></th>
<th className="border border-gray-400 p-1" colSpan={2}> (mm)</th>
<th className="border border-gray-400 p-1" colSpan={2}> (mm)</th>
<th className="border border-gray-400 p-1" colSpan={3}> (mm)</th>
<th className="border border-gray-400 p-1 w-14" rowSpan={2}><br/>(/)</th>
</tr>
<tr className="bg-gray-100">
<th className="border border-gray-400 p-1 w-14"></th>
<th className="border border-gray-400 p-1 w-14"></th>
<th className="border border-gray-400 p-1 w-14"></th>
<th className="border border-gray-400 p-1 w-14"></th>
<th className="border border-gray-400 p-1 w-12"></th>
<th className="border border-gray-400 p-1 w-14"></th>
<th className="border border-gray-400 p-1 w-14"></th>
</tr>
</thead>
<tbody>
{rows.map((row) => {
const judgment = getRowJudgment(row);
return (
<tr key={row.id}>
<td className="border border-gray-400 p-1 text-center">{row.id}</td>
{/* 제품명 */}
<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="-"
/>
</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>
{/* 길이 - 도면치수 */}
<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="-" />
</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="-" />
</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="-" />
</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 || '-'}
</td>
</tr>
);
})}
</tbody>
</table>
{/* ===== 부적합 내용 + 종합판정 ===== */}
<table className="w-full border-collapse text-xs">
<tbody>
<tr>
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium w-24 align-middle text-center"> </td>
<td className="border border-gray-400 px-3 py-2">
<textarea value={inadequateContent} onChange={(e) => !readOnly && setInadequateContent(e.target.value)} disabled={readOnly}
className="w-full border-0 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-xs resize-none" rows={2} />
</td>
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium text-center w-24"></td>
<td className={`border border-gray-400 px-3 py-2 text-center font-bold text-sm w-24 ${
overallResult === '합격' ? 'text-blue-600' : overallResult === '불합격' ? 'text-red-600' : 'text-gray-400'
}`}>
{overallResult || '합격'}
</td>
</tr>
</tbody>
</table>
</div>
);
});

View File

@@ -19,12 +19,15 @@ import type { WorkOrder, ProcessType } from '../types';
import { ScreenInspectionContent } from './ScreenInspectionContent';
import { SlatInspectionContent } from './SlatInspectionContent';
import { BendingInspectionContent } from './BendingInspectionContent';
import { BendingWipInspectionContent } from './BendingWipInspectionContent';
import { SlatJointBarInspectionContent } from './SlatJointBarInspectionContent';
import type { InspectionContentRef } from './ScreenInspectionContent';
const PROCESS_LABELS: Record<ProcessType, string> = {
screen: '스크린',
slat: '슬랫',
bending: '절곡',
bending_wip: '절곡 재공품',
};
interface InspectionReportModalProps {
@@ -33,6 +36,7 @@ interface InspectionReportModalProps {
workOrderId: string | null;
processType?: ProcessType;
readOnly?: boolean;
isJointBar?: boolean;
}
export function InspectionReportModal({
@@ -41,6 +45,7 @@ export function InspectionReportModal({
workOrderId,
processType = 'screen',
readOnly = true,
isJointBar = false,
}: InspectionReportModalProps) {
const [order, setOrder] = useState<WorkOrder | null>(null);
const [isLoading, setIsLoading] = useState(false);
@@ -138,6 +143,11 @@ export function InspectionReportModal({
const processLabel = PROCESS_LABELS[processType] || '스크린';
const subtitle = order ? `${processLabel} 생산부서` : undefined;
const modalTitle = processType === 'bending_wip'
? '절곡품 재고생산 작업일지 중간검사성적서'
: (isJointBar || (order?.items?.some(item => item.productName?.includes('조인트바'))))
? '중간검사성적서 (조인트바)'
: '중간검사 성적서';
const renderContent = () => {
if (!order) return null;
@@ -146,9 +156,15 @@ export function InspectionReportModal({
case 'screen':
return <ScreenInspectionContent ref={contentRef} data={order} readOnly={readOnly} />;
case 'slat':
// 조인트바 여부 체크: isJointBar prop 또는 items에서 자동 감지
if (isJointBar || order.items?.some(item => item.productName?.includes('조인트바'))) {
return <SlatJointBarInspectionContent ref={contentRef} data={order} readOnly={readOnly} />;
}
return <SlatInspectionContent ref={contentRef} data={order} readOnly={readOnly} />;
case 'bending':
return <BendingInspectionContent ref={contentRef} data={order} readOnly={readOnly} />;
case 'bending_wip':
return <BendingWipInspectionContent ref={contentRef} data={order} readOnly={readOnly} />;
default:
return <ScreenInspectionContent ref={contentRef} data={order} readOnly={readOnly} />;
}
@@ -167,7 +183,7 @@ export function InspectionReportModal({
return (
<DocumentViewer
title="중간검사 성적서"
title={modalTitle}
subtitle={subtitle}
preset="inspection"
open={open}

View File

@@ -0,0 +1,375 @@
'use client';
/**
* 슬랫 조인트바 중간검사성적서
*
* 기획서 기준:
* - 제목: "중간검사성적서 (조인트바)"
* - 결재란: 작성/승인/승인/승인
* - 기본 정보: 제품명/슬랫, 규격/슬랫, 수주처, 현장명 / 제품 LOT NO, 부서, 검사일자, 검사자
* - ■ 중간검사 기준서 KOPS-20: 도해 IMG + 치수 기준 (43.1 ± 0.5 등)
* - ■ 중간검사 DATA: No, 가공상태, 조립상태, ①높이(기준치/측정값),
* ②높이(기준치/측정값), ③길이(기준치/측정값), ④간격(기준치/측정값), 판정
* - 부적합 내용 / 종합판정 (자동)
*/
import { useState, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react';
import type { WorkOrder } from '../types';
export interface InspectionContentRef {
getInspectionData: () => unknown;
}
interface SlatJointBarInspectionContentProps {
data: WorkOrder;
readOnly?: boolean;
}
type CheckStatus = '양호' | '불량' | null;
interface InspectionRow {
id: number;
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;
export const SlatJointBarInspectionContent = forwardRef<InspectionContentRef, SlatJointBarInspectionContentProps>(function SlatJointBarInspectionContent({ data: order, readOnly = false }, 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 [rows, setRows] = useState<InspectionRow[]>(() =>
Array.from({ length: DEFAULT_ROW_COUNT }, (_, i) => ({
id: i + 1,
processStatus: null,
assemblyStatus: null,
height1Standard: '43.1 ± 0.5',
height1Measured: '',
height2Standard: '14.5 ± 1',
height2Measured: '',
lengthDesign: '',
lengthMeasured: '',
intervalStandard: '150 ± 4',
intervalMeasured: '',
}))
);
const [inadequateContent, setInadequateContent] = useState('');
const handleStatusChange = useCallback((rowId: number, field: 'processStatus' | 'assemblyStatus', value: CheckStatus) => {
if (readOnly) return;
setRows(prev => prev.map(row =>
row.id === rowId ? { ...row, [field]: value } : row
));
}, [readOnly]);
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 '부';
if (processStatus === '양호' && assemblyStatus === '양호') 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]);
useImperativeHandle(ref, () => ({
getInspectionData: () => ({
rows: rows.map(row => ({
id: row.id,
processStatus: row.processStatus,
assemblyStatus: row.assemblyStatus,
height1Measured: row.height1Measured,
height2Measured: row.height2Measured,
lengthMeasured: row.lengthMeasured,
intervalMeasured: row.intervalMeasured,
})),
inadequateContent,
overallResult,
}),
}), [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>
{/* ===== 기본 정보 ===== */}
<table className="w-full border-collapse text-xs mb-6">
<tbody>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium w-24"></td>
<td className="border border-gray-400 px-3 py-2"></td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium w-28"> LOT NO</td>
<td className="border border-gray-400 px-3 py-2">{order.lotNo || '-'}</td>
</tr>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2"></td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{order.department || '생산부'}</td>
</tr>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{order.client || '-'}</td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{today}</td>
</tr>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{order.projectName || '-'}</td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{primaryAssignee}</td>
</tr>
</tbody>
</table>
{/* ===== 중간검사 기준서 KOPS-20 ===== */}
<div className="mb-1 font-bold text-sm"> KOPS-20</div>
<table className="w-full border-collapse text-xs mb-6">
<tbody>
<tr>
{/* 도해 영역 */}
<td className="border border-gray-400 p-4 text-center text-gray-300 align-middle w-1/5" rowSpan={8}>
<div className="h-40 flex items-center justify-center"> </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>
<td className="border border-gray-400 px-2 py-1"> </td>
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={3}></td>
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={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>
<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"> <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>
<td className="border border-gray-400 px-2 py-1 text-center">43.1 ± 0.5</td>
<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>
<td className="border border-gray-400 px-2 py-1"></td>
</tr>
</tbody>
</table>
{/* ===== 중간검사 DATA ===== */}
<div className="mb-1 font-bold text-sm"> DATA</div>
<table className="w-full border-collapse text-xs mb-4">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-400 p-1 w-8" rowSpan={2}>No.</th>
<th className="border border-gray-400 p-1" colSpan={2}></th>
<th className="border border-gray-400 p-1" colSpan={2}> (mm)</th>
<th className="border border-gray-400 p-1" colSpan={2}> (mm)</th>
<th className="border border-gray-400 p-1" colSpan={2}> (mm)</th>
<th className="border border-gray-400 p-1" colSpan={2}> </th>
<th className="border border-gray-400 p-1 w-14" rowSpan={2}><br/>(/)</th>
</tr>
<tr className="bg-gray-100">
<th className="border border-gray-400 p-1 w-16"></th>
<th className="border border-gray-400 p-1 w-16"></th>
<th className="border border-gray-400 p-1 w-16"></th>
<th className="border border-gray-400 p-1 w-16"></th>
<th className="border border-gray-400 p-1 w-16"></th>
<th className="border border-gray-400 p-1 w-16"></th>
<th className="border border-gray-400 p-1 w-16"></th>
<th className="border border-gray-400 p-1 w-16"></th>
<th className="border border-gray-400 p-1 w-16"></th>
<th className="border border-gray-400 p-1 w-16"></th>
</tr>
</thead>
<tbody>
{rows.map((row) => {
const judgment = getRowJudgment(row);
return (
<tr key={row.id}>
<td className="border border-gray-400 p-1 text-center">{row.id}</td>
{/* 가공상태 */}
{renderCheckStatus(row.id, 'processStatus', row.processStatus)}
{/* 조립상태 */}
{renderCheckStatus(row.id, 'assemblyStatus', row.assemblyStatus)}
{/* ① 높이 */}
<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="-" />
</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="-" />
</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">{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 || '-'}
</td>
</tr>
);
})}
</tbody>
</table>
{/* ===== 부적합 내용 + 종합판정 ===== */}
<table className="w-full border-collapse text-xs">
<tbody>
<tr>
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium w-24 align-middle text-center"> </td>
<td className="border border-gray-400 px-3 py-2">
<textarea value={inadequateContent} onChange={(e) => !readOnly && setInadequateContent(e.target.value)} disabled={readOnly}
className="w-full border-0 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-xs resize-none" rows={2} />
</td>
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium text-center w-24"></td>
<td className={`border border-gray-400 px-3 py-2 text-center font-bold text-sm w-24 ${
overallResult === '합격' ? 'text-blue-600' : overallResult === '불합격' ? 'text-red-600' : 'text-gray-400'
}`}>
{overallResult || '합격'}
</td>
</tr>
</tbody>
</table>
</div>
);
});

View File

@@ -7,6 +7,8 @@ export { BendingWorkLogContent } from './BendingWorkLogContent';
export { ScreenInspectionContent } from './ScreenInspectionContent';
export { SlatInspectionContent } from './SlatInspectionContent';
export { BendingInspectionContent } from './BendingInspectionContent';
export { BendingWipInspectionContent } from './BendingWipInspectionContent';
export { SlatJointBarInspectionContent } from './SlatJointBarInspectionContent';
export type { InspectionContentRef } from './ScreenInspectionContent';
// 모달