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:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -111,5 +111,5 @@ test-results/
|
||||
.playwright/
|
||||
|
||||
# 로컬 테스트/개발용 폴더
|
||||
src/app/\[locale\]/(protected)/dev/
|
||||
|
||||
src/components/common/EditableTable/
|
||||
|
||||
253
src/app/[locale]/(protected)/dev/editable-table/page.tsx
Normal file
253
src/app/[locale]/(protected)/dev/editable-table/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
50
src/app/[locale]/(protected)/dev/quality-inspection/page.tsx
Normal file
50
src/app/[locale]/(protected)/dev/quality-inspection/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
41
src/app/[locale]/(protected)/dev/quality-inspection/types.ts
Normal file
41
src/app/[locale]/(protected)/dev/quality-inspection/types.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user