From fb2be8651e7ef4a1656ce302218830bf2ea20e0f Mon Sep 17 00:00:00 2001 From: byeongcheolryu Date: Mon, 29 Dec 2025 14:53:05 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=92=88=EC=A7=88=EA=B2=80=EC=82=AC=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20PDF=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=8D=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 수입검사 성적서 컴포넌트 추가 - 제품검사 성적서 컴포넌트 추가 - 중간검사 성적서 4종 추가 (스크린/절곡품/슬랫/조인트바) - 품질관리서 PDF 업로드/뷰어 컴포넌트 구현 - InspectionModal 문서 타입별 렌더링 연동 - mockData 샘플 데이터 추가 - types.ts DocumentItem에 subType 필드 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/DocumentList.tsx | 196 ++++--- .../quality-inspection/components/Filters.tsx | 138 ++--- .../components/InspectionModal.tsx | 492 +++++++++++++++--- .../components/ReportList.tsx | 98 ++-- .../components/RouteList.tsx | 174 ++++--- .../documents/BendingInspectionDocument.tsx | 349 +++++++++++++ .../documents/ImportInspectionDocument.tsx | 419 +++++++++++++++ .../documents/JointbarInspectionDocument.tsx | 298 +++++++++++ .../documents/ProductInspectionDocument.tsx | 289 ++++++++++ .../documents/QualityDocumentUploader.tsx | 322 ++++++++++++ .../documents/ScreenInspectionDocument.tsx | 309 +++++++++++ .../documents/SlatInspectionDocument.tsx | 286 ++++++++++ .../components/documents/index.ts | 29 ++ .../dev/quality-inspection/mockData.ts | 339 ++++++++++++ .../dev/quality-inspection/page.tsx | 167 ++++-- .../dev/quality-inspection/types.ts | 4 + 16 files changed, 3554 insertions(+), 355 deletions(-) create mode 100644 src/app/[locale]/(protected)/dev/quality-inspection/components/documents/BendingInspectionDocument.tsx create mode 100644 src/app/[locale]/(protected)/dev/quality-inspection/components/documents/ImportInspectionDocument.tsx create mode 100644 src/app/[locale]/(protected)/dev/quality-inspection/components/documents/JointbarInspectionDocument.tsx create mode 100644 src/app/[locale]/(protected)/dev/quality-inspection/components/documents/ProductInspectionDocument.tsx create mode 100644 src/app/[locale]/(protected)/dev/quality-inspection/components/documents/QualityDocumentUploader.tsx create mode 100644 src/app/[locale]/(protected)/dev/quality-inspection/components/documents/ScreenInspectionDocument.tsx create mode 100644 src/app/[locale]/(protected)/dev/quality-inspection/components/documents/SlatInspectionDocument.tsx create mode 100644 src/app/[locale]/(protected)/dev/quality-inspection/components/documents/index.ts create mode 100644 src/app/[locale]/(protected)/dev/quality-inspection/mockData.ts diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/components/DocumentList.tsx b/src/app/[locale]/(protected)/dev/quality-inspection/components/DocumentList.tsx index c1442222..a8b3d031 100644 --- a/src/app/[locale]/(protected)/dev/quality-inspection/components/DocumentList.tsx +++ b/src/app/[locale]/(protected)/dev/quality-inspection/components/DocumentList.tsx @@ -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 ; - case 'order': return ; - case 'log': return ; - case 'report': return ; - case 'confirmation': return ; - case 'shipping': return ; - case 'product': return ; - default: return ; + switch (type) { + case 'import': return ; + case 'order': return ; + case 'log': return ; + case 'report': return ; + case 'confirmation': return ; + case 'shipping': return ; + case 'product': return ; + case 'quality': return ; + default: return ; + } +}; + +export const DocumentList = ({ documents, routeCode, onViewDocument }: DocumentListProps) => { + const [expandedId, setExpandedId] = useState(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('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 ( -
-

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

+ return ( +
+

+ 관련 서류{' '} + {routeCode && ( + ({routeCode}) + )} +

-
- {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 ? : } -
+
+ {!routeCode ? ( +
+ 수주루트를 선택해주세요. +
+ ) : ( + 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 && ( -
-
- {doc.items.map((item) => ( -
-
-
{item.title}
-
- {item.date} | 로트: RM-2024-1234 -
-
- -
- ))} -
- )} + return ( +
+
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'}`} + > +
+
+ {getIcon(doc.type)}
- ))} -
-
- ); -}; +
+

{doc.title}

+

+ {doc.count > 0 ? `${doc.count}건의 서류` : '서류 없음'} +

+
+
+ {hasMultipleItems && ( + isExpanded ? ( + + ) : ( + + ) + )} +
+ + {isExpanded && hasMultipleItems && ( +
+
+ {doc.items!.map((item) => ( +
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" + > +
+
{item.title}
+
+ {item.date} + {item.code && ( + <> + | + 로트: {item.code} + + )} +
+
+ +
+ ))} +
+ )} +
+ ); + }) + )} +
+
+ ); +}; \ No newline at end of file diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/components/Filters.tsx b/src/app/[locale]/(protected)/dev/quality-inspection/components/Filters.tsx index 55e02acd..920a7b3e 100644 --- a/src/app/[locale]/(protected)/dev/quality-inspection/components/Filters.tsx +++ b/src/app/[locale]/(protected)/dev/quality-inspection/components/Filters.tsx @@ -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 ( -
- {/* Year Selection */} -
- 년도 -
- -
-
- - {/* Quarter Selection */} -
- 분기 -
- {quarters.map((q) => ( - - ))} -
-
- - {/* Search Input */} -
- 검색 -
- - -
-
- - {/* Search Button */} -
- -
+ return ( +
+ {/* Year Selection */} +
+ 년도 +
+
- ); -}; +
+ + {/* Quarter Selection */} +
+ 분기 +
+ {quarters.map((q) => ( + + ))} +
+
+ + {/* Search Input */} +
+ 검색 +
+ + 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" + /> +
+
+ + {/* Search Button */} +
+ +
+
+ ); +}; \ No newline at end of file diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/components/InspectionModal.tsx b/src/app/[locale]/(protected)/dev/quality-inspection/components/InspectionModal.tsx index 2df6d760..2edd41c5 100644 --- a/src/app/[locale]/(protected)/dev/quality-inspection/components/InspectionModal.tsx +++ b/src/app/[locale]/(protected)/dev/quality-inspection/components/InspectionModal.tsx @@ -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 ( - !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 -

-

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

-
-
-
-
- ); +// 문서 타입별 정보 +const DOCUMENT_INFO: Record = { + 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 ( +
+
+ +
+

{info.label}

+

{docItem?.title || '문서'}

+ {docItem?.date && ( +

{docItem.date}

+ )} + {docItem?.code && ( +

+ 로트 번호: {docItem.code} +

+ )} +
+

양식 준비 중

+

디자인 파일이 필요합니다

+
+
+ ); +}; + +// 수주서 문서 컴포넌트 (간소화 버전) +const OrderDocument = () => { + const data = MOCK_ORDER_DATA; + + return ( +
+ {/* 헤더 */} +
+
+
KD
+
경동기업
+
+
수 주 서
+ + + + + + + + + + + + + + + + + + + +
+
+ +
+
작성검토승인
판매생산품질
+
+ + {/* 기본 정보 */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LOT NO.{data.lotNumber}수주일{data.orderDate}
발주처{data.client}현장명{data.siteName}
담당자{data.manager}연락처{data.managerContact}
납기요청일{data.deliveryRequestDate}출고예정일{data.expectedShipDate}
배송방법{data.deliveryMethod}배송지{data.address}
+ + {/* 품목 테이블 */} + + + + + + + + + + + + + + {data.items.map((item, index) => ( + + + + + + + + + + ))} + + + + + + + + + + + + + + + +
No품목명규격단위수량단가금액
{index + 1}{item.name}{item.specification}{item.unit}{item.quantity}{item.unitPrice?.toLocaleString()}{item.amount?.toLocaleString()}
소계{data.subtotal.toLocaleString()}원
할인 ({data.discountRate}%)-{(data.subtotal * data.discountRate / 100).toLocaleString()}원
총액{data.totalAmount.toLocaleString()}원
+ + {/* 비고 */} + {data.remarks && ( +
+

비고

+

{data.remarks}

+
+ )} +
+ ); +}; + +// 작업일지 문서 컴포넌트 (간소화 버전) +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 ( +
+ {/* 헤더 */} +
+
+ KD + 경동기업 +
+
+

작 업 일 지

+

{documentNo}

+

스크린 생산부서

+
+ + + + + + + + + + + + + + + + + + + +
+
+
작성검토승인
+
{order.assignees[0] || '-'}
+
판매생산품질
+
+ + {/* 기본 정보 */} +
+
+
+
발주처
+
{order.client}
+
+
+
현장명
+
{order.projectName}
+
+
+
+
+
작업일자
+
{today}
+
+
+
LOT NO.
+
{lotNo}
+
+
+
+
+
납기일
+
{order.dueDate}
+
+
+
지시수량
+
{order.quantity} EA
+
+
+
+ + {/* 품목 테이블 */} +
+
+
No
+
품목명
+
출/부호
+
규격
+
수량
+
상태
+
+ {items.map((item, index) => ( +
+
{item.no}
+
{item.name}
+
{item.location}
+
{item.spec}
+
{item.qty}
+
+ {item.status} +
+
+ ))} +
+ + {/* 특이사항 */} +
+
특이사항
+
{order.instruction || '-'}
+
+
+ ); +}; + +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 ; + case 'bending': + return ; + case 'slat': + return ; + case 'jointbar': + return ; + default: + // 서브타입이 없으면 기본 스크린 문서 + return ; + } + }; + + // 품질관리서 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 ; + case 'log': + return ; + case 'confirmation': + return ; + case 'shipping': + return ; + case 'import': + return ; + case 'product': + return ; + case 'report': + return renderReportDocument(); + case 'quality': + // 품질관리서는 PDF 업로드/뷰어 사용 + return ( + + ); + default: + // 양식 대기 중인 문서 + return ; + } + }; + + return ( + !open && onClose()}> + + +
+ {doc.title} +

{subtitle}

+
+
+ + {/* Toolbar */} +
+
+ + + +
+
+ 100% + + +
+
+ + {/* Content Area - 남은 공간 모두 사용 */} +
+ {renderDocumentContent()} +
+
+
+ ); }; diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/components/ReportList.tsx b/src/app/[locale]/(protected)/dev/quality-inspection/components/ReportList.tsx index 86d99460..0a80df31 100644 --- a/src/app/[locale]/(protected)/dev/quality-inspection/components/ReportList.tsx +++ b/src/app/[locale]/(protected)/dev/quality-inspection/components/ReportList.tsx @@ -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 ( -
-
-

품질관리서 목록

- - {MOCK_REPORTS.length}건 - -
+export const ReportList = ({ reports, selectedId, onSelect }: ReportListProps) => { + return ( +
+
+

품질관리서 목록

+ + {reports.length}건 + +
-
- {MOCK_REPORTS.map((report) => ( -
-
- {report.quarter} -
+
+ {reports.length === 0 ? ( +
+ 해당 조건의 품질관리서가 없습니다. +
+ ) : ( + reports.map((report) => { + const isSelected = selectedId === report.id; + return ( +
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' + }`} + > +
+ {report.quarter} +
-

{report.code}

-

{report.siteName}

-

인정품목: {report.item}

+

+ {report.code} +

+

{report.siteName}

+

인정품목: {report.item}

-
- - 수주루트 {report.routeCount}건 - (총 {report.totalRoutes}개소) -
-
- ))} -
-
- ); -}; +
+ + 수주루트 {report.routeCount}건 + (총 {report.totalRoutes}개소) +
+
+ ); + }) + )} +
+
+ ); +}; \ No newline at end of file diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/components/RouteList.tsx b/src/app/[locale]/(protected)/dev/quality-inspection/components/RouteList.tsx index f695ebd4..32b373cc 100644 --- a/src/app/[locale]/(protected)/dev/quality-inspection/components/RouteList.tsx +++ b/src/app/[locale]/(protected)/dev/quality-inspection/components/RouteList.tsx @@ -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('1'); +export const RouteList = ({ routes, selectedId, onSelect, reportCode }: RouteListProps) => { + const [expandedId, setExpandedId] = useState(null); - const toggleExpand = (id: string) => { - setExpandedId(expandedId === id ? null : id); - }; + const handleClick = (route: RouteItem) => { + onSelect(route); + setExpandedId(expandedId === route.id ? null : route.id); + }; - return ( -
-

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

+ return ( +
+

+ 수주루트 목록{' '} + {reportCode && ( + ({reportCode}) + )} +

-
- {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 ? : } -
+
+ {routes.length === 0 ? ( +
+ {reportCode ? '수주루트가 없습니다.' : '품질관리서를 선택해주세요.'} +
+ ) : ( + routes.map((route) => { + const isSelected = selectedId === route.id; + const isExpanded = expandedId === route.id; - {expandedId === route.id && ( -
-
- 개소별 제품로트 -
- {route.subItems.map((item) => ( -
-
-
{item.name}
-
{item.location}
-
- - {item.status} - -
- ))} -
- )} + return ( +
+
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' + }`} + > +
+
+ {isSelected &&
} +

+ {route.code} +

- ))} -
-
- ); -}; +

수주일: {route.date}

+

현장: {route.site}

+
+ + {route.locationCount}개소 +
+
+ {isExpanded ? ( + + ) : ( + + )} +
+ + {isExpanded && route.subItems.length > 0 && ( +
+
+ 개소별 제품로트 +
+ {route.subItems.map((item) => ( +
+
+
{item.name}
+
{item.location}
+
+ + {item.status} + +
+ ))} +
+ )} +
+ ); + }) + )} +
+
+ ); +}; \ No newline at end of file diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/components/documents/BendingInspectionDocument.tsx b/src/app/[locale]/(protected)/dev/quality-inspection/components/documents/BendingInspectionDocument.tsx new file mode 100644 index 00000000..874f8074 --- /dev/null +++ b/src/app/[locale]/(protected)/dev/quality-inspection/components/documents/BendingInspectionDocument.tsx @@ -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 ( +
+ {/* 헤더 */} +
+
+
KD
+
경동기업
KYUNGDONG COMPANY
+
+
+
절곡품
+
중간검사 성적서
+
+ + + + + + + + + + + + + + + + + + + +
+
+ +
+
작성검토승인
{data.approvers.writer}{data.approvers.reviewer}{data.approvers.approver}
판매/전진생산품질
+
+ + {/* 기본 정보 테이블 */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
품 명{data.productName}제품 LOT NO{data.lotNo}
규 격 + ☐ 필재 + {' '} + ☑ 스크린 + 로트크기{data.lotSize}
발주처{data.client}검사일자{data.inspectionDate}
현장명{data.siteName}검사자{data.inspector}
제품명{data.itemName}마감유형{data.susFinish}
+ + {/* 중간검사 기준서 - 케이스/커버마감재 */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
케이스커버
마감재
중간검사
기준서
도해검사항목검사기준검사방법검사주기관련규정
+
+ 케이스
도해 +
+
겉모양{data.standardInfo.caseFinish.appearance.criteria}{data.standardInfo.caseFinish.appearance.method}{data.standardInfo.caseFinish.appearance.frequency}{data.standardInfo.caseFinish.appearance.regulation}
치수{data.standardInfo.caseFinish.dimensions.criteria}{data.standardInfo.caseFinish.dimensions.method}{data.standardInfo.caseFinish.dimensions.frequency}{data.standardInfo.caseFinish.dimensions.regulation}
{data.standardInfo.caseFinish.inspection.criteria}
+ + {/* 중간검사 기준서 - 밴딩마감재 */} + + + + + + + + + + + + + + + + + + + + + + + + + + +
밴딩마감재
중간검사
기준서
+
+ 밴딩
도해 +
+
겉모양{data.standardInfo.bandFinish.appearance.criteria}{data.standardInfo.bandFinish.appearance.method}{data.standardInfo.bandFinish.appearance.frequency}{data.standardInfo.bandFinish.appearance.regulation}
치수
(mm)
{data.standardInfo.bandFinish.dimensions.criteria}{data.standardInfo.bandFinish.dimensions.regulation}
{data.standardInfo.bandFinish.inspection.criteria}{data.standardInfo.bandFinish.inspection.method}
+ + {/* 중간검사 DATA */} +
중간검사 DATA
+ + + + + + + + + + + + + + + + + + + + + {data.inspectionData.map((item, index) => ( + + + + + + + + + + + + + ))} + +
분류제품명타입절곡상태치수 [mm]⓪ 건전판정
길이도전차수측정값POINT도전차수
{item.category}{item.itemName}{item.type} + ☐ 양호 ☐ 불량 + {item.length > 0 ? item.length.toLocaleString() : ''}{item.conductance1}{item.measured1}{item.point}{item.conductance2} + ☐ 적합 ☐ 부적합 +
+ + {/* 부적합 내용 */} +
+
【부적합 내용】
+
{data.notes}
+
+ + {/* 문서번호 및 종합판정 */} +
+
{data.documentNo}
+
+
종합판정
+
+ {data.overallResult} +
+
+
KDPS-10-01
+
+
+ ); +}; \ No newline at end of file diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/components/documents/ImportInspectionDocument.tsx b/src/app/[locale]/(protected)/dev/quality-inspection/components/documents/ImportInspectionDocument.tsx new file mode 100644 index 00000000..a4752b5f --- /dev/null +++ b/src/app/[locale]/(protected)/dev/quality-inspection/components/documents/ImportInspectionDocument.tsx @@ -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 ( +
+ {/* 헤더 */} +
+
+
KD
+
경동기업
+
+
수 입 검 사 성 적 서
+
+ + + + + + + + + + + +
담당부서장
결재{data.approvers.writer}
+
접고일자: {data.reportDate}
+
+
+ + {/* 기본 정보 테이블 */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
품 명{data.productName}납품업체
(제조업체)
{data.supplier}
규 격
(두께*너비*길이)
{data.specification}로트번호{data.lotNo}
자재번호{data.materialNo}검사일자{data.inspectionDate}
로트크기{data.lotSize}검사자 {data.inspector}
+ + {/* 검사 항목 테이블 */} + + + + + + + + + + + + + + + + + + + + + {/* 1. 겉모양 */} + + + + + + + + + + + + + {/* 2. 치수 */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* 3. 인장강도 */} + + + + + + + + + + {/* 4. 연신율 */} + + + + + + + + + + + + + + + + + {/* 5. 아연의 최소 부착량 */} + + + + + + + + + +
NO검사항목검사기준검사방식검사주기측정값판정
(적/부)
n1
양호/불량
n2
양호/불량
n3
양호/불량
1겉모양 + 사용상 해로운
결함이 없을 것 +
육안검사 + ☑OK + ☐NG + + ☑OK + ☐NG + + ☑OK + ☐NG + + ☑OK + ☐NG +
2치수 + 두께
{data.inspectionItems.dimensions.thickness.standard} +
+
+ + 0.8 이상
~ 1.0 미만
+ ± 0.07 +
+
+ n = 3
c = 0 +
체크검사 + {data.inspectionItems.dimensions.thickness.measurements[0]} + + {data.inspectionItems.dimensions.thickness.measurements[1]} + + {data.inspectionItems.dimensions.thickness.measurements[2]} +
+
+ + 1.0 이상
~ 1.25 미만
+ ± 0.08 +
+
+
+ + 1.25 이상
~ 1.6 미만
+ ± 0.10 +
+
+
+ + 1.6 이상
~ 2.0 미만
+ ± 0.12 +
+
+ 너비
{data.inspectionItems.dimensions.width.standard} +
+
+ + 1250 미만 + + 7
- 0
+
+
+ {data.inspectionItems.dimensions.width.measurements[0]} + + {data.inspectionItems.dimensions.width.measurements[1]} + + {data.inspectionItems.dimensions.width.measurements[2]} +
+ 길이
{data.inspectionItems.dimensions.length.standard} +
+
+ + 2000 이상
~ 4000 미만
+ + 15
- 0
+
+
+ {data.inspectionItems.dimensions.length.measurements[0]} + + {data.inspectionItems.dimensions.length.measurements[1]} + + {data.inspectionItems.dimensions.length.measurements[2]} +
3인장강도 (N/
㎟)
+ {data.inspectionItems.tensileStrength.standard} + + {data.inspectionItems.tensileStrength.measurements[0]} +
4연신율
%
+
+ + 두께 0.6
이상
~ 1.0 미
+ 36 이상 +
+
+ 공급업체
밀시트 +
입고시 + {data.inspectionItems.elongation.measurements[0]} +
+
+ + 두께 1.0
이상
~ 1.6 미
+ 37 이상 +
+
+
+ + 두께 1.6
이상
~ 2.3
미만
+ 38 이상 +
+
5아연의 최소
부착량 (g/㎡)
+ 편면 17 이상 + + {data.inspectionItems.zincCoating.measurements.join(' / ')} +
+ + {/* 주석 */} +
+

※ 1.55mm의 경우 KS F 4510에 따른 MIN 1.5의 기준에 따름

+

※ 두께의 경우 너비 1000 이상 ~ 1250 미만 기준에 따름

+
+ + {/* 종합판정 */} +
+
+
종합판정
+
+ {data.overallResult === '합격' ? '☑' : '☐'} +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/components/documents/JointbarInspectionDocument.tsx b/src/app/[locale]/(protected)/dev/quality-inspection/components/documents/JointbarInspectionDocument.tsx new file mode 100644 index 00000000..c1fce2cc --- /dev/null +++ b/src/app/[locale]/(protected)/dev/quality-inspection/components/documents/JointbarInspectionDocument.tsx @@ -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 ( +
+ {/* 헤더 */} +
+
+
KD
+
경동기업
KYUNGDONG COMPANY
+
+
+
조인트바
+
중간검사 성적서
+
+ + + + + + + + + + + + + + + + + + + +
+
+ +
+
작성검토승인
{data.approvers.writer}{data.approvers.reviewer}{data.approvers.approver}
판매/전진생산품질
+
+ + {/* 기본 정보 테이블 */} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
품 명{data.productName}제품 LOT NO{data.lotNo}
규 격{data.specification}로트크기{data.lotSize}
발주처{data.client}검사일자{data.inspectionDate}
현장명{data.siteName}검사자{data.inspector}
+ + {/* 중간검사 기준서 */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
중간검사
기준서
도해검사항목검사기준검사방법검사주기관련규정
+ {/* 도해 이미지 영역 */} +
+
+
+ 1 +
+
+ 조인 +
+
+ 2 +
+
+
+
가공상태{data.standardInfo.appearance.criteria}{data.standardInfo.appearance.frequency}{data.standardInfo.appearance.regulation}
{data.standardInfo.assembly.criteria}{data.standardInfo.assembly.method}{data.standardInfo.assembly.regulation}
{data.standardInfo.coating.criteria}{data.standardInfo.coating.regulation}
치수
(mm)
{data.standardInfo.dimensions.criteria}{data.standardInfo.dimensions.method}{data.standardInfo.dimensions.regulation}
+ + {/* 중간검사 DATA */} +
중간검사 DATA
+ + + + + + + + + + + + + + + + + + + + + + + {data.inspectionData.map((item, index) => ( + + + + + + + + + + + + + + + ))} + +
일련
번호
검사항치수 [mm]판정
가공상태조립상태⓪ 높이
기준치
측정값⓪ 높이
기준치
측정값⓪ 길이 (밴드마감재)
기준치
측정값⓪ 간격
기준치
측정값
{item.serialNo} + ☐ 양호 ☐ 불량 + + ☐ 양호 ☐ 불량 + {item.height.standard} ± 1{item.height.measured}{item.height2.standard} ± 1{item.height2.measured}{item.bandLength.standard} ± 4{item.bandLength.measured}{item.gap.standard} ± 4{item.gap.measured} + ☐ 적합 ☐ 부
적합 +
+ + {/* 부적합 내용 */} +
+
【부적합 내용】
+
{data.notes}
+
+ + {/* 문서번호 및 종합판정 */} +
+
{data.documentNo}
+
+
종합판정
+
+ {data.overallResult} +
+
+
KDPS-10-03
+
+
+ ); +}; diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/components/documents/ProductInspectionDocument.tsx b/src/app/[locale]/(protected)/dev/quality-inspection/components/documents/ProductInspectionDocument.tsx new file mode 100644 index 00000000..ef83ccf2 --- /dev/null +++ b/src/app/[locale]/(protected)/dev/quality-inspection/components/documents/ProductInspectionDocument.tsx @@ -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 ( +
+ {/* 헤더 */} +
+
+
KD
+
경동기업
KYUNGDONG COMPANY
+
+
제 품 검 사 성 적 서
+ + + + + + + + + + + + + + + + + + + +
+
+ +
+
작성검토승인
{data.approvers.writer}{data.approvers.reviewer}{data.approvers.approver}
판매/전진생산품질
+
+ + {/* 기본 정보 테이블 */} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
납품명{data.deliveryName}제품 LOT NO{data.lotNo}
제품명{data.productName}로트크기{data.lotSize}
발주처{data.client}검사일자{data.inspectionDate}
현장명{data.siteName}검사자{data.inspector}
+ + {/* 제품사진 및 검사항목 */} +
+ {/* 제품사진 영역 */} +
+
+ {data.productImage ? ( + 제품 사진 + ) : ( +
+
제품사진
+
마이크로 폼 긴 수 제품
188 128 158 화면
+
+ )} +
+
+ + {/* 검사기준 영역 */} +
+

(하단참조)

+

하단참조

+
+
+ + {/* 검사 항목 테이블 */} + + + + + + + + + + + + + {data.inspectionItems.map((item) => ( + + + + + + + + + ))} + +
No검사항목검사기준검사방법검사주기판정
{item.id}{item.name}{item.criteria}{item.method}{item.frequency} + {item.result && ( + + ☑ {item.result === '적합' ? '적합' : '부적합'} ☐ {item.result === '적합' ? '부적합' : '적합'} + + )} +
+ + {/* 특기사항 */} +
+
【특기사항】
+
{data.notes}
+
+ + {/* 문서번호 및 종합판정 */} +
+
{data.documentNo}
+
+
종합판정
+
+ {data.overallResult} +
+
+
서식관리기준
+
+
+ ); +}; \ No newline at end of file diff --git a/src/app/[locale]/(protected)/dev/quality-inspection/components/documents/QualityDocumentUploader.tsx b/src/app/[locale]/(protected)/dev/quality-inspection/components/documents/QualityDocumentUploader.tsx new file mode 100644 index 00000000..415daf98 --- /dev/null +++ b/src/app/[locale]/(protected)/dev/quality-inspection/components/documents/QualityDocumentUploader.tsx @@ -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(null); + const [previewUrl, setPreviewUrl] = useState(null); + const [error, setError] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const [showPreview, setShowPreview] = useState(false); + const fileInputRef = useRef(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) => { + 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 ( +
+ {/* 숨김 파일 입력 */} + + + {currentFile ? ( + // 파일이 있는 경우: 정보 표시 + 미리보기 +
+ {/* 파일 정보 헤더 */} +
+
+
+ +
+
+

+ {selectedFile?.name || existingFile?.name} +

+

+ {formatFileSize(selectedFile?.size || existingFile?.size)} + {existingFile?.uploadedAt && ` • ${existingFile.uploadedAt}`} +

+
+
+
+ + + + +
+
+ + {/* PDF 미리보기 영역 */} + {showPreview && currentPreviewUrl ? ( +
+