feat: [qms] 작업일지/제품검사 독립 모달 컴포넌트

- WorkLogModal: workOrderId로 공정별 작업일지 표시
- ProductInspectionViewModal: locationId로 FQC/레거시 검사 성적서 표시
- QMS 등 외부에서 재사용 가능한 독립 구조
This commit is contained in:
2026-03-13 10:14:45 +09:00
parent 8d33fafb48
commit 742c0ba03e
2 changed files with 285 additions and 0 deletions

View File

@@ -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>
);
}

View File

@@ -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>
);
}