feat: dev 폴더 품질검사 및 편집 테이블 페이지 추가

- quality-inspection 페이지 및 컴포넌트 추가
- editable-table 테스트 페이지 추가
- .gitignore에서 dev 폴더 추적 허용

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-12-29 09:09:37 +09:00
parent f0c0de2ecd
commit d957f72198
10 changed files with 733 additions and 1 deletions

2
.gitignore vendored
View File

@@ -111,5 +111,5 @@ test-results/
.playwright/
# 로컬 테스트/개발용 폴더
src/app/\[locale\]/(protected)/dev/
src/components/common/EditableTable/

View File

@@ -0,0 +1,253 @@
'use client';
import { useState, useCallback } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { EditableTable, EditableColumn } from '@/components/common/EditableTable';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
// 샘플 데이터 타입
interface ProductItem {
id: string;
name: string;
quantity: number;
unitPrice: number;
unit: string;
note: string;
}
// 품목 마스터 단가 타입
interface ProductMaster {
name: string;
unitPrice: number;
}
// 유니크 ID 생성
let idCounter = 0;
const generateId = () => `item-${++idCounter}`;
export default function EditableTableSamplePage() {
// 품목별 마스터 단가
const [masterPrices, setMasterPrices] = useState<ProductMaster[]>([
{ name: '샘플 품목 A', unitPrice: 5000 },
{ name: '샘플 품목 B', unitPrice: 12000 },
{ name: '샘플 품목 C', unitPrice: 8000 },
]);
// 샘플 데이터
const [products, setProducts] = useState<ProductItem[]>([
{ id: generateId(), name: '샘플 품목 A', quantity: 10, unitPrice: 5000, unit: 'EA', note: '' },
{ id: generateId(), name: '샘플 품목 B', quantity: 5, unitPrice: 12000, unit: 'BOX', note: '특이사항 있음' },
]);
// 마스터 단가 변경 시 품목 목록도 업데이트
const handleMasterPriceChange = useCallback((index: number, newPrice: number) => {
const targetName = masterPrices[index].name;
// 마스터 단가 업데이트
setMasterPrices(prev => {
const updated = [...prev];
updated[index] = { ...updated[index], unitPrice: newPrice };
return updated;
});
// 품목 목록에서 해당 품목의 단가도 일괄 업데이트
setProducts(prev =>
prev.map(product =>
product.name === targetName
? { ...product, unitPrice: newPrice }
: product
)
);
}, [masterPrices]);
// 컬럼 정의
const columns: EditableColumn<ProductItem>[] = [
{
key: 'name',
header: '품목명',
type: 'text',
placeholder: '품목명 입력',
width: '200px',
},
{
key: 'quantity',
header: '수량',
type: 'number',
placeholder: '0',
width: '100px',
align: 'right',
},
{
key: 'unit',
header: '단위',
type: 'select',
width: '100px',
options: [
{ label: 'EA', value: 'EA' },
{ label: 'BOX', value: 'BOX' },
{ label: 'SET', value: 'SET' },
{ label: 'KG', value: 'KG' },
{ label: 'M', value: 'M' },
],
},
{
key: 'unitPrice',
header: '단가',
type: 'number',
placeholder: '0',
width: '120px',
align: 'right',
},
{
key: 'note',
header: '비고',
type: 'text',
placeholder: '비고 입력',
},
];
// 새 행 생성 함수
const createNewProduct = (): ProductItem => ({
id: generateId(),
name: '',
quantity: 0,
unitPrice: 0,
unit: 'EA',
note: '',
});
// 합계 계산
const totalAmount = products.reduce((sum, item) => sum + (item.quantity * item.unitPrice), 0);
return (
<PageLayout
title="EditableTable 샘플"
description="행 추가/삭제가 가능한 편집 테이블 컴포넌트 예제"
>
<div className="space-y-6">
{/* 사용법 안내 */}
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p> <Badge variant="outline">+ </Badge> .</p>
<p> <Badge variant="outline" className="text-destructive">🗑</Badge> .</p>
<p> .</p>
<p> <Badge variant="secondary"> </Badge> .</p>
</CardContent>
</Card>
{/* 품목별 마스터 단가 */}
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base"> </CardTitle>
<span className="text-sm text-muted-foreground">
</span>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[60px] text-center"></TableHead>
<TableHead></TableHead>
<TableHead className="w-[150px] text-right"></TableHead>
<TableHead className="w-[100px] text-center"> </TableHead>
</TableRow>
</TableHeader>
<TableBody>
{masterPrices.map((master, index) => {
// 해당 품목이 품목 목록에 몇 건 있는지 계산
const appliedCount = products.filter(p => p.name === master.name).length;
return (
<TableRow key={master.name}>
<TableCell className="text-center text-muted-foreground">
{index + 1}
</TableCell>
<TableCell className="font-medium">{master.name}</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Input
type="number"
value={master.unitPrice}
onChange={(e) => handleMasterPriceChange(index, Number(e.target.value))}
className="w-28 h-8 text-right"
/>
<span className="text-muted-foreground text-sm"></span>
</div>
</TableCell>
<TableCell className="text-center">
{appliedCount > 0 ? (
<Badge variant="secondary">{appliedCount}</Badge>
) : (
<span className="text-muted-foreground text-sm">-</span>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
{/* 편집 테이블 */}
<EditableTable
title="품목 목록"
columns={columns}
data={products}
onChange={setProducts}
createNewRow={createNewProduct}
addButtonLabel="품목 추가"
emptyMessage="품목이 없습니다. 품목을 추가해주세요."
showRowNumber={true}
maxRows={20}
minRows={0}
striped={true}
bulkAdd={true}
bulkAddLabel="품목 추가"
/>
{/* 합계 표시 */}
<Card>
<CardContent className="py-4">
<div className="flex justify-between items-center">
<span className="font-medium"> </span>
<span className="text-xl font-bold text-primary">
{totalAmount.toLocaleString()}
</span>
</div>
</CardContent>
</Card>
{/* 현재 데이터 확인 (디버깅용) */}
<Card>
<CardHeader>
<CardTitle className="text-base"> (JSON)</CardTitle>
</CardHeader>
<CardContent>
<pre className="bg-muted p-4 rounded-md text-xs overflow-auto max-h-60">
{JSON.stringify(products, null, 2)}
</pre>
</CardContent>
</Card>
</div>
</PageLayout>
);
}

View File

@@ -0,0 +1,96 @@
"use client";
import React, { useState } from 'react';
import {
FileText, CheckCircle, ChevronDown, ChevronUp,
Eye, Truck, Calendar, ClipboardCheck, Box
} from 'lucide-react';
import { Document } from '../types';
interface DocumentListProps {
onViewDocument: (doc: Document) => 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" />;
}
};
export const DocumentList = ({ onViewDocument }: DocumentListProps) => {
const [expandedId, setExpandedId] = useState<string | null>('1');
const toggleExpand = (id: string) => {
setExpandedId(expandedId === id ? null : 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-240924-19)</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>
{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>
)}
</div>
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,65 @@
"use client";
import React, { useState } from 'react';
import { Search } from 'lucide-react';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
export const Filters = () => {
const [activeQuarter, setActiveQuarter] = useState('전체');
const quarters = ['전체', '1분기', '2분기', '3분기', '4분기'];
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>
</div>
);
};

View File

@@ -0,0 +1,10 @@
import React from 'react';
export const Header = () => {
return (
<div className="w-full bg-[#1e3a8a] text-white p-6 rounded-lg mb-4 shadow-md flex flex-col justify-center h-24">
<h1 className="text-2xl font-bold mb-1"> </h1>
<p className="text-sm opacity-80 text-blue-100">SAM - Smart Automation Management</p>
</div>
);
};

View File

@@ -0,0 +1,72 @@
"use client";
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';
import { Button } from "@/components/ui/button";
interface InspectionModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
}
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>
);
};

View File

@@ -0,0 +1,53 @@
"use client";
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분기'
}
];
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>
<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>
<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>
<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>
);
};

View File

@@ -0,0 +1,92 @@
"use client";
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: []
}
];
export const RouteList = () => {
const [expandedId, setExpandedId] = useState<string | null>('1');
const toggleExpand = (id: string) => {
setExpandedId(expandedId === id ? null : 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>
<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>
{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>
)}
</div>
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,50 @@
"use client";
import React, { useState } 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';
export default function QualityInspectionPage() {
const [modalOpen, setModalOpen] = useState(false);
const [selectedDoc, setSelectedDoc] = useState<Document | null>(null);
const handleViewDocument = (doc: Document) => {
setSelectedDoc(doc);
setModalOpen(true);
};
return (
<div className="w-full h-[calc(100vh-64px)] p-6 bg-slate-100 flex flex-col overflow-hidden">
<Header />
<Filters />
<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>
{/* Middle Panel: Route List */}
<div className="col-span-12 lg:col-span-4 h-full overflow-hidden">
<RouteList />
</div>
{/* Right Panel: Documents */}
<div className="col-span-12 lg:col-span-5 h-full overflow-hidden">
<DocumentList onViewDocument={handleViewDocument} />
</div>
</div>
<InspectionModal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
title={selectedDoc?.title || '수입검사 성적서'}
/>
</div>
);
}

View File

@@ -0,0 +1,41 @@
export interface InspectionReport {
id: string;
code: string; // e.g., KD-SS-2024-530
siteName: string; // e.g., 강남 아파트 단지
item: string; // e.g., 실리카 스크린
routeCount: number;
totalRoutes: number;
quarter: string; // e.g. 2025년 3분기
}
export interface RouteItem {
id: string;
code: string; // e.g., KD-SS-240924-19
date: string; // 2024-09-24
site: string; // 강남 아파트 A동
locationCount: number;
subItems: UnitInspection[];
}
export interface UnitInspection {
id: string;
name: string; // e.g., KD-SS-240924-19-01
location: string; // e.g., 101동 501호
status: '합격' | '불합격' | '대기';
}
export interface Document {
id: string;
type: 'import' | 'order' | 'log' | 'report' | 'confirmation' | 'shipping' | 'product' | 'quality';
title: string; // e.g., 수입검사 성적서
date?: string;
count: number; // e.g., 3건의 서류
items?: DocumentItem[];
}
export interface DocumentItem {
id: string;
title: string;
date: string;
code?: string;
}