diff --git a/.gitignore b/.gitignore index 5e9d859b..e01d2894 100644 --- a/.gitignore +++ b/.gitignore @@ -111,5 +111,5 @@ test-results/ .playwright/ # 로컬 테스트/개발용 폴더 -src/app/\[locale\]/(protected)/dev/ + src/components/common/EditableTable/ diff --git a/src/app/[locale]/(protected)/dev/editable-table/page.tsx b/src/app/[locale]/(protected)/dev/editable-table/page.tsx new file mode 100644 index 00000000..50d95d6e --- /dev/null +++ b/src/app/[locale]/(protected)/dev/editable-table/page.tsx @@ -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([ + { name: '샘플 품목 A', unitPrice: 5000 }, + { name: '샘플 품목 B', unitPrice: 12000 }, + { name: '샘플 품목 C', unitPrice: 8000 }, + ]); + + // 샘플 데이터 + const [products, setProducts] = useState([ + { 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[] = [ + { + 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 ( + +
+ {/* 사용법 안내 */} + + + 사용법 + + +

+ 행 추가 버튼을 클릭하여 새 행을 추가할 수 있습니다.

+

• 각 행의 🗑 버튼을 클릭하여 행을 삭제할 수 있습니다.

+

• 셀을 클릭하여 직접 값을 수정할 수 있습니다.

+

품목별 단가에서 단가를 수정하면 아래 품목 목록의 단가가 일괄 변경됩니다.

+
+
+ + {/* 품목별 마스터 단가 */} + + +
+ 품목별 단가 + + 단가 수정 시 아래 품목 목록에 자동 반영 + +
+
+ +
+ + + + 번호 + 품목명 + 단가 + 적용 건수 + + + + {masterPrices.map((master, index) => { + // 해당 품목이 품목 목록에 몇 건 있는지 계산 + const appliedCount = products.filter(p => p.name === master.name).length; + return ( + + + {index + 1} + + {master.name} + +
+ handleMasterPriceChange(index, Number(e.target.value))} + className="w-28 h-8 text-right" + /> + +
+
+ + {appliedCount > 0 ? ( + {appliedCount}건 + ) : ( + - + )} + +
+ ); + })} +
+
+
+
+
+ + {/* 편집 테이블 */} + + + {/* 합계 표시 */} + + +
+ 총 합계 + + {totalAmount.toLocaleString()}원 + +
+
+
+ + {/* 현재 데이터 확인 (디버깅용) */} + + + 현재 데이터 (JSON) + + +
+              {JSON.stringify(products, null, 2)}
+            
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/components/DocumentList.tsx b/src/app/[locale]/(protected)/dev/quality-inspection/components/DocumentList.tsx new file mode 100644 index 00000000..c1442222 --- /dev/null +++ b/src/app/[locale]/(protected)/dev/quality-inspection/components/DocumentList.tsx @@ -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 ; + case 'order': return ; + case 'log': return ; + case 'report': return ; + case 'confirmation': return ; + case 'shipping': return ; + case 'product': return ; + default: return ; + } +}; + +export const DocumentList = ({ onViewDocument }: DocumentListProps) => { + const [expandedId, setExpandedId] = useState('1'); + + const toggleExpand = (id: string) => { + setExpandedId(expandedId === id ? null : id); + }; + + return ( +
+

+ 관련 서류 (KD-SS-240924-19) +

+ +
+ {MOCK_DOCUMENTS.map((doc) => ( +
+
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'}`} + > +
+
+ {getIcon(doc.type)} +
+
+

{doc.title}

+

{doc.count}건의 서류

+
+
+ {expandedId === doc.id ? : } +
+ + {expandedId === doc.id && doc.items && ( +
+
+ {doc.items.map((item) => ( +
+
+
{item.title}
+
+ {item.date} | 로트: RM-2024-1234 +
+
+ +
+ ))} +
+ )} +
+ ))} +
+
+ ); +}; diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/components/Filters.tsx b/src/app/[locale]/(protected)/dev/quality-inspection/components/Filters.tsx new file mode 100644 index 00000000..55e02acd --- /dev/null +++ b/src/app/[locale]/(protected)/dev/quality-inspection/components/Filters.tsx @@ -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 ( +
+ {/* Year Selection */} +
+ 년도 +
+ +
+
+ + {/* Quarter Selection */} +
+ 분기 +
+ {quarters.map((q) => ( + + ))} +
+
+ + {/* Search Input */} +
+ 검색 +
+ + +
+
+ + {/* Search Button */} +
+ +
+
+ ); +}; diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/components/Header.tsx b/src/app/[locale]/(protected)/dev/quality-inspection/components/Header.tsx new file mode 100644 index 00000000..cd2dd5f5 --- /dev/null +++ b/src/app/[locale]/(protected)/dev/quality-inspection/components/Header.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export const Header = () => { + return ( +
+

품질인정심사 시스템

+

SAM - Smart Automation Management

+
+ ); +}; diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/components/InspectionModal.tsx b/src/app/[locale]/(protected)/dev/quality-inspection/components/InspectionModal.tsx new file mode 100644 index 00000000..2df6d760 --- /dev/null +++ b/src/app/[locale]/(protected)/dev/quality-inspection/components/InspectionModal.tsx @@ -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 ( + !open && onClose()}> + + +
+ {title} +

원단 수입검사 성적서 - 2024-08-10 로트: RM-2024-1234

+
+ {/* Close button is handled by DialogPrimitive usually, but adding custom controls here is fine */} +
+ + {/* Toolbar */} +
+
+ + + +
+
+ 100% + +
+
+ + {/* Content Area */} +
+
+
+ + + +
+

{title}

+

2024-08-10

+

+ 로트 번호: RM-2024-1234 +

+

실제 서류 이미지가 표시됩니다

+
+
+
+
+ ); +}; diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/components/ReportList.tsx b/src/app/[locale]/(protected)/dev/quality-inspection/components/ReportList.tsx new file mode 100644 index 00000000..86d99460 --- /dev/null +++ b/src/app/[locale]/(protected)/dev/quality-inspection/components/ReportList.tsx @@ -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 ( +
+
+

품질관리서 목록

+ + {MOCK_REPORTS.length}건 + +
+ +
+ {MOCK_REPORTS.map((report) => ( +
+
+ {report.quarter} +
+ +

{report.code}

+

{report.siteName}

+

인정품목: {report.item}

+ +
+ + 수주루트 {report.routeCount}건 + (총 {report.totalRoutes}개소) +
+
+ ))} +
+
+ ); +}; diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/components/RouteList.tsx b/src/app/[locale]/(protected)/dev/quality-inspection/components/RouteList.tsx new file mode 100644 index 00000000..f695ebd4 --- /dev/null +++ b/src/app/[locale]/(protected)/dev/quality-inspection/components/RouteList.tsx @@ -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('1'); + + const toggleExpand = (id: string) => { + setExpandedId(expandedId === id ? null : id); + }; + + return ( +
+

+ 수주루트 목록 (KD-SS-2024-530) +

+ +
+ {MOCK_ROUTES.map((route) => ( +
+
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'}`} + > +
+
+ {expandedId === route.id &&
} +

{route.code}

+
+

수주일: {route.date}

+

현장: {route.site}

+
+ + {route.locationCount}개소 +
+
+ {expandedId === route.id ? : } +
+ + {expandedId === route.id && ( +
+
+ 개소별 제품로트 +
+ {route.subItems.map((item) => ( +
+
+
{item.name}
+
{item.location}
+
+ + {item.status} + +
+ ))} +
+ )} +
+ ))} +
+
+ ); +}; diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/page.tsx b/src/app/[locale]/(protected)/dev/quality-inspection/page.tsx new file mode 100644 index 00000000..2ab7ca6d --- /dev/null +++ b/src/app/[locale]/(protected)/dev/quality-inspection/page.tsx @@ -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(null); + + const handleViewDocument = (doc: Document) => { + setSelectedDoc(doc); + setModalOpen(true); + }; + + return ( +
+
+ + +
+ {/* Left Panel: Report List */} +
+ +
+ + {/* Middle Panel: Route List */} +
+ +
+ + {/* Right Panel: Documents */} +
+ +
+
+ + setModalOpen(false)} + title={selectedDoc?.title || '수입검사 성적서'} + /> +
+ ); +} diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/types.ts b/src/app/[locale]/(protected)/dev/quality-inspection/types.ts new file mode 100644 index 00000000..8c5c00c7 --- /dev/null +++ b/src/app/[locale]/(protected)/dev/quality-inspection/types.ts @@ -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; +}