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:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
339
src/app/[locale]/(protected)/dev/quality-inspection/mockData.ts
Normal file
339
src/app/[locale]/(protected)/dev/quality-inspection/mockData.ts
Normal 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: [] },
|
||||
];
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user