From 742c0ba03ec542282755d45130d7dd378db6b2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Fri, 13 Mar 2026 10:14:45 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[qms]=20=EC=9E=91=EC=97=85=EC=9D=BC?= =?UTF-8?q?=EC=A7=80/=EC=A0=9C=ED=92=88=EA=B2=80=EC=82=AC=20=EB=8F=85?= =?UTF-8?q?=EB=A6=BD=20=EB=AA=A8=EB=8B=AC=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WorkLogModal: workOrderId로 공정별 작업일지 표시 - ProductInspectionViewModal: locationId로 FQC/레거시 검사 성적서 표시 - QMS 등 외부에서 재사용 가능한 독립 구조 --- .../WorkOrders/documents/WorkLogModal.tsx | 99 ++++++++++ .../ProductInspectionViewModal.tsx | 186 ++++++++++++++++++ 2 files changed, 285 insertions(+) create mode 100644 src/components/production/WorkOrders/documents/WorkLogModal.tsx create mode 100644 src/components/quality/InspectionManagement/ProductInspectionViewModal.tsx diff --git a/src/components/production/WorkOrders/documents/WorkLogModal.tsx b/src/components/production/WorkOrders/documents/WorkLogModal.tsx new file mode 100644 index 00000000..405900b7 --- /dev/null +++ b/src/components/production/WorkOrders/documents/WorkLogModal.tsx @@ -0,0 +1,99 @@ +'use client'; + +/** + * 작업일지 모달 (독립형) + * + * workOrderId만 전달하면 WorkOrder를 로드하고 공정별 작업일지를 표시. + * QMS 등 외부에서 재사용 가능. + */ + +import { useState, useEffect } from 'react'; +import { Loader2 } from 'lucide-react'; +import { DocumentViewer } from '@/components/document-system'; +import { getWorkOrderById } from '../actions'; +import type { WorkOrder, ProcessType } from '../types'; +import { ScreenWorkLogContent } from './ScreenWorkLogContent'; +import { SlatWorkLogContent } from './SlatWorkLogContent'; +import { BendingWorkLogContent } from './BendingWorkLogContent'; + +const PROCESS_LABELS: Record = { + screen: '스크린', + slat: '슬랫', + bending: '절곡', +}; + +interface WorkLogModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + workOrderId: string | null; + processType?: ProcessType; +} + +export function WorkLogModal({ + open, + onOpenChange, + workOrderId, + processType = 'screen', +}: WorkLogModalProps) { + const [order, setOrder] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (open && workOrderId) { + setIsLoading(true); + setError(null); + getWorkOrderById(workOrderId) + .then((result) => { + if (result.success && result.data) { + setOrder(result.data); + } else { + setError(result.error || '데이터를 불러올 수 없습니다.'); + } + }) + .catch(() => setError('서버 오류가 발생했습니다.')) + .finally(() => setIsLoading(false)); + } else if (!open) { + setOrder(null); + setError(null); + } + }, [open, workOrderId]); + + if (!workOrderId) return null; + + const processLabel = PROCESS_LABELS[processType] || '스크린'; + + const renderContent = () => { + if (!order) return null; + switch (processType) { + case 'slat': + return ; + case 'bending': + return ; + default: + return ; + } + }; + + return ( + + {isLoading ? ( +
+ +
+ ) : error || !order ? ( +
+

{error || '데이터를 불러올 수 없습니다.'}

+
+ ) : ( + renderContent() + )} +
+ ); +} \ No newline at end of file diff --git a/src/components/quality/InspectionManagement/ProductInspectionViewModal.tsx b/src/components/quality/InspectionManagement/ProductInspectionViewModal.tsx new file mode 100644 index 00000000..afa03f46 --- /dev/null +++ b/src/components/quality/InspectionManagement/ProductInspectionViewModal.tsx @@ -0,0 +1,186 @@ +'use client'; + +/** + * 제품검사 성적서 조회 모달 (독립형, 읽기전용) + * + * locationId(quality_document_order_locations.id)만 전달하면 + * FQC 문서 또는 레거시 inspection_data를 로드하여 표시. + * QMS 등 외부에서 재사용 가능. + */ + +import { useState, useEffect } from 'react'; +import { Loader2, AlertCircle } from 'lucide-react'; +import { DocumentViewer } from '@/components/document-system'; +import { FqcDocumentContent } from './documents/FqcDocumentContent'; +import { InspectionReportDocument } from './documents/InspectionReportDocument'; +import { mockReportInspectionItems, mapInspectionDataToItems } from './mockData'; +import type { FqcTemplate, FqcDocumentData } from './fqcActions'; +import type { InspectionReportDocument as InspectionReportDocumentType, ProductInspectionData } from './types'; + +interface ProductInspectionViewModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + /** quality_document_order_locations.id */ + locationId: string | null; + /** API 호출 함수 (DI — QMS actions에서 주입) */ + fetchDetail: (type: string, id: string) => Promise<{ success: boolean; data?: unknown; error?: string }>; +} + +// FQC 문서 API 응답 → FqcTemplate 변환 +function transformFqcApiToTemplate(apiTemplate: Record): FqcTemplate { + const t = apiTemplate as { + id: number; name: string; category: string; title: string | null; + approval_lines: { id: number; name: string; department: string; sort_order: number }[]; + basic_fields: { id: number; label: string; field_key: string; field_type: string; default_value: string | null; is_required: boolean; sort_order: number }[]; + sections: { id: number; name: string; title: string | null; description: string | null; image_path: string | null; sort_order: number; + items: { id: number; section_id: number; item_name: string; standard: string | null; tolerance: string | null; measurement_type: string; frequency: string; sort_order: number; category: string; method: string }[]; + }[]; + columns: { id: number; label: string; column_type: string; width: string | null; group_name: string | null; sort_order: number }[]; + }; + return { + id: t.id, name: t.name, category: t.category, title: t.title, + approvalLines: (t.approval_lines || []).map(a => ({ id: a.id, name: a.name, department: a.department, sortOrder: a.sort_order })), + basicFields: (t.basic_fields || []).map(f => ({ id: f.id, label: f.label, fieldKey: f.field_key, fieldType: f.field_type, defaultValue: f.default_value, isRequired: f.is_required, sortOrder: f.sort_order })), + sections: (t.sections || []).map(s => ({ + id: s.id, name: s.name, title: s.title, description: s.description, imagePath: s.image_path, sortOrder: s.sort_order, + items: (s.items || []).map(i => ({ id: i.id, sectionId: i.section_id, itemName: i.item_name, standard: i.standard, tolerance: i.tolerance, measurementType: i.measurement_type, frequency: i.frequency, sortOrder: i.sort_order, category: i.category || '', method: i.method || '' })), + })), + columns: (t.columns || []).map(c => ({ id: c.id, label: c.label, columnType: c.column_type, width: c.width, groupName: c.group_name ?? null, sortOrder: c.sort_order })), + }; +} + +function transformFqcApiToData(apiData: { section_id: number | null; column_id: number | null; row_index: number; field_key: string; field_value: string | null }[]): FqcDocumentData[] { + return (apiData || []).map(d => ({ sectionId: d.section_id, columnId: d.column_id, rowIndex: d.row_index, fieldKey: d.field_key, fieldValue: d.field_value })); +} + +export function ProductInspectionViewModal({ + open, + onOpenChange, + locationId, + fetchDetail, +}: ProductInspectionViewModalProps) { + const [fqcTemplate, setFqcTemplate] = useState(null); + const [fqcData, setFqcData] = useState([]); + const [fqcDocumentNo, setFqcDocumentNo] = useState(''); + const [legacyReportData, setLegacyReportData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (open && locationId) { + loadDocument(locationId); + } else if (!open) { + setFqcTemplate(null); + setFqcData([]); + setFqcDocumentNo(''); + setLegacyReportData(null); + setError(null); + } + }, [open, locationId]); + + const loadDocument = async (locId: string) => { + setIsLoading(true); + setError(null); + setLegacyReportData(null); + + try { + const result = await fetchDetail('product', locId); + if (result.success && result.data) { + const data = result.data as { + document_id: number | null; + inspection_status: string | null; + inspection_data: ProductInspectionData | null; + floor_code: string | null; + symbol_code: string | null; + fqc_document?: { + document_no: string; + template: Record; + data: { section_id: number | null; column_id: number | null; row_index: number; field_key: string; field_value: string | null }[]; + }; + }; + + if (data.fqc_document) { + setFqcTemplate(transformFqcApiToTemplate(data.fqc_document.template)); + setFqcData(transformFqcApiToData(data.fqc_document.data)); + setFqcDocumentNo(data.fqc_document.document_no || ''); + } else if (data.inspection_data && data.inspection_status === 'completed') { + const inspData = data.inspection_data; + const mappedItems = mapInspectionDataToItems(mockReportInspectionItems, inspData); + const locationLabel = [data.floor_code, data.symbol_code].filter(Boolean).join(' '); + + setLegacyReportData({ + documentNumber: '', + createdDate: '', + approvalLine: [ + { role: '작성', name: '', department: '' }, + { role: '승인', name: '', department: '' }, + ], + productName: inspData.productName || '', + productLotNo: '', + productCode: '', + lotSize: '1', + client: '', + inspectionDate: '', + siteName: locationLabel, + inspector: '', + productImages: inspData.productImages || [], + inspectionItems: mappedItems, + specialNotes: inspData.specialNotes || '', + finalJudgment: '합격', + }); + } else { + setError('제품검사 성적서 문서가 아직 생성되지 않았습니다.'); + } + } else { + setError(result.error || '제품검사 성적서 조회에 실패했습니다.'); + } + } catch (err) { + console.error('[ProductInspectionViewModal] loadDocument error:', err); + setError('제품검사 성적서 로드 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + }; + + if (!locationId) return null; + + const renderContent = () => { + if (fqcTemplate) { + return ( + + ); + } + if (legacyReportData) { + return ; + } + return null; + }; + + return ( + + {isLoading ? ( +
+ +
+ ) : error ? ( +
+ +

{error}

+
+ ) : ( + renderContent() + )} +
+ ); +}