feat: 품질검사 문서 컴포넌트 추가 및 PDF 업로더 구현

- 수입검사 성적서 컴포넌트 추가
- 제품검사 성적서 컴포넌트 추가
- 중간검사 성적서 4종 추가 (스크린/절곡품/슬랫/조인트바)
- 품질관리서 PDF 업로드/뷰어 컴포넌트 구현
- InspectionModal 문서 타입별 렌더링 연동
- mockData 샘플 데이터 추가
- types.ts DocumentItem에 subType 필드 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-12-29 14:53:05 +09:00
parent d957f72198
commit fb2be8651e
16 changed files with 3554 additions and 355 deletions

View File

@@ -2,95 +2,133 @@
import React, { useState } from 'react';
import {
FileText, CheckCircle, ChevronDown, ChevronUp,
Eye, Truck, Calendar, ClipboardCheck, Box
FileText, CheckCircle, ChevronDown, ChevronUp,
Eye, Truck, Calendar, ClipboardCheck, Box, FileCheck
} from 'lucide-react';
import { Document } from '../types';
import { Document, DocumentItem } from '../types';
interface DocumentListProps {
onViewDocument: (doc: Document) => void;
documents: Document[];
routeCode: string | null;
onViewDocument: (doc: Document, item?: DocumentItem) => void;
}
const MOCK_DOCUMENTS: Document[] = [
{ id: '1', type: 'import', title: '수입검사 성적서', count: 3, items: [{ id: '1-1', title: '원단 수입검사 성적서', date: '2024-08-10' }, { id: '1-2', title: '철판 수입검사 성적서', date: '2024-08-12' }, { id: '1-3', title: '방화실 수입검사 성적서', date: '2024-08-15' }] },
{ id: '2', type: 'order', title: '수주서', count: 1 },
{ id: '3', type: 'log', title: '작업일지', count: 2 },
{ id: '4', type: 'report', title: '중간검사 성적서', count: 2 },
{ id: '5', type: 'confirmation', title: '납품확인서', count: 1 },
{ id: '6', type: 'shipping', title: '출고증', count: 1 },
{ id: '7', type: 'product', title: '제품검사 성적서', count: 7 },
{ id: '8', type: 'quality', title: '품질관리서', count: 1 },
];
const getIcon = (type: string) => {
switch (type) {
case 'import': return <CheckCircle className="text-green-600" />;
case 'order': return <FileText className="text-blue-600" />;
case 'log': return <Calendar className="text-orange-500" />;
case 'report': return <ClipboardCheck className="text-blue-500" />;
case 'confirmation': return <Box className="text-red-500" />;
case 'shipping': return <Truck className="text-gray-600" />;
case 'product': return <CheckCircle className="text-green-500" />;
default: return <FileText className="text-blue-600" />;
switch (type) {
case 'import': return <CheckCircle className="text-green-600" />;
case 'order': return <FileText className="text-blue-600" />;
case 'log': return <Calendar className="text-orange-500" />;
case 'report': return <ClipboardCheck className="text-blue-500" />;
case 'confirmation': return <Box className="text-red-500" />;
case 'shipping': return <Truck className="text-gray-600" />;
case 'product': return <CheckCircle className="text-green-500" />;
case 'quality': return <FileCheck className="text-purple-600" />;
default: return <FileText className="text-blue-600" />;
}
};
export const DocumentList = ({ documents, routeCode, onViewDocument }: DocumentListProps) => {
const [expandedId, setExpandedId] = useState<string | null>(null);
// 문서 카테고리 클릭 핸들러
const handleDocClick = (doc: Document) => {
const hasItems = doc.items && doc.items.length > 0;
if (!hasItems) return;
// 아이템이 1개면 바로 문서 열기
if (doc.items!.length === 1) {
onViewDocument(doc, doc.items![0]);
return;
}
};
export const DocumentList = ({ onViewDocument }: DocumentListProps) => {
const [expandedId, setExpandedId] = useState<string | null>('1');
// 여러 개면 펼치기/접기
setExpandedId(expandedId === doc.id ? null : doc.id);
};
const toggleExpand = (id: string) => {
setExpandedId(expandedId === id ? null : id);
};
// 개별 아이템 클릭 핸들러
const handleItemClick = (doc: Document, item: DocumentItem) => {
onViewDocument(doc, item);
};
return (
<div className="bg-white rounded-lg p-4 shadow-sm h-full overflow-y-auto">
<h2 className="font-bold text-gray-800 text-sm mb-4">
<span className="text-gray-400 font-normal ml-1">(KD-SS-240924-19)</span>
</h2>
return (
<div className="bg-white rounded-lg p-4 shadow-sm h-full flex flex-col overflow-hidden">
<h2 className="font-bold text-gray-800 text-sm mb-4">
{' '}
{routeCode && (
<span className="text-gray-400 font-normal ml-1">({routeCode})</span>
)}
</h2>
<div className="space-y-3">
{MOCK_DOCUMENTS.map((doc) => (
<div key={doc.id} className="border border-gray-200 rounded-lg overflow-hidden">
<div
onClick={() => toggleExpand(doc.id)}
className={`p-4 cursor-pointer flex justify-between items-center transition-colors ${expandedId === doc.id ? 'bg-green-50' : 'bg-white hover:bg-gray-50'}`}
>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${expandedId === doc.id ? 'bg-white' : 'bg-gray-100'}`}>
{getIcon(doc.type)}
</div>
<div>
<h3 className="font-bold text-gray-800 text-sm">{doc.title}</h3>
<p className="text-xs text-gray-500">{doc.count} </p>
</div>
</div>
{expandedId === doc.id ? <ChevronUp size={16} className="text-gray-400" /> : <ChevronDown size={16} className="text-gray-400" />}
</div>
<div className="space-y-3 overflow-y-auto flex-1">
{!routeCode ? (
<div className="flex items-center justify-center h-32 text-gray-400 text-sm">
.
</div>
) : (
documents.map((doc) => {
const isExpanded = expandedId === doc.id;
const hasItems = doc.items && doc.items.length > 0;
const hasMultipleItems = doc.items && doc.items.length > 1;
{expandedId === doc.id && doc.items && (
<div className="bg-white px-4 pb-4 space-y-2">
<div className="h-px bg-gray-100 w-full mb-3"></div>
{doc.items.map((item) => (
<div key={item.id} className="flex items-center justify-between border border-gray-100 p-3 rounded hover:bg-gray-50 group">
<div>
<div className="text-xs font-bold text-gray-700">{item.title}</div>
<div className="text-[10px] text-gray-400 mt-1">
{item.date} <span className="mx-1">|</span> 로트: RM-2024-1234
</div>
</div>
<button
onClick={(e) => { e.stopPropagation(); onViewDocument(doc); }}
className="text-green-600 opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-green-50 rounded"
>
<Eye size={16} />
</button>
</div>
))}
</div>
)}
return (
<div key={doc.id} className="border border-gray-200 rounded-lg overflow-hidden">
<div
onClick={() => handleDocClick(doc)}
className={`p-4 flex justify-between items-center transition-colors ${
hasItems ? 'cursor-pointer hover:bg-gray-50' : 'cursor-default opacity-60'
} ${isExpanded ? 'bg-green-50' : 'bg-white'}`}
>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${isExpanded ? 'bg-white' : 'bg-gray-100'}`}>
{getIcon(doc.type)}
</div>
))}
</div>
</div>
);
};
<div>
<h3 className="font-bold text-gray-800 text-sm">{doc.title}</h3>
<p className="text-xs text-gray-500">
{doc.count > 0 ? `${doc.count}건의 서류` : '서류 없음'}
</p>
</div>
</div>
{hasMultipleItems && (
isExpanded ? (
<ChevronUp size={16} className="text-gray-400" />
) : (
<ChevronDown size={16} className="text-gray-400" />
)
)}
</div>
{isExpanded && hasMultipleItems && (
<div className="bg-white px-4 pb-4 space-y-2">
<div className="h-px bg-gray-100 w-full mb-3" />
{doc.items!.map((item) => (
<div
key={item.id}
onClick={() => handleItemClick(doc, item)}
className="flex items-center justify-between border border-gray-100 p-3 rounded cursor-pointer hover:bg-green-50 hover:border-green-200 transition-colors group"
>
<div>
<div className="text-xs font-bold text-gray-700">{item.title}</div>
<div className="text-[10px] text-gray-400 mt-1">
{item.date}
{item.code && (
<>
<span className="mx-1">|</span>
: {item.code}
</>
)}
</div>
</div>
<Eye size={16} className="text-green-600 opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
))}
</div>
)}
</div>
);
})
)}
</div>
</div>
);
};

View File

@@ -1,65 +1,87 @@
"use client";
import React, { useState } from 'react';
import React from 'react';
import { Search } from 'lucide-react';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
export const Filters = () => {
const [activeQuarter, setActiveQuarter] = useState('전체');
interface FiltersProps {
selectedYear: number;
selectedQuarter: string;
searchTerm: string;
onYearChange: (year: number) => void;
onQuarterChange: (quarter: string) => void;
onSearchChange: (term: string) => void;
}
const quarters = ['전체', '1분기', '2분기', '3분기', '4분기'];
export const Filters = ({
selectedYear,
selectedQuarter,
searchTerm,
onYearChange,
onQuarterChange,
onSearchChange,
}: FiltersProps) => {
const quarters = ['전체', '1분기', '2분기', '3분기', '4분기'];
const years = [2025, 2024, 2023, 2022, 2021];
return (
<div className="w-full bg-white p-4 rounded-lg mb-4 shadow-sm flex flex-wrap items-center gap-4">
{/* Year Selection */}
<div className="flex flex-col gap-1">
<span className="text-xs font-semibold text-gray-500"></span>
<div className="w-32">
<select className="flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50">
<option>2025</option>
<option>2024</option>
</select>
</div>
</div>
{/* Quarter Selection */}
<div className="flex flex-col gap-1">
<span className="text-xs font-semibold text-gray-500"></span>
<div className="flex bg-gray-100 rounded-md p-1 gap-1">
{quarters.map((q) => (
<button
key={q}
onClick={() => setActiveQuarter(q)}
className={`px-4 py-1.5 text-sm rounded-sm transition-all ${activeQuarter === q
? 'bg-blue-600 text-white shadow-sm'
: 'text-gray-600 hover:bg-gray-200'
}`}
>
{q}
</button>
))}
</div>
</div>
{/* Search Input */}
<div className="flex flex-col gap-1 flex-1 min-w-[200px]">
<span className="text-xs font-semibold text-gray-500"></span>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<input
type="text"
placeholder="품질관리서번호, 현장명, 인정품목..."
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-md text-sm focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
/>
</div>
</div>
{/* Search Button */}
<div className="flex flex-col gap-1 justify-end h-full mt-auto mb-0.5">
<button className="bg-[#1e3a8a] text-white px-6 py-2 rounded-md text-sm hover:bg-blue-800 transition-colors">
</button>
</div>
return (
<div className="w-full bg-white p-4 rounded-lg mb-4 shadow-sm flex flex-wrap items-center gap-4">
{/* Year Selection */}
<div className="flex flex-col gap-1">
<span className="text-xs font-semibold text-gray-500"></span>
<div className="w-32">
<select
value={selectedYear}
onChange={(e) => onYearChange(parseInt(e.target.value))}
className="flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
{years.map((year) => (
<option key={year} value={year}>{year}</option>
))}
</select>
</div>
);
};
</div>
{/* Quarter Selection */}
<div className="flex flex-col gap-1">
<span className="text-xs font-semibold text-gray-500"></span>
<div className="flex bg-gray-100 rounded-md p-1 gap-1">
{quarters.map((q) => (
<button
key={q}
onClick={() => onQuarterChange(q)}
className={`px-4 py-1.5 text-sm rounded-sm transition-all ${
selectedQuarter === q
? 'bg-blue-600 text-white shadow-sm'
: 'text-gray-600 hover:bg-gray-200'
}`}
>
{q}
</button>
))}
</div>
</div>
{/* Search Input */}
<div className="flex flex-col gap-1 flex-1 min-w-[200px]">
<span className="text-xs font-semibold text-gray-500"></span>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<input
type="text"
placeholder="품질관리서번호, 현장명, 인정품목..."
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-md text-sm focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
/>
</div>
</div>
{/* Search Button */}
<div className="flex flex-col gap-1 justify-end h-full mt-auto mb-0.5">
<button className="bg-[#1e3a8a] text-white px-6 py-2 rounded-md text-sm hover:bg-blue-800 transition-colors">
</button>
</div>
</div>
);
};

View File

@@ -2,71 +2,439 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogClose
} from "@/components/ui/dialog"; // Assuming standard path, verified existence
import { X, ZoomIn, ZoomOut, RotateCw, Download } from 'lucide-react';
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { ZoomIn, ZoomOut, RotateCw, Download, Printer, AlertCircle } from 'lucide-react';
import { Button } from "@/components/ui/button";
import { Document, DocumentItem } from '../types';
import { MOCK_ORDER_DATA, MOCK_WORK_ORDER, MOCK_SHIPMENT_DETAIL } from '../mockData';
// 기존 문서 컴포넌트 import
import { DeliveryConfirmation } from '@/components/outbound/ShipmentManagement/documents/DeliveryConfirmation';
import { ShippingSlip } from '@/components/outbound/ShipmentManagement/documents/ShippingSlip';
// 품질검사 문서 컴포넌트 import
import {
ImportInspectionDocument,
ProductInspectionDocument,
ScreenInspectionDocument,
BendingInspectionDocument,
SlatInspectionDocument,
JointbarInspectionDocument,
QualityDocumentUploader,
} from './documents';
interface InspectionModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
isOpen: boolean;
onClose: () => void;
document: Document | null;
documentItem: DocumentItem | null;
}
export const InspectionModal = ({ isOpen, onClose, title }: InspectionModalProps) => {
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-4xl p-0 overflow-hidden bg-gray-50">
<DialogHeader className="p-4 bg-white border-b border-gray-200 flex flex-row items-center justify-between space-y-0">
<div>
<DialogTitle className="text-lg font-bold text-gray-800">{title}</DialogTitle>
<p className="text-xs text-gray-500 mt-1"> - 2024-08-10 로트: RM-2024-1234</p>
</div>
{/* Close button is handled by DialogPrimitive usually, but adding custom controls here is fine */}
</DialogHeader>
{/* Toolbar */}
<div className="flex items-center justify-between px-4 py-2 bg-white border-b border-gray-100">
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" className="h-8 gap-1 text-xs">
<ZoomOut size={14} />
</Button>
<Button variant="ghost" size="sm" className="h-8 gap-1 text-xs">
<ZoomIn size={14} />
</Button>
<Button variant="ghost" size="sm" className="h-8 gap-1 text-xs">
<RotateCw size={14} />
</Button>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-mono text-gray-600">100%</span>
<Button size="sm" className="h-8 bg-green-600 hover:bg-green-700 text-white gap-1 text-xs">
<Download size={14} />
</Button>
</div>
</div>
{/* Content Area */}
<div className="relative w-full h-[600px] flex items-center justify-center p-8 overflow-auto">
<div className="bg-white shadow-lg p-16 max-w-full rounded flex flex-col items-center justify-center text-center">
<div className="w-16 h-16 text-green-500 mb-4 mx-auto">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h2 className="text-xl font-bold text-gray-800 mb-2">{title}</h2>
<p className="text-gray-500 text-sm mb-4">2024-08-10</p>
<p className="text-xs text-green-600 font-mono bg-green-50 px-2 py-1 rounded mb-8">
번호: RM-2024-1234
</p>
<p className="text-xs text-gray-300"> </p>
</div>
</div>
</DialogContent>
</Dialog>
);
// 문서 타입별 정보
const DOCUMENT_INFO: Record<string, { label: string; hasTemplate: boolean; color: string }> = {
import: { label: '수입검사 성적서', hasTemplate: true, color: 'text-green-600' },
order: { label: '수주서', hasTemplate: true, color: 'text-blue-600' },
log: { label: '작업일지', hasTemplate: true, color: 'text-orange-500' },
report: { label: '중간검사 성적서', hasTemplate: true, color: 'text-blue-500' },
confirmation: { label: '납품확인서', hasTemplate: true, color: 'text-red-500' },
shipping: { label: '출고증', hasTemplate: true, color: 'text-gray-600' },
product: { label: '제품검사 성적서', hasTemplate: true, color: 'text-green-500' },
quality: { label: '품질관리서', hasTemplate: false, color: 'text-purple-600' },
};
// Placeholder 컴포넌트 (양식 대기 문서용)
const PlaceholderDocument = ({ docType, docItem }: { docType: string; docItem: DocumentItem | null }) => {
const info = DOCUMENT_INFO[docType] || { label: '문서', hasTemplate: false, color: 'text-gray-600' };
return (
<div className="bg-white shadow-sm p-16 w-full h-full rounded flex flex-col items-center justify-center text-center">
<div className="w-16 h-16 text-amber-500 mb-4 mx-auto">
<AlertCircle className="w-full h-full" />
</div>
<h2 className="text-xl font-bold text-gray-800 mb-2">{info.label}</h2>
<p className="text-gray-500 text-sm mb-2">{docItem?.title || '문서'}</p>
{docItem?.date && (
<p className="text-gray-400 text-xs mb-2">{docItem.date}</p>
)}
{docItem?.code && (
<p className="text-xs text-green-600 font-mono bg-green-50 px-2 py-1 rounded mb-4">
: {docItem.code}
</p>
)}
<div className="mt-4 p-4 bg-amber-50 rounded-lg border border-amber-200">
<p className="text-amber-700 text-sm font-medium"> </p>
<p className="text-amber-600 text-xs mt-1"> </p>
</div>
</div>
);
};
// 수주서 문서 컴포넌트 (간소화 버전)
const OrderDocument = () => {
const data = MOCK_ORDER_DATA;
return (
<div className="bg-white p-8 w-full text-sm shadow-sm">
{/* 헤더 */}
<div className="flex justify-between items-start mb-6">
<div className="flex items-center gap-4">
<div className="text-2xl font-bold">KD</div>
<div className="text-xs"></div>
</div>
<div className="text-2xl font-bold tracking-[0.5rem]"> </div>
<table className="text-xs border-collapse">
<tbody>
<tr>
<td className="border px-2 py-1 bg-gray-100" rowSpan={3}>
<div className="flex flex-col items-center">
<span></span><span></span>
</div>
</td>
<td className="border px-2 py-1 bg-gray-100 text-center w-16"></td>
<td className="border px-2 py-1 bg-gray-100 text-center w-16"></td>
<td className="border px-2 py-1 bg-gray-100 text-center w-16"></td>
</tr>
<tr>
<td className="border px-2 py-1 h-10"></td>
<td className="border px-2 py-1 h-10"></td>
<td className="border px-2 py-1 h-10"></td>
</tr>
<tr>
<td className="border px-2 py-1 text-center bg-gray-50"></td>
<td className="border px-2 py-1 text-center bg-gray-50"></td>
<td className="border px-2 py-1 text-center bg-gray-50"></td>
</tr>
</tbody>
</table>
</div>
{/* 기본 정보 */}
<table className="w-full border-collapse mb-6 text-xs">
<tbody>
<tr>
<td className="border px-3 py-2 bg-gray-100 w-24">LOT NO.</td>
<td className="border px-3 py-2">{data.lotNumber}</td>
<td className="border px-3 py-2 bg-gray-100 w-24"></td>
<td className="border px-3 py-2">{data.orderDate}</td>
</tr>
<tr>
<td className="border px-3 py-2 bg-gray-100"></td>
<td className="border px-3 py-2">{data.client}</td>
<td className="border px-3 py-2 bg-gray-100"></td>
<td className="border px-3 py-2">{data.siteName}</td>
</tr>
<tr>
<td className="border px-3 py-2 bg-gray-100"></td>
<td className="border px-3 py-2">{data.manager}</td>
<td className="border px-3 py-2 bg-gray-100"></td>
<td className="border px-3 py-2">{data.managerContact}</td>
</tr>
<tr>
<td className="border px-3 py-2 bg-gray-100"></td>
<td className="border px-3 py-2">{data.deliveryRequestDate}</td>
<td className="border px-3 py-2 bg-gray-100"></td>
<td className="border px-3 py-2">{data.expectedShipDate}</td>
</tr>
<tr>
<td className="border px-3 py-2 bg-gray-100"></td>
<td className="border px-3 py-2">{data.deliveryMethod}</td>
<td className="border px-3 py-2 bg-gray-100"></td>
<td className="border px-3 py-2">{data.address}</td>
</tr>
</tbody>
</table>
{/* 품목 테이블 */}
<table className="w-full border-collapse mb-6 text-xs">
<thead>
<tr className="bg-gray-100">
<th className="border px-2 py-2 w-10">No</th>
<th className="border px-2 py-2"></th>
<th className="border px-2 py-2 w-24"></th>
<th className="border px-2 py-2 w-12"></th>
<th className="border px-2 py-2 w-12"></th>
<th className="border px-2 py-2 w-20"></th>
<th className="border px-2 py-2 w-24"></th>
</tr>
</thead>
<tbody>
{data.items.map((item, index) => (
<tr key={item.id}>
<td className="border px-2 py-2 text-center">{index + 1}</td>
<td className="border px-2 py-2">{item.name}</td>
<td className="border px-2 py-2 text-center">{item.specification}</td>
<td className="border px-2 py-2 text-center">{item.unit}</td>
<td className="border px-2 py-2 text-center">{item.quantity}</td>
<td className="border px-2 py-2 text-right">{item.unitPrice?.toLocaleString()}</td>
<td className="border px-2 py-2 text-right">{item.amount?.toLocaleString()}</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<td colSpan={5} className="border px-2 py-2 text-right bg-gray-50"></td>
<td colSpan={2} className="border px-2 py-2 text-right">{data.subtotal.toLocaleString()}</td>
</tr>
<tr>
<td colSpan={5} className="border px-2 py-2 text-right bg-gray-50"> ({data.discountRate}%)</td>
<td colSpan={2} className="border px-2 py-2 text-right text-red-600">-{(data.subtotal * data.discountRate / 100).toLocaleString()}</td>
</tr>
<tr>
<td colSpan={5} className="border px-2 py-2 text-right bg-gray-100 font-bold"></td>
<td colSpan={2} className="border px-2 py-2 text-right font-bold text-blue-600">{data.totalAmount.toLocaleString()}</td>
</tr>
</tfoot>
</table>
{/* 비고 */}
{data.remarks && (
<div className="border p-4">
<h3 className="font-medium mb-2 text-xs"></h3>
<p className="text-xs text-gray-600">{data.remarks}</p>
</div>
)}
</div>
);
};
// 작업일지 문서 컴포넌트 (간소화 버전)
const WorkLogDocument = () => {
const order = MOCK_WORK_ORDER;
const today = new Date().toLocaleDateString('ko-KR').replace(/\. /g, '-').replace('.', '');
const documentNo = `WL-${order.process.toUpperCase().slice(0, 3)}`;
const lotNo = `KD-TS-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-01`;
const items = [
{ no: 1, name: order.productName, location: '1층/A-01', spec: '3000×2500', qty: 1, status: '완료' },
{ no: 2, name: order.productName, location: '2층/A-02', spec: '3000×2500', qty: 1, status: '작업중' },
{ no: 3, name: order.productName, location: '3층/A-03', spec: '-', qty: 1, status: '대기' },
];
return (
<div className="bg-white p-8 w-full text-sm shadow-sm">
{/* 헤더 */}
<div className="flex justify-between items-start mb-6 border border-gray-300">
<div className="w-24 border-r border-gray-300 flex flex-col items-center justify-center p-3">
<span className="text-2xl font-bold">KD</span>
<span className="text-xs text-gray-500"></span>
</div>
<div className="flex-1 flex flex-col items-center justify-center p-3 border-r border-gray-300">
<h1 className="text-xl font-bold tracking-widest mb-1"> </h1>
<p className="text-xs text-gray-500">{documentNo}</p>
<p className="text-sm font-medium mt-1"> </p>
</div>
<table className="text-xs shrink-0">
<tbody>
<tr>
<td rowSpan={3} className="w-8 text-center font-medium bg-gray-100 border-r border-b border-gray-300">
<div className="flex flex-col items-center"><span></span><span></span></div>
</td>
<td className="w-16 p-2 text-center font-medium bg-gray-100 border-r border-b border-gray-300"></td>
<td className="w-16 p-2 text-center font-medium bg-gray-100 border-r border-b border-gray-300"></td>
<td className="w-16 p-2 text-center font-medium bg-gray-100 border-b border-gray-300"></td>
</tr>
<tr>
<td className="w-16 p-2 text-center border-r border-b border-gray-300">
<div>{order.assignees[0] || '-'}</div>
</td>
<td className="w-16 p-2 text-center border-r border-b border-gray-300"></td>
<td className="w-16 p-2 text-center border-b border-gray-300"></td>
</tr>
<tr>
<td className="w-16 p-2 text-center bg-gray-50 border-r border-gray-300"></td>
<td className="w-16 p-2 text-center bg-gray-50 border-r border-gray-300"></td>
<td className="w-16 p-2 text-center bg-gray-50"></td>
</tr>
</tbody>
</table>
</div>
{/* 기본 정보 */}
<div className="border border-gray-300 mb-6">
<div className="grid grid-cols-2 border-b border-gray-300">
<div className="flex border-r border-gray-300">
<div className="w-24 bg-gray-100 p-3 text-sm font-medium border-r border-gray-300"></div>
<div className="flex-1 p-3 text-sm">{order.client}</div>
</div>
<div className="flex">
<div className="w-24 bg-gray-100 p-3 text-sm font-medium border-r border-gray-300"></div>
<div className="flex-1 p-3 text-sm">{order.projectName}</div>
</div>
</div>
<div className="grid grid-cols-2 border-b border-gray-300">
<div className="flex border-r border-gray-300">
<div className="w-24 bg-gray-100 p-3 text-sm font-medium border-r border-gray-300"></div>
<div className="flex-1 p-3 text-sm">{today}</div>
</div>
<div className="flex">
<div className="w-24 bg-gray-100 p-3 text-sm font-medium border-r border-gray-300">LOT NO.</div>
<div className="flex-1 p-3 text-sm">{lotNo}</div>
</div>
</div>
<div className="grid grid-cols-2">
<div className="flex border-r border-gray-300">
<div className="w-24 bg-gray-100 p-3 text-sm font-medium border-r border-gray-300"></div>
<div className="flex-1 p-3 text-sm">{order.dueDate}</div>
</div>
<div className="flex">
<div className="w-24 bg-gray-100 p-3 text-sm font-medium border-r border-gray-300"></div>
<div className="flex-1 p-3 text-sm">{order.quantity} EA</div>
</div>
</div>
</div>
{/* 품목 테이블 */}
<div className="border border-gray-300 mb-6">
<div className="grid grid-cols-12 border-b border-gray-300 bg-gray-100">
<div className="col-span-1 p-2 text-sm font-medium text-center border-r border-gray-300">No</div>
<div className="col-span-4 p-2 text-sm font-medium text-center border-r border-gray-300"></div>
<div className="col-span-2 p-2 text-sm font-medium text-center border-r border-gray-300">/</div>
<div className="col-span-2 p-2 text-sm font-medium text-center border-r border-gray-300"></div>
<div className="col-span-1 p-2 text-sm font-medium text-center border-r border-gray-300"></div>
<div className="col-span-2 p-2 text-sm font-medium text-center"></div>
</div>
{items.map((item, index) => (
<div key={item.no} className={`grid grid-cols-12 ${index < items.length - 1 ? 'border-b border-gray-300' : ''}`}>
<div className="col-span-1 p-2 text-sm text-center border-r border-gray-300">{item.no}</div>
<div className="col-span-4 p-2 text-sm border-r border-gray-300">{item.name}</div>
<div className="col-span-2 p-2 text-sm text-center border-r border-gray-300">{item.location}</div>
<div className="col-span-2 p-2 text-sm text-center border-r border-gray-300">{item.spec}</div>
<div className="col-span-1 p-2 text-sm text-center border-r border-gray-300">{item.qty}</div>
<div className="col-span-2 p-2 text-sm text-center">
<span className={`px-2 py-0.5 rounded text-xs ${
item.status === '완료' ? 'bg-green-100 text-green-700' :
item.status === '작업중' ? 'bg-blue-100 text-blue-700' :
'bg-gray-100 text-gray-700'
}`}>{item.status}</span>
</div>
</div>
))}
</div>
{/* 특이사항 */}
<div className="border border-gray-300">
<div className="bg-gray-800 text-white p-2.5 text-sm font-medium text-center"></div>
<div className="p-4 min-h-[60px] text-sm">{order.instruction || '-'}</div>
</div>
</div>
);
};
export const InspectionModal = ({ isOpen, onClose, document: doc, documentItem }: InspectionModalProps) => {
if (!doc) return null;
const docInfo = DOCUMENT_INFO[doc.type] || { label: doc.title, hasTemplate: false, color: 'text-gray-600' };
const subtitle = documentItem
? `${docInfo.label} - ${documentItem.date}${documentItem.code ? ` 로트: ${documentItem.code}` : ''}`
: docInfo.label;
const handlePrint = () => {
window.print();
};
// 중간검사 성적서 서브타입에 따른 렌더링
const renderReportDocument = () => {
const subType = documentItem?.subType;
switch (subType) {
case 'screen':
return <ScreenInspectionDocument />;
case 'bending':
return <BendingInspectionDocument />;
case 'slat':
return <SlatInspectionDocument />;
case 'jointbar':
return <JointbarInspectionDocument />;
default:
// 서브타입이 없으면 기본 스크린 문서
return <ScreenInspectionDocument />;
}
};
// 품질관리서 PDF 업로드 핸들러
const handleQualityFileUpload = (file: File) => {
console.log('[InspectionModal] 품질관리서 PDF 업로드:', file.name);
// TODO: 실제 API 연동 시 파일 업로드 로직 구현
};
const handleQualityFileDelete = () => {
console.log('[InspectionModal] 품질관리서 PDF 삭제');
// TODO: 실제 API 연동 시 파일 삭제 로직 구현
};
// 문서 타입에 따른 컨텐츠 렌더링
const renderDocumentContent = () => {
switch (doc.type) {
case 'order':
return <OrderDocument />;
case 'log':
return <WorkLogDocument />;
case 'confirmation':
return <DeliveryConfirmation data={MOCK_SHIPMENT_DETAIL} />;
case 'shipping':
return <ShippingSlip data={MOCK_SHIPMENT_DETAIL} />;
case 'import':
return <ImportInspectionDocument />;
case 'product':
return <ProductInspectionDocument />;
case 'report':
return renderReportDocument();
case 'quality':
// 품질관리서는 PDF 업로드/뷰어 사용
return (
<QualityDocumentUploader
onFileUpload={handleQualityFileUpload}
onFileDelete={handleQualityFileDelete}
/>
);
default:
// 양식 대기 중인 문서
return <PlaceholderDocument docType={doc.type} docItem={documentItem} />;
}
};
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="w-[95vw] max-w-[1200px] sm:max-w-[1200px] h-[90vh] p-0 overflow-hidden bg-gray-50 flex flex-col">
<DialogHeader className="p-4 bg-white border-b border-gray-200 flex flex-row items-center justify-between space-y-0 shrink-0">
<div>
<DialogTitle className="text-lg font-bold text-gray-800">{doc.title}</DialogTitle>
<p className="text-xs text-gray-500 mt-1">{subtitle}</p>
</div>
</DialogHeader>
{/* Toolbar */}
<div className="flex items-center justify-between px-4 py-2 bg-white border-b border-gray-100 shrink-0">
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" className="h-8 gap-1 text-xs">
<ZoomOut size={14} />
</Button>
<Button variant="ghost" size="sm" className="h-8 gap-1 text-xs">
<ZoomIn size={14} />
</Button>
<Button variant="ghost" size="sm" className="h-8 gap-1 text-xs">
<RotateCw size={14} />
</Button>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-mono text-gray-600">100%</span>
<Button variant="outline" size="sm" className="h-8 gap-1 text-xs" onClick={handlePrint}>
<Printer size={14} />
</Button>
<Button size="sm" className="h-8 bg-green-600 hover:bg-green-700 text-white gap-1 text-xs">
<Download size={14} />
</Button>
</div>
</div>
{/* Content Area - 남은 공간 모두 사용 */}
<div className="flex-1 p-4 overflow-auto bg-gray-100">
{renderDocumentContent()}
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -4,50 +4,62 @@ import React from 'react';
import { Package } from 'lucide-react';
import { InspectionReport } from '../types';
const MOCK_REPORTS: InspectionReport[] = [
{
id: '1',
code: 'KD-SS-2024-530',
siteName: '강남 아파트 단지',
item: '실리카 스크린',
routeCount: 2,
totalRoutes: 14,
quarter: '2025년 3분기'
}
];
interface ReportListProps {
reports: InspectionReport[];
selectedId: string | null;
onSelect: (report: InspectionReport) => void;
}
export const ReportList = () => {
return (
<div className="bg-white rounded-lg p-4 shadow-sm h-full">
<div className="flex items-center justify-between mb-4">
<h2 className="font-bold text-lg text-gray-800"> </h2>
<span className="bg-blue-100 text-blue-800 text-xs font-bold px-2 py-1 rounded-full">
{MOCK_REPORTS.length}
</span>
</div>
export const ReportList = ({ reports, selectedId, onSelect }: ReportListProps) => {
return (
<div className="bg-white rounded-lg p-4 shadow-sm h-full flex flex-col">
<div className="flex items-center justify-between mb-4">
<h2 className="font-bold text-lg text-gray-800"> </h2>
<span className="bg-blue-100 text-blue-800 text-xs font-bold px-2 py-1 rounded-full">
{reports.length}
</span>
</div>
<div className="space-y-3">
{MOCK_REPORTS.map((report) => (
<div
key={report.id}
className="border border-blue-500 rounded-lg p-4 bg-blue-50 cursor-pointer relative hover:shadow-md transition-shadow"
>
<div className="absolute top-4 right-4 text-xs text-gray-400 bg-gray-100 px-2 py-1 rounded">
{report.quarter}
</div>
<div className="space-y-3 overflow-y-auto flex-1">
{reports.length === 0 ? (
<div className="flex items-center justify-center h-32 text-gray-400 text-sm">
.
</div>
) : (
reports.map((report) => {
const isSelected = selectedId === report.id;
return (
<div
key={report.id}
onClick={() => onSelect(report)}
className={`rounded-lg p-4 cursor-pointer relative hover:shadow-md transition-all ${
isSelected
? 'border-2 border-blue-500 bg-blue-50'
: 'border border-gray-200 bg-white hover:border-blue-300'
}`}
>
<div className="absolute top-4 right-4 text-xs text-gray-400 bg-gray-100 px-2 py-1 rounded">
{report.quarter}
</div>
<h3 className="font-bold text-blue-900 text-lg mb-1">{report.code}</h3>
<p className="text-gray-700 font-medium mb-1">{report.siteName}</p>
<p className="text-sm text-gray-500 mb-3">: {report.item}</p>
<h3 className={`font-bold text-lg mb-1 ${isSelected ? 'text-blue-900' : 'text-gray-800'}`}>
{report.code}
</h3>
<p className="text-gray-700 font-medium mb-1">{report.siteName}</p>
<p className="text-sm text-gray-500 mb-3">: {report.item}</p>
<div className="flex items-center gap-2 bg-blue-100 p-2 rounded text-sm text-blue-700 font-medium">
<Package size={16} />
<span> {report.routeCount}</span>
<span className="text-gray-400 text-xs ml-1">( {report.totalRoutes})</span>
</div>
</div>
))}
</div>
</div>
);
};
<div className={`flex items-center gap-2 p-2 rounded text-sm font-medium ${
isSelected ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-600'
}`}>
<Package size={16} />
<span> {report.routeCount}</span>
<span className="text-gray-400 text-xs ml-1">( {report.totalRoutes})</span>
</div>
</div>
);
})
)}
</div>
</div>
);
};

View File

@@ -4,89 +4,103 @@ import React, { useState } from 'react';
import { ChevronDown, ChevronUp, MapPin } from 'lucide-react';
import { RouteItem } from '../types';
const MOCK_ROUTES: RouteItem[] = [
{
id: '1',
code: 'KD-SS-240924-19',
date: '2024-09-24',
site: '강남 아파트 A동',
locationCount: 7,
subItems: [
{ id: '1-1', name: 'KD-SS-240924-19-01', location: '101동 501호', status: '합격' },
{ id: '1-2', name: 'KD-SS-240924-19-02', location: '101동 502호', status: '합격' },
{ id: '1-3', name: 'KD-SS-240924-19-03', location: '101동 503호', status: '합격' },
{ id: '1-4', name: 'KD-SS-240924-19-04', location: '101동 601호', status: '합격' },
{ id: '1-5', name: 'KD-SS-240924-19-05', location: '101동 602호', status: '합격' },
{ id: '1-6', name: 'KD-SS-240924-19-06', location: '101동 603호', status: '합격' },
{ id: '1-7', name: 'KD-SS-240924-19-07', location: '102동 501호', status: '합격' },
]
},
{
id: '2',
code: 'KD-SS-241024-15',
date: '2024-10-24',
site: '강남 아파트 B동',
locationCount: 7,
subItems: []
}
];
interface RouteListProps {
routes: RouteItem[];
selectedId: string | null;
onSelect: (route: RouteItem) => void;
reportCode: string | null;
}
export const RouteList = () => {
const [expandedId, setExpandedId] = useState<string | null>('1');
export const RouteList = ({ routes, selectedId, onSelect, reportCode }: RouteListProps) => {
const [expandedId, setExpandedId] = useState<string | null>(null);
const toggleExpand = (id: string) => {
setExpandedId(expandedId === id ? null : id);
};
const handleClick = (route: RouteItem) => {
onSelect(route);
setExpandedId(expandedId === route.id ? null : route.id);
};
return (
<div className="bg-white rounded-lg p-4 shadow-sm h-full overflow-y-auto">
<h2 className="font-bold text-gray-800 text-sm mb-4">
<span className="text-gray-400 font-normal ml-1">(KD-SS-2024-530)</span>
</h2>
return (
<div className="bg-white rounded-lg p-4 shadow-sm h-full flex flex-col overflow-hidden">
<h2 className="font-bold text-gray-800 text-sm mb-4">
{' '}
{reportCode && (
<span className="text-gray-400 font-normal ml-1">({reportCode})</span>
)}
</h2>
<div className="space-y-3">
{MOCK_ROUTES.map((route) => (
<div key={route.id} className="border border-gray-200 rounded-lg overflow-hidden">
<div
onClick={() => toggleExpand(route.id)}
className={`p-4 cursor-pointer flex justify-between items-start transition-colors ${expandedId === route.id ? 'bg-green-50 border-b border-green-100' : 'bg-white hover:bg-gray-50'}`}
>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
{expandedId === route.id && <div className="w-1 h-4 bg-green-500 rounded-full"></div>}
<h3 className={`font-bold ${expandedId === route.id ? 'text-green-700' : 'text-gray-700'}`}>{route.code}</h3>
</div>
<p className="text-xs text-gray-500 mb-1">: {route.date}</p>
<p className="text-xs text-gray-500 mb-2">: {route.site}</p>
<div className="inline-flex items-center gap-1 bg-gray-100 px-2 py-0.5 rounded text-xs text-gray-600">
<MapPin size={10} />
<span>{route.locationCount}</span>
</div>
</div>
{expandedId === route.id ? <ChevronUp size={16} className="text-gray-400" /> : <ChevronDown size={16} className="text-gray-400" />}
</div>
<div className="space-y-3 overflow-y-auto flex-1">
{routes.length === 0 ? (
<div className="flex items-center justify-center h-32 text-gray-400 text-sm">
{reportCode ? '수주루트가 없습니다.' : '품질관리서를 선택해주세요.'}
</div>
) : (
routes.map((route) => {
const isSelected = selectedId === route.id;
const isExpanded = expandedId === route.id;
{expandedId === route.id && (
<div className="bg-white p-3 space-y-2">
<div className="text-xs font-bold text-gray-600 mb-2 flex items-center gap-1">
<MapPin size={10} />
</div>
{route.subItems.map((item) => (
<div key={item.id} className="flex items-center justify-between border border-gray-100 p-2 rounded hover:bg-gray-50">
<div>
<div className="text-xs font-bold text-green-700">{item.name}</div>
<div className="text-xs text-gray-500">{item.location}</div>
</div>
<span className="text-[10px] font-bold text-green-600 border border-green-200 bg-green-50 px-1.5 py-0.5 rounded">
{item.status}
</span>
</div>
))}
</div>
)}
return (
<div key={route.id} className="border border-gray-200 rounded-lg overflow-hidden">
<div
onClick={() => handleClick(route)}
className={`p-4 cursor-pointer flex justify-between items-start transition-colors ${
isSelected ? 'bg-green-50 border-b border-green-100' : 'bg-white hover:bg-gray-50'
}`}
>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
{isSelected && <div className="w-1 h-4 bg-green-500 rounded-full" />}
<h3 className={`font-bold ${isSelected ? 'text-green-700' : 'text-gray-700'}`}>
{route.code}
</h3>
</div>
))}
</div>
</div>
);
};
<p className="text-xs text-gray-500 mb-1">: {route.date}</p>
<p className="text-xs text-gray-500 mb-2">: {route.site}</p>
<div className="inline-flex items-center gap-1 bg-gray-100 px-2 py-0.5 rounded text-xs text-gray-600">
<MapPin size={10} />
<span>{route.locationCount}</span>
</div>
</div>
{isExpanded ? (
<ChevronUp size={16} className="text-gray-400" />
) : (
<ChevronDown size={16} className="text-gray-400" />
)}
</div>
{isExpanded && route.subItems.length > 0 && (
<div className="bg-white p-3 space-y-2">
<div className="text-xs font-bold text-gray-600 mb-2 flex items-center gap-1">
<MapPin size={10} />
</div>
{route.subItems.map((item) => (
<div
key={item.id}
className="flex items-center justify-between border border-gray-100 p-2 rounded hover:bg-gray-50"
>
<div>
<div className="text-xs font-bold text-green-700">{item.name}</div>
<div className="text-xs text-gray-500">{item.location}</div>
</div>
<span
className={`text-[10px] font-bold px-1.5 py-0.5 rounded border ${
item.status === '합격'
? 'text-green-600 border-green-200 bg-green-50'
: item.status === '불합격'
? 'text-red-600 border-red-200 bg-red-50'
: 'text-yellow-600 border-yellow-200 bg-yellow-50'
}`}
>
{item.status}
</span>
</div>
))}
</div>
)}
</div>
);
})
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,349 @@
"use client";
import React from 'react';
// 절곡품 중간검사 성적서 데이터 타입
export interface BendingInspectionData {
documentNo: string;
productName: string;
productType: '필재' | '스크린';
specification: string;
client: string;
siteName: string;
itemName: string;
lotNo: string;
lotSize: string;
inspectionDate: string;
inspector: string;
finishType: string;
susFinish: string;
approvers: {
writer?: string;
reviewer?: string;
approver?: string;
};
// 중간검사 기준서 정보
standardInfo: {
caseFinish: {
appearance: { criteria: string; method: string; frequency: string; regulation: string };
dimensions: { criteria: string; method: string; frequency: string; regulation: string };
inspection: { criteria: string; method: string; frequency: string; regulation: string };
};
bandFinish: {
appearance: { criteria: string; method: string; frequency: string; regulation: string };
dimensions: { criteria: string; method: string; frequency: string; regulation: string };
inspection: { criteria: string; method: string; frequency: string; regulation: string };
};
};
// 중간검사 DATA
inspectionData: {
category: string;
itemName: string;
type: string;
bendState: '양호' | '불량';
length: number;
conductance1: string;
measured1: number | null;
point: number;
conductance2: string;
measured2: number | null;
result: '적합' | '부적합';
}[];
notes: string;
overallResult: '합격' | '불합격';
}
// Mock 데이터
export const MOCK_BENDING_INSPECTION: BendingInspectionData = {
documentNo: 'KDQP-01-007',
productName: '절곡품',
productType: '스크린',
specification: '□ 필재 □ 스크린',
client: '경동기업',
siteName: '용산고등학교(4호)',
itemName: 'KWE01',
lotNo: 'KD-WE-251015-01-(3)',
lotSize: '11 개소',
inspectionDate: '2025.',
inspector: '',
finishType: '마감유형',
susFinish: 'SUS마감',
approvers: {
writer: '전진',
reviewer: '',
approver: '',
},
standardInfo: {
caseFinish: {
appearance: {
criteria: '사용상 해로운 결함이 없을 것',
method: '절무검사',
frequency: 'n = L, c = 0',
regulation: 'KS F 4510 5.1항',
},
dimensions: {
criteria: '길이\n도전차수 ± 4',
method: '체크검사',
frequency: 'n = L, c = 0',
regulation: 'KS F 4510 7항\n외주 자재검규',
},
inspection: {
criteria: '도전차수 ± 2',
method: '',
frequency: '',
regulation: '',
},
},
bandFinish: {
appearance: {
criteria: '사용상 해로운 결함이 없을 것',
method: '절무검사',
frequency: 'n = L, c = 0',
regulation: 'KS F 4510 5.1항',
},
dimensions: {
criteria: '길이\n도전차수 ± 4\nW50 : 50 ± 5\nW80 : 80 ± 5',
method: '',
frequency: '',
regulation: 'KS F 4510 7항\n외 9',
},
inspection: {
criteria: '도전차수 ± 2',
method: '체크검사',
frequency: '',
regulation: '',
},
},
},
inspectionData: [
{ category: '필재\n(K0150)', itemName: '가이드레일', type: '아연판', bendState: '양호', length: 4300, conductance1: 'N/A', measured1: null, point: 30, conductance2: '', measured2: null, result: '적합' },
{ category: '', itemName: '', type: '', bendState: '양호', length: 0, conductance1: '', measured1: null, point: 78, conductance2: '', measured2: null, result: '적합' },
{ category: '', itemName: '', type: '', bendState: '양호', length: 0, conductance1: '', measured1: null, point: 25, conductance2: '', measured2: null, result: '적합' },
{ category: '', itemName: '', type: '', bendState: '양호', length: 0, conductance1: '', measured1: null, point: 45, conductance2: '', measured2: null, result: '적합' },
],
notes: '',
overallResult: '합격',
};
interface BendingInspectionDocumentProps {
data?: BendingInspectionData;
}
export const BendingInspectionDocument = ({ data = MOCK_BENDING_INSPECTION }: BendingInspectionDocumentProps) => {
return (
<div className="bg-white p-8 w-full text-sm shadow-sm print:shadow-none">
{/* 헤더 */}
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-2">
<div className="text-2xl font-bold">KD</div>
<div className="text-xs text-gray-600"><br/>KYUNGDONG COMPANY</div>
</div>
<div className="text-center">
<div className="text-xl font-bold"></div>
<div className="text-xl font-bold tracking-[0.2rem]"> </div>
</div>
<table className="text-xs border-collapse">
<tbody>
<tr>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-8 text-center" rowSpan={3}>
<div className="flex flex-col items-center">
<span></span><span></span>
</div>
</td>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center"></td>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center"></td>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center"></td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 h-8 text-center font-medium">{data.approvers.writer}</td>
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.reviewer}</td>
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.approver}</td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">/</td>
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50"></td>
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50"></td>
</tr>
</tbody>
</table>
</div>
{/* 기본 정보 테이블 */}
<table className="w-full border-collapse mb-4 text-xs">
<tbody>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 w-20 font-medium"> </td>
<td className="border border-gray-400 px-3 py-2">{data.productName}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 w-24 font-medium"> LOT NO</td>
<td className="border border-gray-400 px-3 py-2">{data.lotNo}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"> </td>
<td className="border border-gray-400 px-3 py-2">
<span className={data.productType === '필재' ? 'font-bold' : ''}> </span>
{' '}
<span className={data.productType === '스크린' ? 'font-bold' : ''}> </span>
</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.lotSize}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.client}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.inspectionDate}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.siteName}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.inspector}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.itemName}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.susFinish}</td>
</tr>
</tbody>
</table>
{/* 중간검사 기준서 - 케이스/커버마감재 */}
<table className="w-full border-collapse mb-2 text-xs">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-400 px-2 py-1 w-24" rowSpan={4}><br/><br/><br/></th>
<th className="border border-gray-400 px-2 py-1 w-16"></th>
<th className="border border-gray-400 px-2 py-1 w-16"></th>
<th className="border border-gray-400 px-2 py-1"></th>
<th className="border border-gray-400 px-2 py-1 w-14"></th>
<th className="border border-gray-400 px-2 py-1 w-14"></th>
<th className="border border-gray-400 px-2 py-1 w-24"></th>
</tr>
</thead>
<tbody>
<tr>
<td className="border border-gray-400 p-1 text-center align-middle" rowSpan={3}>
<div className="w-14 h-16 mx-auto border border-gray-300 bg-gray-50 flex items-center justify-center text-[8px] text-gray-400">
<br/>
</div>
</td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1">{data.standardInfo.caseFinish.appearance.criteria}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{data.standardInfo.caseFinish.appearance.method}</td>
<td className="border border-gray-400 px-1 py-1 text-center whitespace-pre-line">{data.standardInfo.caseFinish.appearance.frequency}</td>
<td className="border border-gray-400 px-1 py-1 text-center text-[9px]">{data.standardInfo.caseFinish.appearance.regulation}</td>
</tr>
<tr>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 whitespace-pre-line">{data.standardInfo.caseFinish.dimensions.criteria}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{data.standardInfo.caseFinish.dimensions.method}</td>
<td className="border border-gray-400 px-1 py-1 text-center whitespace-pre-line">{data.standardInfo.caseFinish.dimensions.frequency}</td>
<td className="border border-gray-400 px-1 py-1 text-center text-[9px] whitespace-pre-line">{data.standardInfo.caseFinish.dimensions.regulation}</td>
</tr>
<tr>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1">{data.standardInfo.caseFinish.inspection.criteria}</td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
</tr>
</tbody>
</table>
{/* 중간검사 기준서 - 밴딩마감재 */}
<table className="w-full border-collapse mb-4 text-xs">
<tbody>
<tr>
<td className="border border-gray-400 px-2 py-1 w-24 bg-gray-100 text-center align-middle" rowSpan={3}><br/><br/></td>
<td className="border border-gray-400 p-1 text-center align-middle w-16" rowSpan={3}>
<div className="w-14 h-12 mx-auto border border-gray-300 bg-gray-50 flex items-center justify-center text-[8px] text-gray-400">
<br/>
</div>
</td>
<td className="border border-gray-400 px-1 py-1 text-center w-16"></td>
<td className="border border-gray-400 px-1 py-1">{data.standardInfo.bandFinish.appearance.criteria}</td>
<td className="border border-gray-400 px-1 py-1 text-center w-14">{data.standardInfo.bandFinish.appearance.method}</td>
<td className="border border-gray-400 px-1 py-1 text-center w-14 whitespace-pre-line">{data.standardInfo.bandFinish.appearance.frequency}</td>
<td className="border border-gray-400 px-1 py-1 text-center w-24 text-[9px]">{data.standardInfo.bandFinish.appearance.regulation}</td>
</tr>
<tr>
<td className="border border-gray-400 px-1 py-1 text-center"><br/>(mm)</td>
<td className="border border-gray-400 px-1 py-1 whitespace-pre-line">{data.standardInfo.bandFinish.dimensions.criteria}</td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 text-center text-[9px] whitespace-pre-line">{data.standardInfo.bandFinish.dimensions.regulation}</td>
</tr>
<tr>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1">{data.standardInfo.bandFinish.inspection.criteria}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{data.standardInfo.bandFinish.inspection.method}</td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
</tr>
</tbody>
</table>
{/* 중간검사 DATA */}
<div className="mb-2 text-xs font-medium"> DATA</div>
<table className="w-full border-collapse text-xs mb-4">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-400 px-1 py-1" rowSpan={2}></th>
<th className="border border-gray-400 px-1 py-1" rowSpan={2}></th>
<th className="border border-gray-400 px-1 py-1" rowSpan={2}></th>
<th className="border border-gray-400 px-1 py-1" rowSpan={2}></th>
<th className="border border-gray-400 px-1 py-1" colSpan={3}> [mm]</th>
<th className="border border-gray-400 px-1 py-1" colSpan={2}> </th>
<th className="border border-gray-400 px-1 py-1" rowSpan={2}></th>
</tr>
<tr className="bg-gray-100">
<th className="border border-gray-400 px-1 py-1"></th>
<th className="border border-gray-400 px-1 py-1"></th>
<th className="border border-gray-400 px-1 py-1"></th>
<th className="border border-gray-400 px-1 py-1">POINT</th>
<th className="border border-gray-400 px-1 py-1"></th>
</tr>
</thead>
<tbody>
{data.inspectionData.map((item, index) => (
<tr key={index}>
<td className="border border-gray-400 px-1 py-1 text-center whitespace-pre-line">{item.category}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.itemName}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.type}</td>
<td className="border border-gray-400 px-1 py-1 text-center">
</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.length > 0 ? item.length.toLocaleString() : ''}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.conductance1}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.measured1}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.point}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.conductance2}</td>
<td className="border border-gray-400 px-1 py-1 text-center">
</td>
</tr>
))}
</tbody>
</table>
{/* 부적합 내용 */}
<div className="border border-gray-400 mb-4">
<div className="bg-gray-100 px-3 py-1 text-xs font-medium border-b border-gray-400"> </div>
<div className="px-3 py-2 text-xs text-gray-600 min-h-[30px]">{data.notes}</div>
</div>
{/* 문서번호 및 종합판정 */}
<div className="flex justify-between items-end">
<div className="text-xs text-gray-500">{data.documentNo}</div>
<div className="border border-gray-400">
<div className="bg-gray-100 px-4 py-1 text-center text-xs font-medium border-b border-gray-400"></div>
<div className={`px-8 py-2 text-center text-sm font-bold ${data.overallResult === '합격' ? 'text-blue-600' : 'text-red-600'}`}>
{data.overallResult}
</div>
</div>
<div className="text-xs text-gray-500">KDPS-10-01</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,419 @@
"use client";
import React from 'react';
// 수입검사 성적서 데이터 타입
export interface ImportInspectionData {
// 헤더 정보
documentNo: string;
reportDate: string;
productName: string;
specification: string;
materialNo: string;
lotSize: number;
supplier: string;
lotNo: string;
inspectionDate: string;
inspector: string;
approvers: {
writer?: string;
reviewer?: string;
approver?: string;
};
// 검사 항목
inspectionItems: {
appearance: {
result: 'OK' | 'NG';
measurements: ('OK' | 'NG')[];
};
dimensions: {
thickness: {
standard: number;
tolerance: string;
measurements: number[];
};
width: {
standard: number;
tolerance: string;
measurements: number[];
};
length: {
standard: number;
tolerance: string;
measurements: number[];
};
};
tensileStrength: {
standard: string;
measurements: number[];
};
elongation: {
thicknessRange: string;
standard: string;
supplier: string;
measurements: number[];
};
zincCoating: {
standard: string;
measurements: number[];
};
};
overallResult: '합격' | '불합격';
}
// Mock 데이터
export const MOCK_IMPORT_INSPECTION: ImportInspectionData = {
documentNo: 'KDQP-01-001',
reportDate: '2025-07-15',
productName: '전기 아연도금 강판 (KS D 3528, SECC) "EGI 평국판"',
specification: '1.55 * 1218 × 480',
materialNo: 'PE02RB',
lotSize: 200,
supplier: '지오TNS (KG스틸)',
lotNo: '250715-02',
inspectionDate: '07/15',
inspector: '노원호',
approvers: {
writer: '노원호',
reviewer: '',
approver: '',
},
inspectionItems: {
appearance: {
result: 'OK',
measurements: ['OK', 'OK', 'OK'],
},
dimensions: {
thickness: {
standard: 1.55,
tolerance: '±0.10',
measurements: [1.528, 1.533, 1.521],
},
width: {
standard: 1219,
tolerance: '±7',
measurements: [1222, 1222, 1222],
},
length: {
standard: 480,
tolerance: '±15',
measurements: [480, 480, 480],
},
},
tensileStrength: {
standard: '270 이상',
measurements: [313.8],
},
elongation: {
thicknessRange: '두께 1.0 이상 ~ 1.6 미만',
standard: '37 이상',
supplier: '공급업체 밀시트',
measurements: [46.5],
},
zincCoating: {
standard: '편면 17 이상',
measurements: [17.21, 17.17],
},
},
overallResult: '합격',
};
interface ImportInspectionDocumentProps {
data?: ImportInspectionData;
}
export const ImportInspectionDocument = ({ data = MOCK_IMPORT_INSPECTION }: ImportInspectionDocumentProps) => {
return (
<div className="bg-white p-8 w-full text-sm shadow-sm print:shadow-none">
{/* 헤더 */}
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-2">
<div className="text-2xl font-bold">KD</div>
<div className="text-xs text-gray-600"></div>
</div>
<div className="text-2xl font-bold tracking-[0.3rem]"> </div>
<div className="text-right">
<table className="text-xs border-collapse">
<tbody>
<tr>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-12"></td>
<td className="border border-gray-400 px-2 py-1 w-16"></td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 bg-gray-100"></td>
<td className="border border-gray-400 px-2 py-1 h-8">{data.approvers.writer}</td>
</tr>
</tbody>
</table>
<div className="text-xs text-right mt-1">: {data.reportDate}</div>
</div>
</div>
{/* 기본 정보 테이블 */}
<table className="w-full border-collapse mb-4 text-xs">
<tbody>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 w-20 font-medium"> </td>
<td className="border border-gray-400 px-3 py-2" colSpan={3}>{data.productName}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 w-20 font-medium"><br/>()</td>
<td className="border border-gray-400 px-3 py-2 w-28">{data.supplier}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"> <br/>(**)</td>
<td className="border border-gray-400 px-3 py-2">{data.specification}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 w-20 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.lotNo}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium" rowSpan={2}></td>
<td className="border border-gray-400 px-3 py-2" rowSpan={2}></td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.materialNo}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.inspectionDate}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.lotSize}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2"> {data.inspector} <span className="ml-2"></span></td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2"></td>
</tr>
</tbody>
</table>
{/* 검사 항목 테이블 */}
<table className="w-full border-collapse text-xs">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-400 px-2 py-2 w-10" rowSpan={2}>NO</th>
<th className="border border-gray-400 px-2 py-2 w-20" rowSpan={2}></th>
<th className="border border-gray-400 px-2 py-2" colSpan={2}></th>
<th className="border border-gray-400 px-2 py-2 w-16" rowSpan={2}></th>
<th className="border border-gray-400 px-2 py-2 w-16" rowSpan={2}></th>
<th className="border border-gray-400 px-2 py-2" colSpan={3}></th>
<th className="border border-gray-400 px-2 py-2 w-14" rowSpan={2}><br/>(/)</th>
</tr>
<tr className="bg-gray-100">
<th className="border border-gray-400 px-2 py-1"></th>
<th className="border border-gray-400 px-2 py-1"></th>
<th className="border border-gray-400 px-2 py-1 w-16">n1<br/>/</th>
<th className="border border-gray-400 px-2 py-1 w-16">n2<br/>/</th>
<th className="border border-gray-400 px-2 py-1 w-16">n3<br/>/</th>
</tr>
</thead>
<tbody>
{/* 1. 겉모양 */}
<tr>
<td className="border border-gray-400 px-2 py-2 text-center align-middle" rowSpan={1}>1</td>
<td className="border border-gray-400 px-2 py-2 text-center align-middle"></td>
<td className="border border-gray-400 px-2 py-2 text-center" colSpan={2}>
<br/>
</td>
<td className="border border-gray-400 px-2 py-2 text-center"></td>
<td className="border border-gray-400 px-2 py-2 text-center">
<span className={data.inspectionItems.appearance.result === 'OK' ? 'font-bold' : ''}>OK</span>
<span className={data.inspectionItems.appearance.result === 'NG' ? 'font-bold' : ''}>NG</span>
</td>
<td className="border border-gray-400 px-2 py-2 text-center">
<span className={data.inspectionItems.appearance.measurements[0] === 'OK' ? 'font-bold' : ''}>OK</span>
<span className={data.inspectionItems.appearance.measurements[0] === 'NG' ? 'font-bold' : ''}>NG</span>
</td>
<td className="border border-gray-400 px-2 py-2 text-center">
<span className={data.inspectionItems.appearance.measurements[1] === 'OK' ? 'font-bold' : ''}>OK</span>
<span className={data.inspectionItems.appearance.measurements[1] === 'NG' ? 'font-bold' : ''}>NG</span>
</td>
<td className="border border-gray-400 px-2 py-2 text-center">
<span className={data.inspectionItems.appearance.measurements[2] === 'OK' ? 'font-bold' : ''}>OK</span>
<span className={data.inspectionItems.appearance.measurements[2] === 'NG' ? 'font-bold' : ''}>NG</span>
</td>
<td className="border border-gray-400 px-2 py-2 text-center font-medium"></td>
</tr>
{/* 2. 치수 */}
<tr>
<td className="border border-gray-400 px-2 py-2 text-center align-middle" rowSpan={6}>2</td>
<td className="border border-gray-400 px-2 py-2 text-center align-middle" rowSpan={6}></td>
<td className="border border-gray-400 px-2 py-1 text-center align-middle" rowSpan={4}>
<br/>{data.inspectionItems.dimensions.thickness.standard}
</td>
<td className="border border-gray-400 px-1 py-1 text-xs">
<div className="flex items-center gap-1">
<span></span>
<span>0.8 <br/>~ 1.0 </span>
<span className="ml-auto">± 0.07</span>
</div>
</td>
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={6}>
n = 3<br/>c = 0
</td>
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={6}></td>
<td className="border border-gray-400 px-2 py-2 text-center" rowSpan={4}>
{data.inspectionItems.dimensions.thickness.measurements[0]}
</td>
<td className="border border-gray-400 px-2 py-2 text-center" rowSpan={4}>
{data.inspectionItems.dimensions.thickness.measurements[1]}
</td>
<td className="border border-gray-400 px-2 py-2 text-center" rowSpan={4}>
{data.inspectionItems.dimensions.thickness.measurements[2]}
</td>
<td className="border border-gray-400 px-2 py-2 text-center font-medium" rowSpan={6}></td>
</tr>
<tr>
<td className="border border-gray-400 px-1 py-1 text-xs">
<div className="flex items-center gap-1">
<span></span>
<span>1.0 <br/>~ 1.25 </span>
<span className="ml-auto">± 0.08</span>
</div>
</td>
</tr>
<tr>
<td className="border border-gray-400 px-1 py-1 text-xs bg-blue-50">
<div className="flex items-center gap-1">
<span></span>
<span>1.25 <br/>~ 1.6 </span>
<span className="ml-auto">± 0.10</span>
</div>
</td>
</tr>
<tr>
<td className="border border-gray-400 px-1 py-1 text-xs">
<div className="flex items-center gap-1">
<span></span>
<span>1.6 <br/>~ 2.0 </span>
<span className="ml-auto">± 0.12</span>
</div>
</td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 text-center align-middle" rowSpan={1}>
<br/>{data.inspectionItems.dimensions.width.standard}
</td>
<td className="border border-gray-400 px-1 py-1 text-xs bg-blue-50">
<div className="flex items-center gap-1">
<span></span>
<span>1250 </span>
<span className="ml-auto">+ 7<br/>- 0</span>
</div>
</td>
<td className="border border-gray-400 px-2 py-2 text-center">
{data.inspectionItems.dimensions.width.measurements[0]}
</td>
<td className="border border-gray-400 px-2 py-2 text-center">
{data.inspectionItems.dimensions.width.measurements[1]}
</td>
<td className="border border-gray-400 px-2 py-2 text-center">
{data.inspectionItems.dimensions.width.measurements[2]}
</td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 text-center align-middle" rowSpan={1}>
<br/>{data.inspectionItems.dimensions.length.standard}
</td>
<td className="border border-gray-400 px-1 py-1 text-xs bg-blue-50">
<div className="flex items-center gap-1">
<span></span>
<span>2000 <br/>~ 4000 </span>
<span className="ml-auto">+ 15<br/>- 0</span>
</div>
</td>
<td className="border border-gray-400 px-2 py-2 text-center">
{data.inspectionItems.dimensions.length.measurements[0]}
</td>
<td className="border border-gray-400 px-2 py-2 text-center">
{data.inspectionItems.dimensions.length.measurements[1]}
</td>
<td className="border border-gray-400 px-2 py-2 text-center">
{data.inspectionItems.dimensions.length.measurements[2]}
</td>
</tr>
{/* 3. 인장강도 */}
<tr>
<td className="border border-gray-400 px-2 py-2 text-center">3</td>
<td className="border border-gray-400 px-2 py-2 text-center"> (N/<br/>)</td>
<td className="border border-gray-400 px-2 py-2 text-center" colSpan={2}>
{data.inspectionItems.tensileStrength.standard}
</td>
<td className="border border-gray-400 px-2 py-2 text-center" colSpan={2}></td>
<td className="border border-gray-400 px-2 py-2 text-center" colSpan={3}>
{data.inspectionItems.tensileStrength.measurements[0]}
</td>
<td className="border border-gray-400 px-2 py-2 text-center font-medium"></td>
</tr>
{/* 4. 연신율 */}
<tr>
<td className="border border-gray-400 px-2 py-2 text-center" rowSpan={3}>4</td>
<td className="border border-gray-400 px-2 py-2 text-center" rowSpan={3}><br/>%</td>
<td className="border border-gray-400 px-1 py-1 text-xs" rowSpan={1}>
<div className="flex items-center gap-1">
<span></span>
<span> 0.6<br/><br/>~ 1.0 <br/></span>
<span className="ml-auto">36 </span>
</div>
</td>
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={3}>
<br/>
</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-2 text-center" rowSpan={3} colSpan={3}>
{data.inspectionItems.elongation.measurements[0]}
</td>
<td className="border border-gray-400 px-2 py-2 text-center font-medium" rowSpan={3}></td>
</tr>
<tr>
<td className="border border-gray-400 px-1 py-1 text-xs bg-blue-50">
<div className="flex items-center gap-1">
<span></span>
<span> 1.0<br/><br/>~ 1.6 <br/></span>
<span className="ml-auto">37 </span>
</div>
</td>
</tr>
<tr>
<td className="border border-gray-400 px-1 py-1 text-xs">
<div className="flex items-center gap-1">
<span></span>
<span> 1.6<br/><br/>~ 2.3<br/></span>
<span className="ml-auto">38 </span>
</div>
</td>
</tr>
{/* 5. 아연의 최소 부착량 */}
<tr>
<td className="border border-gray-400 px-2 py-2 text-center">5</td>
<td className="border border-gray-400 px-2 py-2 text-center"> <br/> (g/)</td>
<td className="border border-gray-400 px-2 py-2 text-center" colSpan={2}>
17
</td>
<td className="border border-gray-400 px-2 py-2 text-center" colSpan={2}></td>
<td className="border border-gray-400 px-2 py-2 text-center" colSpan={3}>
{data.inspectionItems.zincCoating.measurements.join(' / ')}
</td>
<td className="border border-gray-400 px-2 py-2 text-center font-medium"></td>
</tr>
</tbody>
</table>
{/* 주석 */}
<div className="mt-2 text-xs text-gray-600">
<p> 1.55mm의 KS F 4510 MIN 1.5 </p>
<p> 1000 ~ 1250 </p>
</div>
{/* 종합판정 */}
<div className="mt-4 flex justify-end">
<div className="border border-gray-400">
<div className="bg-gray-100 px-4 py-1 text-center text-xs font-medium border-b border-gray-400"></div>
<div className={`px-8 py-2 text-center text-sm font-bold ${data.overallResult === '합격' ? 'text-blue-600' : 'text-red-600'}`}>
{data.overallResult === '합격' ? '☑' : '☐'}
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,298 @@
"use client";
import React from 'react';
// 조인트바 중간검사 성적서 데이터 타입
export interface JointbarInspectionData {
documentNo: string;
productName: string;
specification: string;
client: string;
siteName: string;
lotNo: string;
lotSize: string;
inspectionDate: string;
inspector: string;
approvers: {
writer?: string;
reviewer?: string;
approver?: string;
};
// 중간검사 기준서 정보
standardInfo: {
appearance: { criteria: string; method: string; frequency: string; regulation: string };
assembly: { criteria: string; method: string; frequency: string; regulation: string };
coating: { criteria: string; method: string; frequency: string; regulation: string };
dimensions: { criteria: string; method: string; frequency: string; regulation: string };
};
// 중간검사 DATA
inspectionData: {
serialNo: string;
processState: '양호' | '불량';
assemblyState: '양호' | '불량';
height: { standard: number; measured: number };
height2: { standard: number; measured: number };
bandLength: { standard: number; measured: number };
gap: { standard: number; measured: number };
result: '적합' | '부적합';
}[];
notes: string;
overallResult: '합격' | '불합격';
}
// Mock 데이터
export const MOCK_JOINTBAR_INSPECTION: JointbarInspectionData = {
documentNo: 'KDQP-01-009',
productName: '조인트바',
specification: '와이어 클러치 크립지름',
client: '주일',
siteName: '용산고등학교(4호)',
lotNo: 'KD-WE-251015-01-(3)',
lotSize: '11 개소',
inspectionDate: '2025.',
inspector: '',
approvers: {
writer: '전진',
reviewer: '',
approver: '',
},
standardInfo: {
appearance: {
criteria: '사용상 해로운 결함이 없을 것',
method: '',
frequency: 'n = 1, c = 0',
regulation: 'KS F 4510 5.1항',
},
assembly: {
criteria: '밴드시트 읍동에 의해\n견고하게 조립되어야 함',
method: '확인점검',
frequency: '',
regulation: 'KS F 4510 9항',
},
coating: {
criteria: '용접부위에 락터스베이\n도포하여야 함',
method: '',
frequency: '',
regulation: '자체규정',
},
dimensions: {
criteria: '⓪\n16.5 ± 1\n14.5 ± 1\n300(밴드마감재) ± 4\n150 ± 4',
method: '체크검사',
frequency: '',
regulation: 'KS F 4510 7항\n외 9',
},
},
inspectionData: [
{ serialNo: '1', processState: '양호', assemblyState: '양호', height: { standard: 16.5, measured: 14.5 }, height2: { standard: 14.5, measured: 14.5 }, bandLength: { standard: 300, measured: 300 }, gap: { standard: 150, measured: 150 }, result: '적합' },
{ serialNo: '2', processState: '양호', assemblyState: '양호', height: { standard: 16.5, measured: 14.5 }, height2: { standard: 14.5, measured: 14.5 }, bandLength: { standard: 300, measured: 300 }, gap: { standard: 150, measured: 150 }, result: '적합' },
{ serialNo: '3', processState: '양호', assemblyState: '양호', height: { standard: 16.5, measured: 14.5 }, height2: { standard: 14.5, measured: 14.5 }, bandLength: { standard: 300, measured: 300 }, gap: { standard: 150, measured: 150 }, result: '적합' },
{ serialNo: '4', processState: '양호', assemblyState: '양호', height: { standard: 16.5, measured: 14.5 }, height2: { standard: 14.5, measured: 14.5 }, bandLength: { standard: 300, measured: 300 }, gap: { standard: 150, measured: 150 }, result: '적합' },
{ serialNo: '5', processState: '양호', assemblyState: '양호', height: { standard: 16.5, measured: 14.5 }, height2: { standard: 14.5, measured: 14.5 }, bandLength: { standard: 300, measured: 300 }, gap: { standard: 150, measured: 150 }, result: '적합' },
],
notes: '',
overallResult: '합격',
};
interface JointbarInspectionDocumentProps {
data?: JointbarInspectionData;
}
export const JointbarInspectionDocument = ({ data = MOCK_JOINTBAR_INSPECTION }: JointbarInspectionDocumentProps) => {
return (
<div className="bg-white p-8 w-full text-sm shadow-sm print:shadow-none">
{/* 헤더 */}
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-2">
<div className="text-2xl font-bold">KD</div>
<div className="text-xs text-gray-600"><br/>KYUNGDONG COMPANY</div>
</div>
<div className="text-center">
<div className="text-xl font-bold"></div>
<div className="text-xl font-bold tracking-[0.2rem]"> </div>
</div>
<table className="text-xs border-collapse">
<tbody>
<tr>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-8 text-center" rowSpan={3}>
<div className="flex flex-col items-center">
<span></span><span></span>
</div>
</td>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center"></td>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center"></td>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center"></td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 h-8 text-center font-medium">{data.approvers.writer}</td>
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.reviewer}</td>
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.approver}</td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">/</td>
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50"></td>
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50"></td>
</tr>
</tbody>
</table>
</div>
{/* 기본 정보 테이블 */}
<table className="w-full border-collapse mb-4 text-xs">
<tbody>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 w-20 font-medium"> </td>
<td className="border border-gray-400 px-3 py-2">{data.productName}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 w-24 font-medium"> LOT NO</td>
<td className="border border-gray-400 px-3 py-2">{data.lotNo}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"> </td>
<td className="border border-gray-400 px-3 py-2">{data.specification}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.lotSize}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.client}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.inspectionDate}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.siteName}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.inspector}</td>
</tr>
</tbody>
</table>
{/* 중간검사 기준서 */}
<table className="w-full border-collapse mb-4 text-xs">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-400 px-2 py-1 w-24" rowSpan={5}><br/></th>
<th className="border border-gray-400 px-2 py-1 w-16"></th>
<th className="border border-gray-400 px-2 py-1 w-16"></th>
<th className="border border-gray-400 px-2 py-1"></th>
<th className="border border-gray-400 px-2 py-1 w-14"></th>
<th className="border border-gray-400 px-2 py-1 w-14"></th>
<th className="border border-gray-400 px-2 py-1 w-24"></th>
</tr>
</thead>
<tbody>
<tr>
<td className="border border-gray-400 p-1 text-center align-middle" rowSpan={4}>
{/* 도해 이미지 영역 */}
<div className="w-14 h-20 mx-auto border border-gray-300 bg-gray-50 flex items-center justify-center">
<div className="text-center text-gray-400">
<div className="flex flex-col items-center gap-1">
<span className="w-3 h-3 border border-gray-400 rounded-full flex items-center justify-center text-[6px]">1</span>
<div className="flex items-center gap-1">
<div className="w-2 h-6 border border-gray-300"></div>
<span className="w-3 h-3 border border-gray-400 flex items-center justify-center text-[6px]"></span>
<div className="w-2 h-6 border border-gray-300"></div>
</div>
<span className="w-3 h-3 border border-gray-400 rounded-full flex items-center justify-center text-[6px]">2</span>
</div>
</div>
</div>
</td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1">{data.standardInfo.appearance.criteria}</td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 text-center">{data.standardInfo.appearance.frequency}</td>
<td className="border border-gray-400 px-1 py-1 text-center text-[9px]">{data.standardInfo.appearance.regulation}</td>
</tr>
<tr>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 whitespace-pre-line">{data.standardInfo.assembly.criteria}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{data.standardInfo.assembly.method}</td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 text-center text-[9px]">{data.standardInfo.assembly.regulation}</td>
</tr>
<tr>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 whitespace-pre-line">{data.standardInfo.coating.criteria}</td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 text-center text-[9px]">{data.standardInfo.coating.regulation}</td>
</tr>
<tr>
<td className="border border-gray-400 px-1 py-1 text-center"><br/>(mm)</td>
<td className="border border-gray-400 px-1 py-1 whitespace-pre-line">{data.standardInfo.dimensions.criteria}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{data.standardInfo.dimensions.method}</td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 text-center text-[9px] whitespace-pre-line">{data.standardInfo.dimensions.regulation}</td>
</tr>
</tbody>
</table>
{/* 중간검사 DATA */}
<div className="mb-2 text-xs font-medium"> DATA</div>
<table className="w-full border-collapse text-xs mb-4">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-400 px-1 py-1" rowSpan={2}><br/></th>
<th className="border border-gray-400 px-1 py-1" colSpan={2}></th>
<th className="border border-gray-400 px-1 py-1" colSpan={8}> [mm]</th>
<th className="border border-gray-400 px-1 py-1" rowSpan={2}></th>
</tr>
<tr className="bg-gray-100">
<th className="border border-gray-400 px-1 py-1"></th>
<th className="border border-gray-400 px-1 py-1"></th>
<th className="border border-gray-400 px-1 py-1"> <br/></th>
<th className="border border-gray-400 px-1 py-1"></th>
<th className="border border-gray-400 px-1 py-1"> <br/></th>
<th className="border border-gray-400 px-1 py-1"></th>
<th className="border border-gray-400 px-1 py-1"> ()<br/></th>
<th className="border border-gray-400 px-1 py-1"></th>
<th className="border border-gray-400 px-1 py-1"> <br/></th>
<th className="border border-gray-400 px-1 py-1"></th>
</tr>
</thead>
<tbody>
{data.inspectionData.map((item, index) => (
<tr key={index}>
<td className="border border-gray-400 px-1 py-1 text-center">{item.serialNo}</td>
<td className="border border-gray-400 px-1 py-1 text-center">
</td>
<td className="border border-gray-400 px-1 py-1 text-center">
</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.height.standard} ± 1</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.height.measured}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.height2.standard} ± 1</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.height2.measured}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.bandLength.standard} ± 4</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.bandLength.measured}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.gap.standard} ± 4</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.gap.measured}</td>
<td className="border border-gray-400 px-1 py-1 text-center">
<br/>
</td>
</tr>
))}
</tbody>
</table>
{/* 부적합 내용 */}
<div className="border border-gray-400 mb-4">
<div className="bg-gray-100 px-3 py-1 text-xs font-medium border-b border-gray-400"> </div>
<div className="px-3 py-2 text-xs text-gray-600 min-h-[30px]">{data.notes}</div>
</div>
{/* 문서번호 및 종합판정 */}
<div className="flex justify-between items-end">
<div className="text-xs text-gray-500">{data.documentNo}</div>
<div className="border border-gray-400">
<div className="bg-gray-100 px-4 py-1 text-center text-xs font-medium border-b border-gray-400"></div>
<div className={`px-8 py-2 text-center text-sm font-bold ${data.overallResult === '합격' ? 'text-blue-600' : 'text-red-600'}`}>
{data.overallResult}
</div>
</div>
<div className="text-xs text-gray-500">KDPS-10-03</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,289 @@
"use client";
import React from 'react';
// 제품검사 성적서 데이터 타입
export interface ProductInspectionData {
documentNo: string;
deliveryName: string;
productName: string;
client: string;
siteName: string;
lotNo: string;
lotSize: string;
inspectionDate: string;
inspector: string;
approvers: {
writer?: string;
reviewer?: string;
approver?: string;
};
productImage?: string;
inspectionItems: {
id: number;
name: string;
criteria: string;
method: string;
frequency: string;
result: '적합' | '부적합' | '';
}[];
notes: string;
overallResult: '합격' | '불합격';
}
// Mock 데이터
export const MOCK_PRODUCT_INSPECTION: ProductInspectionData = {
documentNo: 'KDQP-01-003',
deliveryName: '스크린 방화셔터',
productName: 'WY-SC780 방화셔터',
client: '삼성물산(주)',
siteName: '용산고등학교(4호)',
lotNo: 'KD-SA-251218-01',
lotSize: 'EA',
inspectionDate: '2023.',
inspector: '',
approvers: {
writer: '김검사',
reviewer: '',
approver: '박승인',
},
productImage: '',
inspectionItems: [
{
id: 1,
name: '가공상태',
criteria: '사용상 해로운 결함이 없을 것',
method: '',
frequency: '',
result: '적합',
},
{
id: 2,
name: '외관검사',
criteria: '내용물이 타 견고하게 결합되어야 한\n(찌그러지 않아야 함)',
method: '',
frequency: '',
result: '적합',
},
{
id: 3,
name: '절단면',
criteria: '밴드시트 전고무에 프라이머와\n젤리이어야 함',
method: '육안검사',
frequency: '',
result: '적합',
},
{
id: 4,
name: '도포상태',
criteria: '용접부위에 락터스베이\n도포하여야 함',
method: '',
frequency: '',
result: '적합',
},
{
id: 5,
name: '조립\n(W4-SC780 전제품 확인)',
criteria: '간섭부위 프라이머 락터스베이(케노x, 깔대기) W-80스크린',
method: '',
frequency: '',
result: '적합',
},
{
id: 6,
name: '슬릿',
criteria: '현장이동 ± 30mm',
method: '',
frequency: '',
result: '',
},
{
id: 7,
name: '규격치수\n(슬릿/가이드레일)',
criteria: '현장이동 ± 30mm\n10 ± 6mm(측정치), (총 높이 152 이상)',
method: '줄자/측정기',
frequency: '',
result: '',
},
{
id: 8,
name: '마감처리',
criteria: '가이드레일과 하단가이드를 높이 25mm이상 처리 있음\n20mm 균형케이스 화보완연 손',
method: '',
frequency: '',
result: '',
},
{
id: 9,
name: '내벽시트',
criteria: '프린팅\n용지 치 상부 및 완전보완시 작업하위',
method: '',
frequency: '',
result: '',
},
{
id: 10,
name: '마감시트',
criteria: '4-5cycle\n젤 두께 3 ~ 7cycle',
method: '',
frequency: '',
result: '',
},
{
id: 11,
name: '배색시트',
criteria: '2차 시트 길이 및 완전보완시 작업하위',
method: '',
frequency: '',
result: '',
},
],
notes: '1.내벽시트, 마감시트, 배색시트에 내용기재라, 시트 관리업무(업무인력)시 사용, 견적위함, □ 관련사항을 기록하며',
overallResult: '합격',
};
interface ProductInspectionDocumentProps {
data?: ProductInspectionData;
}
export const ProductInspectionDocument = ({ data = MOCK_PRODUCT_INSPECTION }: ProductInspectionDocumentProps) => {
return (
<div className="bg-white p-8 w-full text-sm shadow-sm print:shadow-none">
{/* 헤더 */}
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-2">
<div className="text-2xl font-bold">KD</div>
<div className="text-xs text-gray-600"><br/>KYUNGDONG COMPANY</div>
</div>
<div className="text-2xl font-bold tracking-[0.3rem]"> </div>
<table className="text-xs border-collapse">
<tbody>
<tr>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-8 text-center" rowSpan={3}>
<div className="flex flex-col items-center">
<span></span><span></span>
</div>
</td>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center"></td>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center"></td>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center"></td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.writer}</td>
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.reviewer}</td>
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.approver}</td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">/</td>
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50"></td>
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50"></td>
</tr>
</tbody>
</table>
</div>
{/* 기본 정보 테이블 */}
<table className="w-full border-collapse mb-4 text-xs">
<tbody>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 w-20 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.deliveryName}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 w-20 font-medium"> LOT NO</td>
<td className="border border-gray-400 px-3 py-2">{data.lotNo}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.productName}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.lotSize}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.client}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.inspectionDate}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.siteName}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.inspector}</td>
</tr>
</tbody>
</table>
{/* 제품사진 및 검사항목 */}
<div className="flex gap-4 mb-4">
{/* 제품사진 영역 */}
<div className="w-48 shrink-0">
<div className="border border-gray-400 h-36 flex items-center justify-center bg-gray-50">
{data.productImage ? (
<img src={data.productImage} alt="제품 사진" className="max-w-full max-h-full object-contain" />
) : (
<div className="text-center text-gray-400 text-xs">
<div className="mb-2"></div>
<div className="text-[10px]"> <br/>188 128 158 </div>
</div>
)}
</div>
</div>
{/* 검사기준 영역 */}
<div className="flex-1 text-xs text-gray-500">
<p>()</p>
<p></p>
</div>
</div>
{/* 검사 항목 테이블 */}
<table className="w-full border-collapse text-xs mb-4">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-400 px-2 py-2 w-10">No</th>
<th className="border border-gray-400 px-2 py-2 w-24"></th>
<th className="border border-gray-400 px-2 py-2"></th>
<th className="border border-gray-400 px-2 py-2 w-20"></th>
<th className="border border-gray-400 px-2 py-2 w-16"></th>
<th className="border border-gray-400 px-2 py-2 w-16"></th>
</tr>
</thead>
<tbody>
{data.inspectionItems.map((item) => (
<tr key={item.id}>
<td className="border border-gray-400 px-2 py-2 text-center">{item.id}</td>
<td className="border border-gray-400 px-2 py-2 text-center whitespace-pre-line">{item.name}</td>
<td className="border border-gray-400 px-2 py-2 whitespace-pre-line">{item.criteria}</td>
<td className="border border-gray-400 px-2 py-2 text-center">{item.method}</td>
<td className="border border-gray-400 px-2 py-2 text-center">{item.frequency}</td>
<td className="border border-gray-400 px-2 py-2 text-center">
{item.result && (
<span className={item.result === '적합' ? 'text-blue-600' : 'text-red-600'}>
{item.result === '적합' ? '적합' : '부적합'} {item.result === '적합' ? '부적합' : '적합'}
</span>
)}
</td>
</tr>
))}
</tbody>
</table>
{/* 특기사항 */}
<div className="border border-gray-400 mb-4">
<div className="bg-gray-100 px-3 py-1 text-xs font-medium border-b border-gray-400"></div>
<div className="px-3 py-2 text-xs text-gray-600 min-h-[40px]">{data.notes}</div>
</div>
{/* 문서번호 및 종합판정 */}
<div className="flex justify-between items-end">
<div className="text-xs text-gray-500">{data.documentNo}</div>
<div className="border border-gray-400">
<div className="bg-gray-100 px-4 py-1 text-center text-xs font-medium border-b border-gray-400"></div>
<div className={`px-8 py-2 text-center text-sm font-bold ${data.overallResult === '합격' ? 'text-blue-600' : 'text-red-600'}`}>
{data.overallResult}
</div>
</div>
<div className="text-xs text-gray-500"></div>
</div>
</div>
);
};

View File

@@ -0,0 +1,322 @@
'use client';
import React, { useState, useRef, useCallback } from 'react';
import { Upload, FileText, Download, Trash2, Eye, RefreshCw, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
export interface QualityDocumentFile {
id?: number;
name: string;
url: string;
size?: number;
uploadedAt?: string;
}
interface QualityDocumentUploaderProps {
/** 기존 업로드된 파일 정보 */
existingFile?: QualityDocumentFile | null;
/** 파일 업로드 콜백 */
onFileUpload: (file: File) => void;
/** 파일 삭제 콜백 */
onFileDelete?: () => void;
/** 파일 다운로드 콜백 */
onFileDownload?: (file: QualityDocumentFile) => void;
/** 비활성화 여부 */
disabled?: boolean;
/** 최대 파일 크기 (MB) */
maxSize?: number;
}
/**
* 품질관리서 PDF 업로드/뷰어 컴포넌트
*
* - PDF 파일 업로드 (드래그 앤 드롭 / 클릭)
* - PDF 미리보기 (iframe)
* - 다운로드 / 삭제 / 교체 기능
*/
export function QualityDocumentUploader({
existingFile,
onFileUpload,
onFileDelete,
onFileDownload,
disabled = false,
maxSize = 20, // 20MB default
}: QualityDocumentUploaderProps) {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// 파일 유효성 검증
const validateFile = (file: File): string | null => {
// PDF 파일만 허용
if (file.type !== 'application/pdf') {
return 'PDF 파일만 업로드 가능합니다.';
}
// 파일 크기 검증
const fileSizeMB = file.size / (1024 * 1024);
if (fileSizeMB > maxSize) {
return `파일 크기는 ${maxSize}MB 이하여야 합니다.`;
}
return null;
};
// 파일 선택 처리
const handleFileSelect = useCallback((file: File) => {
const validationError = validateFile(file);
if (validationError) {
setError(validationError);
return;
}
setError(null);
setSelectedFile(file);
// Blob URL 생성하여 미리보기
const url = URL.createObjectURL(file);
setPreviewUrl(url);
// 부모 컴포넌트에 알림
onFileUpload(file);
}, [maxSize, onFileUpload]);
// Input 변경 처리
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
handleFileSelect(file);
}
};
// 드래그 앤 드롭 이벤트
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (!disabled) setIsDragging(true);
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
if (disabled) return;
const file = e.dataTransfer.files?.[0];
if (file) {
handleFileSelect(file);
}
};
// 파일 선택 버튼 클릭
const handleClick = () => {
if (!disabled) {
fileInputRef.current?.click();
}
};
// 파일 삭제
const handleDelete = () => {
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
}
setSelectedFile(null);
setPreviewUrl(null);
setError(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
onFileDelete?.();
};
// 파일 다운로드
const handleDownload = () => {
if (existingFile) {
onFileDownload?.(existingFile);
} else if (selectedFile && previewUrl) {
// 로컬 파일 다운로드
const a = document.createElement('a');
a.href = previewUrl;
a.download = selectedFile.name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
};
// 미리보기 토글
const togglePreview = () => {
setShowPreview(!showPreview);
};
// 현재 표시할 파일 정보
const currentFile = selectedFile || existingFile;
const currentPreviewUrl = previewUrl || existingFile?.url;
// 파일 크기 포맷
const formatFileSize = (bytes?: number) => {
if (!bytes) return '';
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
return (
<div className="w-full h-full flex flex-col">
{/* 숨김 파일 입력 */}
<input
ref={fileInputRef}
type="file"
accept=".pdf,application/pdf"
onChange={handleInputChange}
disabled={disabled}
className="hidden"
/>
{currentFile ? (
// 파일이 있는 경우: 정보 표시 + 미리보기
<div className="flex flex-col h-full">
{/* 파일 정보 헤더 */}
<div className="bg-white border-b p-4 flex items-center justify-between shrink-0">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center">
<FileText className="w-5 h-5 text-red-600" />
</div>
<div>
<p className="font-medium text-sm text-gray-800">
{selectedFile?.name || existingFile?.name}
</p>
<p className="text-xs text-gray-500">
{formatFileSize(selectedFile?.size || existingFile?.size)}
{existingFile?.uploadedAt && `${existingFile.uploadedAt}`}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={togglePreview}
className="gap-1.5"
>
<Eye className="w-4 h-4" />
{showPreview ? '미리보기 닫기' : '미리보기'}
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleDownload}
className="gap-1.5"
>
<Download className="w-4 h-4" />
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleClick}
disabled={disabled}
className="gap-1.5"
>
<RefreshCw className="w-4 h-4" />
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleDelete}
disabled={disabled}
className="gap-1.5 text-red-600 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
{/* PDF 미리보기 영역 */}
{showPreview && currentPreviewUrl ? (
<div className="flex-1 bg-gray-100 p-4">
<iframe
src={currentPreviewUrl}
className="w-full h-full border-0 rounded-lg shadow-sm bg-white"
title="PDF Preview"
/>
</div>
) : (
<div className="flex-1 flex items-center justify-center bg-gray-50">
<div className="text-center">
<FileText className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500 text-sm"> PDF를 </p>
</div>
</div>
)}
</div>
) : (
// 파일이 없는 경우: 업로드 영역
<div
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
onClick={handleClick}
className={`
flex-1 border-2 border-dashed rounded-lg
flex flex-col items-center justify-center
cursor-pointer transition-all duration-200
${isDragging
? 'border-purple-500 bg-purple-50'
: 'border-gray-300 hover:border-purple-400 hover:bg-gray-50'
}
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
`}
>
<div className="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mb-4">
<Upload className="w-8 h-8 text-purple-600" />
</div>
<h3 className="text-lg font-medium text-gray-800 mb-2">
PDF
</h3>
<p className="text-sm text-gray-500 mb-1">
</p>
<p className="text-xs text-gray-400">
PDF {maxSize}MB
</p>
{error && (
<div className="mt-4 px-4 py-2 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-600">{error}</p>
</div>
)}
</div>
)}
</div>
);
}
// Mock 데이터 (테스트용)
export const MOCK_QUALITY_DOCUMENT: QualityDocumentFile = {
id: 1,
name: '품질관리서_KD-SS-2024-530.pdf',
url: '/sample-quality-document.pdf',
size: 2456789,
uploadedAt: '2024-10-10',
};

View File

@@ -0,0 +1,309 @@
"use client";
import React from 'react';
// 스크린 중간검사 성적서 데이터 타입
export interface ScreenInspectionData {
documentNo: string;
productName: string;
specification: string;
client: string;
siteName: string;
lotNo: string;
lotSize: string;
inspectionDate: string;
inspector: string;
approvers: {
writer?: string;
reviewer?: string;
approver?: string;
};
// 중간검사 기준서 정보
standardInfo: {
appearance: { criteria: string; method: string; frequency: string; regulation: string };
assembly: { criteria: string; method: string; frequency: string; regulation: string };
dimensions: { criteria: string; method: string; frequency: string; regulation: string };
coating: { criteria: string; method: string; frequency: string; regulation: string };
};
// 중간검사 DATA
inspectionData: {
serialNo: string;
processState: '양호' | '불량';
materialState: '양호' | '불량';
assemblyState: '양호' | '불량';
height: { standard: number; measured: number };
width: { standard: number; measured: number };
checkCount: string;
checkResult: 'OK' | 'NG';
result: '적합' | '부적합';
}[];
notes: string;
overallResult: '합격' | '불합격';
}
// Mock 데이터
export const MOCK_SCREEN_INSPECTION: ScreenInspectionData = {
documentNo: 'KDQP-01-006',
productName: '스크린',
specification: '와이어 클러치 크립지름',
client: '주일',
siteName: '용산고등학교(4호)',
lotNo: 'KD-WE-251015-01-(3)',
lotSize: '11 개소',
inspectionDate: '2025.',
inspector: '',
approvers: {
writer: '전진',
reviewer: '',
approver: '',
},
standardInfo: {
appearance: {
criteria: '사용상 해로운 결함이 없을 것',
method: '',
frequency: '',
regulation: 'KS F 4510 5.1항',
},
assembly: {
criteria: '밴드시트 읍동에 의해\n견고하게 조립되어야 함',
method: '육안검사',
frequency: 'n = L, c = 0',
regulation: 'KS F 4510 7항\n유지 건설규',
},
dimensions: {
criteria: '도면대로 + 레벨상황',
method: '',
frequency: '',
regulation: '',
},
coating: {
criteria: '400 이하',
method: 'GO/NO\nGAGE',
frequency: '',
regulation: '',
},
},
inspectionData: [
{ serialNo: '01', processState: '양호', materialState: '양호', assemblyState: '양호', height: { standard: 7400, measured: 2950 }, width: { standard: 2950, measured: 2950 }, checkCount: '400 이하', checkResult: 'OK', result: '적합' },
{ serialNo: '02', processState: '양호', materialState: '양호', assemblyState: '양호', height: { standard: 4700, measured: 2950 }, width: { standard: 2950, measured: 2950 }, checkCount: '400 이하', checkResult: 'OK', result: '적합' },
{ serialNo: '03', processState: '양호', materialState: '양호', assemblyState: '양호', height: { standard: 6790, measured: 2950 }, width: { standard: 2950, measured: 2950 }, checkCount: '400 이하', checkResult: 'OK', result: '적합' },
{ serialNo: '04', processState: '양호', materialState: '양호', assemblyState: '양호', height: { standard: 3700, measured: 2950 }, width: { standard: 2950, measured: 2950 }, checkCount: '400 이하', checkResult: 'OK', result: '적합' },
{ serialNo: '05', processState: '양호', materialState: '양호', assemblyState: '양호', height: { standard: 6000, measured: 2950 }, width: { standard: 2950, measured: 2950 }, checkCount: '400 이하', checkResult: 'OK', result: '적합' },
{ serialNo: '06', processState: '양호', materialState: '양호', assemblyState: '양호', height: { standard: 7300, measured: 2950 }, width: { standard: 2950, measured: 2950 }, checkCount: '400 이하', checkResult: 'OK', result: '적합' },
{ serialNo: '07', processState: '양호', materialState: '양호', assemblyState: '양호', height: { standard: 3700, measured: 2950 }, width: { standard: 2950, measured: 2950 }, checkCount: '400 이하', checkResult: 'OK', result: '적합' },
],
notes: '',
overallResult: '합격',
};
interface ScreenInspectionDocumentProps {
data?: ScreenInspectionData;
}
export const ScreenInspectionDocument = ({ data = MOCK_SCREEN_INSPECTION }: ScreenInspectionDocumentProps) => {
return (
<div className="bg-white p-8 w-full text-sm shadow-sm print:shadow-none">
{/* 헤더 */}
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-2">
<div className="text-2xl font-bold">KD</div>
<div className="text-xs text-gray-600"><br/>KYUNGDONG COMPANY</div>
</div>
<div className="text-center">
<div className="text-xl font-bold"></div>
<div className="text-xl font-bold tracking-[0.2rem]"> </div>
</div>
<table className="text-xs border-collapse">
<tbody>
<tr>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-8 text-center" rowSpan={3}>
<div className="flex flex-col items-center">
<span></span><span></span>
</div>
</td>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center"></td>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center"></td>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center"></td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 h-8 text-center font-medium">{data.approvers.writer}</td>
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.reviewer}</td>
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.approver}</td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">/</td>
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50"></td>
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50"></td>
</tr>
</tbody>
</table>
</div>
{/* 기본 정보 테이블 */}
<table className="w-full border-collapse mb-4 text-xs">
<tbody>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 w-20 font-medium"> </td>
<td className="border border-gray-400 px-3 py-2">{data.productName}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 w-24 font-medium"> LOT NO</td>
<td className="border border-gray-400 px-3 py-2">{data.lotNo}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"> </td>
<td className="border border-gray-400 px-3 py-2">{data.specification}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.lotSize}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.client}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.inspectionDate}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.siteName}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.inspector}</td>
</tr>
</tbody>
</table>
{/* 중간검사 기준서 */}
<table className="w-full border-collapse mb-4 text-xs">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-400 px-2 py-2 w-24" rowSpan={2}><br/></th>
<th className="border border-gray-400 px-2 py-2 w-12"></th>
<th className="border border-gray-400 px-2 py-2 w-20"></th>
<th className="border border-gray-400 px-2 py-2"></th>
<th className="border border-gray-400 px-2 py-2 w-16"></th>
<th className="border border-gray-400 px-2 py-2 w-16"></th>
<th className="border border-gray-400 px-2 py-2 w-24"></th>
</tr>
</thead>
<tbody>
{/* 도해 영역 - 첫 번째 행 */}
<tr>
<td className="border border-gray-400 p-2 text-center align-middle" rowSpan={4}>
{/* 도해 이미지 영역 */}
<div className="w-20 h-28 mx-auto border border-gray-300 bg-gray-50 flex items-center justify-center">
<div className="text-center text-gray-400">
<div className="flex flex-col items-center gap-1">
<div className="flex items-center gap-2">
<span className="w-4 h-4 border border-gray-400 rounded-full flex items-center justify-center text-[8px]">1</span>
</div>
<div className="w-12 h-1 bg-gray-300"></div>
<div className="flex items-center gap-2">
<span className="w-4 h-4 border border-gray-400 rounded-full flex items-center justify-center text-[8px]">2</span>
</div>
<div className="w-12 h-8 border border-gray-300"></div>
<div className="flex items-center gap-2">
<span className="w-4 h-4 border border-gray-400 rounded-full flex items-center justify-center text-[8px]">3</span>
</div>
</div>
</div>
</div>
</td>
<td className="border border-gray-400 px-2 py-2 text-center"></td>
<td className="border border-gray-400 px-2 py-2 text-center"></td>
<td className="border border-gray-400 px-2 py-2">{data.standardInfo.appearance.criteria}</td>
<td className="border border-gray-400 px-2 py-2 text-center"></td>
<td className="border border-gray-400 px-2 py-2 text-center"></td>
<td className="border border-gray-400 px-2 py-2 text-center text-[10px]">{data.standardInfo.appearance.regulation}</td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-2 text-center"></td>
<td className="border border-gray-400 px-2 py-2 text-center"></td>
<td className="border border-gray-400 px-2 py-2 whitespace-pre-line">{data.standardInfo.assembly.criteria}</td>
<td className="border border-gray-400 px-2 py-2 text-center">{data.standardInfo.assembly.method}</td>
<td className="border border-gray-400 px-2 py-2 text-center whitespace-pre-line">{data.standardInfo.assembly.frequency}</td>
<td className="border border-gray-400 px-2 py-2 text-center text-[10px] whitespace-pre-line">{data.standardInfo.assembly.regulation}</td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-2 text-center"></td>
<td className="border border-gray-400 px-2 py-2 text-center"> + </td>
<td className="border border-gray-400 px-2 py-2">{data.standardInfo.dimensions.criteria}</td>
<td className="border border-gray-400 px-2 py-2 text-center"></td>
<td className="border border-gray-400 px-2 py-2 text-center"></td>
<td className="border border-gray-400 px-2 py-2 text-center"></td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-2 text-center"></td>
<td className="border border-gray-400 px-2 py-2 text-center"></td>
<td className="border border-gray-400 px-2 py-2">{data.standardInfo.coating.criteria}</td>
<td className="border border-gray-400 px-2 py-2 text-center whitespace-pre-line">{data.standardInfo.coating.method}</td>
<td className="border border-gray-400 px-2 py-2 text-center"></td>
<td className="border border-gray-400 px-2 py-2 text-center"></td>
</tr>
</tbody>
</table>
{/* 중간검사 DATA */}
<div className="mb-2 text-xs font-medium"> DATA</div>
<table className="w-full border-collapse text-xs mb-4">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-400 px-1 py-1" rowSpan={2}><br/></th>
<th className="border border-gray-400 px-1 py-1 w-12" colSpan={1}></th>
<th className="border border-gray-400 px-1 py-1" colSpan={4}> [mm]</th>
<th className="border border-gray-400 px-1 py-1 w-12" colSpan={2}> </th>
<th className="border border-gray-400 px-1 py-1 w-14" rowSpan={2}></th>
</tr>
<tr className="bg-gray-100">
<th className="border border-gray-400 px-1 py-1"></th>
<th className="border border-gray-400 px-1 py-1"></th>
<th className="border border-gray-400 px-1 py-1"></th>
<th className="border border-gray-400 px-1 py-1"> </th>
<th className="border border-gray-400 px-1 py-1"> </th>
<th className="border border-gray-400 px-1 py-1"></th>
<th className="border border-gray-400 px-1 py-1"> </th>
</tr>
</thead>
<tbody>
{data.inspectionData.map((item, index) => (
<tr key={index}>
<td className="border border-gray-400 px-1 py-1 text-center">{item.serialNo}</td>
<td className="border border-gray-400 px-1 py-1 text-center">
</td>
<td className="border border-gray-400 px-1 py-1 text-center">
</td>
<td className="border border-gray-400 px-1 py-1 text-center">
</td>
<td className="border border-gray-400 px-1 py-1 text-center font-medium">{item.height.standard.toLocaleString()}</td>
<td className="border border-gray-400 px-1 py-1 text-center font-medium">{item.width.standard.toLocaleString()}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.checkCount}</td>
<td className="border border-gray-400 px-1 py-1 text-center">
OK NG
</td>
<td className="border border-gray-400 px-1 py-1 text-center">
</td>
</tr>
))}
</tbody>
</table>
{/* 부적합 내용 */}
<div className="border border-gray-400 mb-4">
<div className="bg-gray-100 px-3 py-1 text-xs font-medium border-b border-gray-400"> </div>
<div className="px-3 py-2 text-xs text-gray-600 min-h-[30px]">{data.notes}</div>
</div>
{/* 문서번호 및 종합판정 */}
<div className="flex justify-between items-end">
<div className="text-xs text-gray-500">{data.documentNo}</div>
<div className="border border-gray-400">
<div className="bg-gray-100 px-4 py-1 text-center text-xs font-medium border-b border-gray-400"></div>
<div className={`px-8 py-2 text-center text-sm font-bold ${data.overallResult === '합격' ? 'text-blue-600' : 'text-red-600'}`}>
{data.overallResult}
</div>
</div>
<div className="text-xs text-gray-500">KDPS-03-01</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,286 @@
"use client";
import React from 'react';
// 슬랫 중간검사 성적서 데이터 타입
export interface SlatInspectionData {
documentNo: string;
productName: string;
specification: string;
client: string;
siteName: string;
lotNo: string;
lotSize: string;
inspectionDate: string;
inspector: string;
approvers: {
writer?: string;
reviewer?: string;
approver?: string;
};
// 중간검사 기준서 정보
standardInfo: {
appearance: { criteria: string; method: string; frequency: string; regulation: string };
assembly: { criteria: string; method: string; frequency: string; regulation: string };
dimensions: { criteria: string; method: string; frequency: string; regulation: string };
inspection: { criteria: string; method: string; frequency: string; regulation: string };
};
// 중간검사 DATA
inspectionData: {
serialNo: string;
processState: '양호' | '불량';
assemblyState: '양호' | '불량';
height1: { standard: number; measured: number };
height2: { standard: number; measured: number };
bandLength: { conductance: string; measured: number };
result: '적합' | '부적합';
}[];
notes: string;
overallResult: '합격' | '불합격';
}
// Mock 데이터
export const MOCK_SLAT_INSPECTION: SlatInspectionData = {
documentNo: 'KDQP-01-008',
productName: '슬랫',
specification: '와이어 클러치 크립지름',
client: '주일',
siteName: '용산고등학교(4호)',
lotNo: 'KD-WE-251015-01-(3)',
lotSize: '11 개소',
inspectionDate: '2025.',
inspector: '',
approvers: {
writer: '전진',
reviewer: '',
approver: '',
},
standardInfo: {
appearance: {
criteria: '사용상 해로운 결함이 없을 것',
method: '',
frequency: 'n = 1, c = 0',
regulation: 'KS F 4510 5.1항',
},
assembly: {
criteria: '밴드시트 읍동에 의해\n견고하게 조립되어야 함',
method: '확인점검',
frequency: '',
regulation: 'KS F 4510 9항',
},
dimensions: {
criteria: '용접부위에 락터스베이\n도포하여야 함',
method: '',
frequency: '',
regulation: '자체규정',
},
inspection: {
criteria: '16.5 ± 1\n14.5 ± 1\n도전차수+(밴드마감재) ± 4',
method: '체크검사',
frequency: '',
regulation: 'KS F 4510 7항\n외 9',
},
},
inspectionData: [
{ serialNo: '01', processState: '양호', assemblyState: '양호', height1: { standard: 16.5, measured: 14.5 }, height2: { standard: 14.5, measured: 14.5 }, bandLength: { conductance: '4,510', measured: 4510 }, result: '적합' },
{ serialNo: '02', processState: '양호', assemblyState: '양호', height1: { standard: 16.5, measured: 14.5 }, height2: { standard: 14.5, measured: 14.5 }, bandLength: { conductance: '', measured: 0 }, result: '적합' },
{ serialNo: '03', processState: '양호', assemblyState: '양호', height1: { standard: 16.5, measured: 14.5 }, height2: { standard: 14.5, measured: 14.5 }, bandLength: { conductance: '', measured: 0 }, result: '적합' },
{ serialNo: '04', processState: '양호', assemblyState: '양호', height1: { standard: 16.5, measured: 14.5 }, height2: { standard: 14.5, measured: 14.5 }, bandLength: { conductance: '', measured: 0 }, result: '적합' },
{ serialNo: '05', processState: '양호', assemblyState: '양호', height1: { standard: 16.5, measured: 14.5 }, height2: { standard: 14.5, measured: 14.5 }, bandLength: { conductance: '', measured: 0 }, result: '적합' },
],
notes: '',
overallResult: '합격',
};
interface SlatInspectionDocumentProps {
data?: SlatInspectionData;
}
export const SlatInspectionDocument = ({ data = MOCK_SLAT_INSPECTION }: SlatInspectionDocumentProps) => {
return (
<div className="bg-white p-8 w-full text-sm shadow-sm print:shadow-none">
{/* 헤더 */}
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-2">
<div className="text-2xl font-bold">KD</div>
<div className="text-xs text-gray-600"><br/>KYUNGDONG COMPANY</div>
</div>
<div className="text-center">
<div className="text-xl font-bold"></div>
<div className="text-xl font-bold tracking-[0.2rem]"> </div>
</div>
<table className="text-xs border-collapse">
<tbody>
<tr>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-8 text-center" rowSpan={3}>
<div className="flex flex-col items-center">
<span></span><span></span>
</div>
</td>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center"></td>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center"></td>
<td className="border border-gray-400 px-2 py-1 bg-gray-100 w-14 text-center"></td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 h-8 text-center font-medium">{data.approvers.writer}</td>
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.reviewer}</td>
<td className="border border-gray-400 px-2 py-1 h-8 text-center">{data.approvers.approver}</td>
</tr>
<tr>
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50">/</td>
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50"></td>
<td className="border border-gray-400 px-2 py-1 text-center bg-gray-50"></td>
</tr>
</tbody>
</table>
</div>
{/* 기본 정보 테이블 */}
<table className="w-full border-collapse mb-4 text-xs">
<tbody>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 w-20 font-medium"> </td>
<td className="border border-gray-400 px-3 py-2">{data.productName}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 w-24 font-medium"> LOT NO</td>
<td className="border border-gray-400 px-3 py-2">{data.lotNo}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"> </td>
<td className="border border-gray-400 px-3 py-2">{data.specification}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.lotSize}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.client}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.inspectionDate}</td>
</tr>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.siteName}</td>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{data.inspector}</td>
</tr>
</tbody>
</table>
{/* 중간검사 기준서 */}
<table className="w-full border-collapse mb-4 text-xs">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-400 px-2 py-1 w-24" rowSpan={5}><br/></th>
<th className="border border-gray-400 px-2 py-1 w-16"></th>
<th className="border border-gray-400 px-2 py-1 w-16"></th>
<th className="border border-gray-400 px-2 py-1"></th>
<th className="border border-gray-400 px-2 py-1 w-14"></th>
<th className="border border-gray-400 px-2 py-1 w-14"></th>
<th className="border border-gray-400 px-2 py-1 w-24"></th>
</tr>
</thead>
<tbody>
<tr>
<td className="border border-gray-400 p-1 text-center align-middle" rowSpan={4}>
{/* 도해 이미지 영역 */}
<div className="w-14 h-20 mx-auto border border-gray-300 bg-gray-50 flex items-center justify-center">
<div className="text-center text-gray-400">
<div className="flex flex-col items-center gap-1">
<span className="w-3 h-3 border border-gray-400 rounded-full flex items-center justify-center text-[6px]">1</span>
<div className="w-8 h-1 bg-gray-300"></div>
<span className="w-3 h-3 border border-gray-400 rounded-full flex items-center justify-center text-[6px]">2</span>
<div className="w-8 h-6 border border-gray-300"></div>
</div>
</div>
</div>
</td>
<td className="border border-gray-400 px-1 py-1 text-center"><br/>()</td>
<td className="border border-gray-400 px-1 py-1">{data.standardInfo.appearance.criteria}</td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 text-center">{data.standardInfo.appearance.frequency}</td>
<td className="border border-gray-400 px-1 py-1 text-center text-[9px]">{data.standardInfo.appearance.regulation}</td>
</tr>
<tr>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 whitespace-pre-line">{data.standardInfo.assembly.criteria}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{data.standardInfo.assembly.method}</td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 text-center text-[9px]">{data.standardInfo.assembly.regulation}</td>
</tr>
<tr>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 whitespace-pre-line">{data.standardInfo.dimensions.criteria}</td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 text-center text-[9px]">{data.standardInfo.dimensions.regulation}</td>
</tr>
<tr>
<td className="border border-gray-400 px-1 py-1 text-center"><br/>(mm)</td>
<td className="border border-gray-400 px-1 py-1 whitespace-pre-line">{data.standardInfo.inspection.criteria}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{data.standardInfo.inspection.method}</td>
<td className="border border-gray-400 px-1 py-1 text-center"></td>
<td className="border border-gray-400 px-1 py-1 text-center text-[9px] whitespace-pre-line">{data.standardInfo.inspection.regulation}</td>
</tr>
</tbody>
</table>
{/* 중간검사 DATA */}
<div className="mb-2 text-xs font-medium"> DATA</div>
<table className="w-full border-collapse text-xs mb-4">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-400 px-1 py-1" rowSpan={2}><br/></th>
<th className="border border-gray-400 px-1 py-1" colSpan={2}></th>
<th className="border border-gray-400 px-1 py-1" colSpan={4}> [mm]</th>
<th className="border border-gray-400 px-1 py-1" rowSpan={2}></th>
</tr>
<tr className="bg-gray-100">
<th className="border border-gray-400 px-1 py-1"></th>
<th className="border border-gray-400 px-1 py-1"></th>
<th className="border border-gray-400 px-1 py-1"> <br/></th>
<th className="border border-gray-400 px-1 py-1"></th>
<th className="border border-gray-400 px-1 py-1"> ()<br/></th>
<th className="border border-gray-400 px-1 py-1"></th>
</tr>
</thead>
<tbody>
{data.inspectionData.map((item, index) => (
<tr key={index}>
<td className="border border-gray-400 px-1 py-1 text-center">{item.serialNo}</td>
<td className="border border-gray-400 px-1 py-1 text-center">
</td>
<td className="border border-gray-400 px-1 py-1 text-center">
</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.height1.standard} ± 1</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.height1.measured}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.bandLength.conductance}</td>
<td className="border border-gray-400 px-1 py-1 text-center">{item.bandLength.measured > 0 ? item.bandLength.measured.toLocaleString() : ''}</td>
<td className="border border-gray-400 px-1 py-1 text-center">
<br/>
</td>
</tr>
))}
</tbody>
</table>
{/* 부적합 내용 */}
<div className="border border-gray-400 mb-4">
<div className="bg-gray-100 px-3 py-1 text-xs font-medium border-b border-gray-400"> </div>
<div className="px-3 py-2 text-xs text-gray-600 min-h-[30px]">{data.notes}</div>
</div>
{/* 문서번호 및 종합판정 */}
<div className="flex justify-between items-end">
<div className="text-xs text-gray-500">{data.documentNo}</div>
<div className="border border-gray-400">
<div className="bg-gray-100 px-4 py-1 text-center text-xs font-medium border-b border-gray-400"></div>
<div className={`px-8 py-2 text-center text-sm font-bold ${data.overallResult === '합격' ? 'text-blue-600' : 'text-red-600'}`}>
{data.overallResult}
</div>
</div>
<div className="text-xs text-gray-500">KDPS-10-02</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,29 @@
// 품질인정심사 문서 컴포넌트 exports
// 수입검사 성적서
export { ImportInspectionDocument, MOCK_IMPORT_INSPECTION } from './ImportInspectionDocument';
export type { ImportInspectionData } from './ImportInspectionDocument';
// 제품검사 성적서
export { ProductInspectionDocument, MOCK_PRODUCT_INSPECTION } from './ProductInspectionDocument';
export type { ProductInspectionData } from './ProductInspectionDocument';
// 스크린 중간검사 성적서
export { ScreenInspectionDocument, MOCK_SCREEN_INSPECTION } from './ScreenInspectionDocument';
export type { ScreenInspectionData } from './ScreenInspectionDocument';
// 절곡품 중간검사 성적서
export { BendingInspectionDocument, MOCK_BENDING_INSPECTION } from './BendingInspectionDocument';
export type { BendingInspectionData } from './BendingInspectionDocument';
// 슬랫 중간검사 성적서
export { SlatInspectionDocument, MOCK_SLAT_INSPECTION } from './SlatInspectionDocument';
export type { SlatInspectionData } from './SlatInspectionDocument';
// 조인트바 중간검사 성적서
export { JointbarInspectionDocument, MOCK_JOINTBAR_INSPECTION } from './JointbarInspectionDocument';
export type { JointbarInspectionData } from './JointbarInspectionDocument';
// 품질관리서 PDF 업로더
export { QualityDocumentUploader, MOCK_QUALITY_DOCUMENT } from './QualityDocumentUploader';
export type { QualityDocumentFile } from './QualityDocumentUploader';

View File

@@ -0,0 +1,339 @@
import { InspectionReport, RouteItem, Document } from './types';
import type { WorkOrder } from '@/components/production/ProductionDashboard/types';
import type { ShipmentDetail } from '@/components/outbound/ShipmentManagement/types';
// 품질검사용 수주서 아이템 타입 (자체 정의)
interface QualityOrderItem {
id: string;
name: string;
specification: string;
unit: string;
quantity: number;
unitPrice?: number;
amount?: number;
}
// 품질검사용 수주서 데이터 타입 (자체 정의)
export interface QualityOrderData {
lotNumber: string;
orderDate: string;
client: string;
siteName: string;
manager: string;
managerContact: string;
deliveryRequestDate: string;
expectedShipDate: string;
deliveryMethod: string;
address: string;
items: QualityOrderItem[];
subtotal: number;
discountRate: number;
totalAmount: number;
remarks?: string;
}
// 수주서 샘플 데이터
export const MOCK_ORDER_DATA: QualityOrderData = {
lotNumber: 'KD-SS-240924-19',
orderDate: '2024-09-24',
client: '삼성물산(주)',
siteName: '강남 아파트 단지',
manager: '김담당',
managerContact: '010-1234-5678',
deliveryRequestDate: '2024-10-05',
expectedShipDate: '2024-10-04',
deliveryMethod: '직접배차',
address: '서울시 강남구 테헤란로 123',
items: [
{ id: '1', name: '스크린 셔터 (표준형)', specification: '3000×2500', unit: 'SET', quantity: 5, unitPrice: 1200000, amount: 6000000 },
{ id: '2', name: '스크린 셔터 (방화형)', specification: '3000×2500', unit: 'SET', quantity: 3, unitPrice: 1500000, amount: 4500000 },
{ id: '3', name: '슬랫 패널', specification: '1000×500', unit: 'EA', quantity: 20, unitPrice: 50000, amount: 1000000 },
],
subtotal: 11500000,
discountRate: 5,
totalAmount: 10925000,
remarks: '납기일 엄수 요청',
};
// 작업일지 샘플 데이터
export const MOCK_WORK_ORDER: WorkOrder = {
id: 'wo-1',
orderNo: 'KD-WO-240924-01',
productName: '스크린 셔터 (표준형)',
process: 'screen',
client: '삼성물산(주)',
projectName: '강남 아파트 단지',
assignees: ['김작업', '이생산'],
quantity: 5,
dueDate: '2024-10-05',
priority: 1,
status: 'inProgress',
isUrgent: false,
isDelayed: false,
instruction: '품질 검수 철저히 진행',
createdAt: '2024-09-20',
};
// 출하 상세 샘플 데이터 (납품확인서, 출고증용)
export const MOCK_SHIPMENT_DETAIL: ShipmentDetail = {
id: 'ship-1',
shipmentNo: 'SHP-2024-530',
lotNo: 'KD-SS-240924-19',
scheduledDate: '2024-10-04',
status: 'completed',
priority: 'normal',
deliveryMethod: 'direct',
depositConfirmed: true,
invoiceIssued: true,
customerGrade: 'A',
canShip: true,
loadingManager: '박상차',
registrant: '이등록',
customerName: '삼성물산(주)',
siteName: '강남 아파트 단지',
deliveryAddress: '서울시 강남구 테헤란로 123',
receiver: '김인수',
receiverContact: '010-9876-5432',
products: [
{ id: 'p1', no: 1, itemCode: 'SS-001', itemName: '스크린 셔터 (표준형)', floorUnit: '101동/5F', specification: '3000×2500', quantity: 2, lotNo: 'KD-SS-240924-19-01' },
{ id: 'p2', no: 2, itemCode: 'SS-001', itemName: '스크린 셔터 (표준형)', floorUnit: '101동/6F', specification: '3000×2500', quantity: 3, lotNo: 'KD-SS-240924-19-02' },
{ id: 'p3', no: 3, itemCode: 'SS-002', itemName: '스크린 셔터 (방화형)', floorUnit: '102동/5F', specification: '3000×2500', quantity: 3, lotNo: 'KD-SS-240924-19-03' },
],
logisticsCompany: '한진택배',
vehicleTonnage: '5톤',
shippingCost: 150000,
vehicleNo: '서울 12가 3456',
driverName: '최운전',
driverContact: '010-5555-6666',
remarks: '하차 시 주의 요망',
};
// 품질관리서 목록
export const MOCK_REPORTS: InspectionReport[] = [
{
id: '1',
code: 'KD-SS-2024-530',
siteName: '강남 아파트 단지',
item: '실리카 스크린',
routeCount: 2,
totalRoutes: 14,
quarter: '2025년 3분기',
year: 2025,
quarterNum: 3,
},
{
id: '2',
code: 'KD-SS-2024-531',
siteName: '서초 오피스텔',
item: '알루미늄 패널',
routeCount: 1,
totalRoutes: 8,
quarter: '2025년 3분기',
year: 2025,
quarterNum: 3,
},
{
id: '3',
code: 'KD-SS-2024-520',
siteName: '송파 주상복합',
item: '유리 커튼월',
routeCount: 3,
totalRoutes: 21,
quarter: '2025년 2분기',
year: 2025,
quarterNum: 2,
},
];
// 수주루트 목록 (reportId로 연결)
export const MOCK_ROUTES: Record<string, RouteItem[]> = {
'1': [
{
id: '1-1',
code: 'KD-SS-240924-19',
date: '2024-09-24',
site: '강남 아파트 A동',
locationCount: 7,
subItems: [
{ id: '1-1-1', name: 'KD-SS-240924-19-01', location: '101동 501호', status: '합격' },
{ id: '1-1-2', name: 'KD-SS-240924-19-02', location: '101동 502호', status: '합격' },
{ id: '1-1-3', name: 'KD-SS-240924-19-03', location: '101동 503호', status: '합격' },
{ id: '1-1-4', name: 'KD-SS-240924-19-04', location: '101동 601호', status: '합격' },
{ id: '1-1-5', name: 'KD-SS-240924-19-05', location: '101동 602호', status: '합격' },
{ id: '1-1-6', name: 'KD-SS-240924-19-06', location: '101동 603호', status: '합격' },
{ id: '1-1-7', name: 'KD-SS-240924-19-07', location: '102동 501호', status: '합격' },
],
},
{
id: '1-2',
code: 'KD-SS-241024-15',
date: '2024-10-24',
site: '강남 아파트 B동',
locationCount: 7,
subItems: [
{ id: '1-2-1', name: 'KD-SS-241024-15-01', location: '103동 501호', status: '합격' },
{ id: '1-2-2', name: 'KD-SS-241024-15-02', location: '103동 502호', status: '대기' },
],
},
],
'2': [
{
id: '2-1',
code: 'SC-AP-241101-01',
date: '2024-11-01',
site: '서초 오피스텔 본관',
locationCount: 8,
subItems: [
{ id: '2-1-1', name: 'SC-AP-241101-01-01', location: '1층 로비', status: '합격' },
{ id: '2-1-2', name: 'SC-AP-241101-01-02', location: '2층 사무실', status: '합격' },
],
},
],
'3': [
{
id: '3-1',
code: 'SP-CW-240801-01',
date: '2024-08-01',
site: '송파 주상복합 A타워',
locationCount: 10,
subItems: [
{ id: '3-1-1', name: 'SP-CW-240801-01-01', location: '1층 외벽', status: '합격' },
],
},
{
id: '3-2',
code: 'SP-CW-240815-02',
date: '2024-08-15',
site: '송파 주상복합 B타워',
locationCount: 8,
subItems: [],
},
{
id: '3-3',
code: 'SP-CW-240901-03',
date: '2024-09-01',
site: '송파 주상복합 상가동',
locationCount: 3,
subItems: [],
},
],
};
// 문서 목록 (routeId로 연결)
export const MOCK_DOCUMENTS: Record<string, Document[]> = {
'1-1': [
{
id: 'doc-1',
type: 'import',
title: '수입검사 성적서',
count: 3,
items: [
{ id: 'doc-1-1', title: '원단 수입검사 성적서', date: '2024-08-10', code: 'RM-2024-1234' },
{ id: 'doc-1-2', title: '철판 수입검사 성적서', date: '2024-08-12', code: 'RM-2024-1235' },
{ id: 'doc-1-3', title: '방화실 수입검사 성적서', date: '2024-08-15', code: 'RM-2024-1236' },
],
},
{
id: 'doc-2',
type: 'order',
title: '수주서',
count: 1,
items: [
{ id: 'doc-2-1', title: '수주서', date: '2024-09-20', code: 'ORD-2024-530' },
],
},
{
id: 'doc-3',
type: 'log',
title: '작업일지',
count: 2,
items: [
{ id: 'doc-3-1', title: '생산 작업일지', date: '2024-09-25', code: 'WL-2024-0925' },
{ id: 'doc-3-2', title: '후가공 작업일지', date: '2024-09-26', code: 'WL-2024-0926' },
],
},
{
id: 'doc-4',
type: 'report',
title: '중간검사 성적서',
count: 4,
items: [
{ id: 'doc-4-1', title: '스크린 중간검사 성적서', date: '2024-09-25', code: 'MID-2024-001', subType: 'screen' as const },
{ id: 'doc-4-2', title: '절곡품 중간검사 성적서', date: '2024-09-26', code: 'MID-2024-002', subType: 'bending' as const },
{ id: 'doc-4-3', title: '슬랫 중간검사 성적서', date: '2024-09-27', code: 'MID-2024-003', subType: 'slat' as const },
{ id: 'doc-4-4', title: '조인트바 중간검사 성적서', date: '2024-09-28', code: 'MID-2024-004', subType: 'jointbar' as const },
],
},
{
id: 'doc-5',
type: 'confirmation',
title: '납품확인서',
count: 1,
items: [
{ id: 'doc-5-1', title: '납품확인서', date: '2024-10-05', code: 'DEL-2024-530' },
],
},
{
id: 'doc-6',
type: 'shipping',
title: '출고증',
count: 1,
items: [
{ id: 'doc-6-1', title: '출고증', date: '2024-10-04', code: 'SHP-2024-530' },
],
},
{
id: 'doc-7',
type: 'product',
title: '제품검사 성적서',
count: 7,
items: [
{ id: 'doc-7-1', title: '제품검사 성적서-01', date: '2024-09-30', code: 'PRD-2024-001' },
{ id: 'doc-7-2', title: '제품검사 성적서-02', date: '2024-09-30', code: 'PRD-2024-002' },
{ id: 'doc-7-3', title: '제품검사 성적서-03', date: '2024-09-30', code: 'PRD-2024-003' },
],
},
{
id: 'doc-8',
type: 'quality',
title: '품질관리서',
count: 1,
items: [
{ id: 'doc-8-1', title: '품질관리서', date: '2024-10-10', code: 'QC-2024-530' },
],
},
],
'1-2': [
{
id: 'doc-9',
type: 'import',
title: '수입검사 성적서',
count: 2,
items: [
{ id: 'doc-9-1', title: '원단 수입검사 성적서', date: '2024-10-20', code: 'RM-2024-2001' },
{ id: 'doc-9-2', title: '철판 수입검사 성적서', date: '2024-10-21', code: 'RM-2024-2002' },
],
},
{
id: 'doc-10',
type: 'order',
title: '수주서',
count: 1,
items: [
{ id: 'doc-10-1', title: '수주서', date: '2024-10-22', code: 'ORD-2024-531' },
],
},
],
};
// 기본 문서 (루트가 선택되지 않았을 때)
export const DEFAULT_DOCUMENTS: Document[] = [
{ id: 'def-1', type: 'import', title: '수입검사 성적서', count: 0, items: [] },
{ id: 'def-2', type: 'order', title: '수주서', count: 0, items: [] },
{ id: 'def-3', type: 'log', title: '작업일지', count: 0, items: [] },
{ id: 'def-4', type: 'report', title: '중간검사 성적서', count: 0, items: [] },
{ id: 'def-5', type: 'confirmation', title: '납품확인서', count: 0, items: [] },
{ id: 'def-6', type: 'shipping', title: '출고증', count: 0, items: [] },
{ id: 'def-7', type: 'product', title: '제품검사 성적서', count: 0, items: [] },
{ id: 'def-8', type: 'quality', title: '품질관리서', count: 0, items: [] },
];

View File

@@ -1,50 +1,151 @@
"use client";
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
import { Header } from './components/Header';
import { Filters } from './components/Filters';
import { ReportList } from './components/ReportList';
import { RouteList } from './components/RouteList';
import { DocumentList } from './components/DocumentList';
import { InspectionModal } from './components/InspectionModal';
import { Document } from './types';
import { InspectionReport, RouteItem, Document, DocumentItem } from './types';
import { MOCK_REPORTS, MOCK_ROUTES, MOCK_DOCUMENTS, DEFAULT_DOCUMENTS } from './mockData';
export default function QualityInspectionPage() {
const [modalOpen, setModalOpen] = useState(false);
const [selectedDoc, setSelectedDoc] = useState<Document | null>(null);
// 필터 상태
const [selectedYear, setSelectedYear] = useState(2025);
const [selectedQuarter, setSelectedQuarter] = useState<string>('전체');
const [searchTerm, setSearchTerm] = useState('');
const handleViewDocument = (doc: Document) => {
setSelectedDoc(doc);
setModalOpen(true);
};
// 선택 상태
const [selectedReport, setSelectedReport] = useState<InspectionReport | null>(null);
const [selectedRoute, setSelectedRoute] = useState<RouteItem | null>(null);
return (
<div className="w-full h-[calc(100vh-64px)] p-6 bg-slate-100 flex flex-col overflow-hidden">
<Header />
<Filters />
// 모달 상태
const [modalOpen, setModalOpen] = useState(false);
const [selectedDoc, setSelectedDoc] = useState<Document | null>(null);
const [selectedDocItem, setSelectedDocItem] = useState<DocumentItem | null>(null);
<div className="flex-1 grid grid-cols-12 gap-6 min-h-0">
{/* Left Panel: Report List */}
<div className="col-span-12 lg:col-span-3 h-full overflow-hidden">
<ReportList />
</div>
// 필터링된 리포트 목록
const filteredReports = useMemo(() => {
return MOCK_REPORTS.filter((report) => {
// 년도 필터
if (report.year !== selectedYear) return false;
{/* Middle Panel: Route List */}
<div className="col-span-12 lg:col-span-4 h-full overflow-hidden">
<RouteList />
</div>
// 분기 필터
if (selectedQuarter !== '전체') {
const quarterNum = parseInt(selectedQuarter.replace('분기', ''));
if (report.quarterNum !== quarterNum) return false;
}
{/* Right Panel: Documents */}
<div className="col-span-12 lg:col-span-5 h-full overflow-hidden">
<DocumentList onViewDocument={handleViewDocument} />
</div>
</div>
// 검색어 필터
if (searchTerm) {
const term = searchTerm.toLowerCase();
const matchesCode = report.code.toLowerCase().includes(term);
const matchesSite = report.siteName.toLowerCase().includes(term);
const matchesItem = report.item.toLowerCase().includes(term);
if (!matchesCode && !matchesSite && !matchesItem) return false;
}
<InspectionModal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
title={selectedDoc?.title || '수입검사 성적서'}
/>
return true;
});
}, [selectedYear, selectedQuarter, searchTerm]);
// 선택된 리포트의 루트 목록
const currentRoutes = useMemo(() => {
if (!selectedReport) return [];
return MOCK_ROUTES[selectedReport.id] || [];
}, [selectedReport]);
// 선택된 루트의 문서 목록
const currentDocuments = useMemo(() => {
if (!selectedRoute) return DEFAULT_DOCUMENTS;
return MOCK_DOCUMENTS[selectedRoute.id] || DEFAULT_DOCUMENTS;
}, [selectedRoute]);
// 리포트 선택 핸들러
const handleReportSelect = (report: InspectionReport) => {
setSelectedReport(report);
setSelectedRoute(null); // 루트 선택 초기화
};
// 루트 선택 핸들러
const handleRouteSelect = (route: RouteItem) => {
setSelectedRoute(route);
};
// 문서 보기 핸들러
const handleViewDocument = (doc: Document, item?: DocumentItem) => {
setSelectedDoc(doc);
setSelectedDocItem(item || null);
setModalOpen(true);
};
// 필터 핸들러
const handleYearChange = (year: number) => {
setSelectedYear(year);
setSelectedReport(null);
setSelectedRoute(null);
};
const handleQuarterChange = (quarter: string) => {
setSelectedQuarter(quarter);
setSelectedReport(null);
setSelectedRoute(null);
};
const handleSearchChange = (term: string) => {
setSearchTerm(term);
};
return (
<div className="w-full h-[calc(100vh-64px)] p-6 bg-slate-100 flex flex-col overflow-hidden">
<Header />
<Filters
selectedYear={selectedYear}
selectedQuarter={selectedQuarter}
searchTerm={searchTerm}
onYearChange={handleYearChange}
onQuarterChange={handleQuarterChange}
onSearchChange={handleSearchChange}
/>
<div className="flex-1 grid grid-cols-12 gap-6 min-h-0">
{/* Left Panel: Report List */}
<div className="col-span-12 lg:col-span-3 h-full overflow-hidden">
<ReportList
reports={filteredReports}
selectedId={selectedReport?.id || null}
onSelect={handleReportSelect}
/>
</div>
);
}
{/* Middle Panel: Route List */}
<div className="col-span-12 lg:col-span-4 h-full overflow-hidden">
<RouteList
routes={currentRoutes}
selectedId={selectedRoute?.id || null}
onSelect={handleRouteSelect}
reportCode={selectedReport?.code || null}
/>
</div>
{/* Right Panel: Documents */}
<div className="col-span-12 lg:col-span-5 h-full overflow-hidden">
<DocumentList
documents={currentDocuments}
routeCode={selectedRoute?.code || null}
onViewDocument={handleViewDocument}
/>
</div>
</div>
<InspectionModal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
document={selectedDoc}
documentItem={selectedDocItem}
/>
</div>
);
}

View File

@@ -6,6 +6,8 @@ export interface InspectionReport {
routeCount: number;
totalRoutes: number;
quarter: string; // e.g. 2025년 3분기
year: number; // 필터용
quarterNum: number; // 필터용 (1, 2, 3, 4)
}
export interface RouteItem {
@@ -38,4 +40,6 @@ export interface DocumentItem {
title: string;
date: string;
code?: string;
// 중간검사 성적서 서브타입 (report 타입일 때만 사용)
subType?: 'screen' | 'bending' | 'slat' | 'jointbar';
}