feat: [qms] 작업일지/제품검사 독립 모달 컴포넌트
- WorkLogModal: workOrderId로 공정별 작업일지 표시 - ProductInspectionViewModal: locationId로 FQC/레거시 검사 성적서 표시 - QMS 등 외부에서 재사용 가능한 독립 구조
This commit is contained in:
@@ -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<string, string> = {
|
||||
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<WorkOrder | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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 <SlatWorkLogContent data={order} />;
|
||||
case 'bending':
|
||||
return <BendingWorkLogContent data={order} />;
|
||||
default:
|
||||
return <ScreenWorkLogContent data={order} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DocumentViewer
|
||||
title="작업일지"
|
||||
subtitle={`${processLabel} 생산부서`}
|
||||
preset="inspection"
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-64 bg-white">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : error || !order ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 gap-4 bg-white">
|
||||
<p className="text-muted-foreground">{error || '데이터를 불러올 수 없습니다.'}</p>
|
||||
</div>
|
||||
) : (
|
||||
renderContent()
|
||||
)}
|
||||
</DocumentViewer>
|
||||
);
|
||||
}
|
||||
@@ -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<string, unknown>): 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<FqcTemplate | null>(null);
|
||||
const [fqcData, setFqcData] = useState<FqcDocumentData[]>([]);
|
||||
const [fqcDocumentNo, setFqcDocumentNo] = useState('');
|
||||
const [legacyReportData, setLegacyReportData] = useState<InspectionReportDocumentType | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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<string, unknown>;
|
||||
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 (
|
||||
<FqcDocumentContent
|
||||
template={fqcTemplate}
|
||||
documentData={fqcData}
|
||||
documentNo={fqcDocumentNo}
|
||||
readonly
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (legacyReportData) {
|
||||
return <InspectionReportDocument data={legacyReportData} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<DocumentViewer
|
||||
title="제품검사 성적서"
|
||||
subtitle={fqcDocumentNo || undefined}
|
||||
preset="inspection"
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-64 bg-white">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 gap-4 bg-white">
|
||||
<AlertCircle className="w-12 h-12 text-red-500" />
|
||||
<p className="text-muted-foreground">{error}</p>
|
||||
</div>
|
||||
) : (
|
||||
renderContent()
|
||||
)}
|
||||
</DocumentViewer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user