fix: [QMS] 제품검사 성적서 렌더링 개선 (FQC + inspection_data fallback)

- InspectionModal: FQC 문서 없을 때 inspection_data JSON으로 레거시 리포트 렌더링
- InspectionReportDocument 컴포넌트 재활용 (기존 검사 페이지와 동일 포맷)
- mockData: convertJudgment, mapInspectionDataToItems export 추가
This commit is contained in:
2026-03-12 09:48:32 +09:00
parent b7f7aad2fd
commit 86383719ec
2 changed files with 228 additions and 37 deletions

View File

@@ -6,6 +6,7 @@ import { DocumentViewer } from '@/components/document-system';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { Document, DocumentItem } from '../types';
import { getDocumentDetail } from '../actions';
import { MOCK_SHIPMENT_DETAIL } from '../mockData';
// 기존 문서 컴포넌트 import
@@ -24,10 +25,12 @@ import {
QualityDocumentUploader,
} from './documents';
// 제품검사 성적서 (신규 양식) import
// 제품검사 성적서 (FQC 양식) import
import { FqcDocumentContent } from '@/components/quality/InspectionManagement/documents/FqcDocumentContent';
import { InspectionReportDocument } from '@/components/quality/InspectionManagement/documents/InspectionReportDocument';
import { mockReportInspectionItems } from '@/components/quality/InspectionManagement/mockData';
import type { InspectionReportDocument as InspectionReportDocumentType } from '@/components/quality/InspectionManagement/types';
import type { FqcTemplate, FqcDocumentData } from '@/components/quality/InspectionManagement/fqcActions';
import type { InspectionReportDocument as InspectionReportDocumentType, ProductInspectionData } from '@/components/quality/InspectionManagement/types';
import { mockReportInspectionItems, mapInspectionDataToItems } from '@/components/quality/InspectionManagement/mockData';
import type { ImportInspectionTemplate, ImportInspectionRef, InspectionItemValue } from './documents/ImportInspectionDocument';
// 작업일지 + 중간검사 성적서 문서 컴포넌트 import (공정별 신규 버전)
@@ -44,6 +47,9 @@ import type { WorkOrder } from '@/components/production/WorkOrders/types';
// 검사 템플릿 API
import { getInspectionTemplate } from '@/components/material/ReceivingManagement/actions';
// 작업지시 상세 API (QMS 작업일지/중간검사용)
import { getWorkOrderById } from '@/components/production/WorkOrders/actions';
/**
* 저장된 document.data (field_key 기반)를 ImportInspectionDocument의 initialValues로 변환
*
@@ -160,26 +166,32 @@ const QMS_MOCK_ORDER_ITEMS: OrderItem[] = [
{ id: 'bf-1', itemCode: 'BF-001', itemName: '하단마감재', specification: 'EGI 1.5ST', type: '하단마감재', quantity: 8, unit: 'EA', unitPrice: 18000, supplyAmount: 144000, taxAmount: 14400, totalAmount: 158400, sortOrder: 5 },
];
// QMS용 제품검사 성적서 Mock 데이터
const QMS_MOCK_REPORT_DATA: InspectionReportDocumentType = {
documentNumber: 'RPT-KD-SS-2024-530',
createdDate: '2024-09-24',
approvalLine: [
{ role: '작성', name: '김검사', department: '품질관리부' },
{ role: '승인', name: '박승인', department: '품질관리부' },
],
productName: '방화스크린',
productLotNo: 'KD-SS-240924-19',
productCode: 'WY-SC780',
lotSize: '8',
client: '삼성물산(주)',
inspectionDate: '2024-09-26',
siteName: '강남 아파트 단지',
inspector: '김검사',
inspectionItems: mockReportInspectionItems,
specialNotes: '',
finalJudgment: '합격',
};
// 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 }));
}
// QMS용 작업일지 Mock WorkOrder 생성
const createQmsMockWorkOrder = (subType?: string): WorkOrder => ({
@@ -270,10 +282,24 @@ export const InspectionModal = ({
const [isLoadingTemplate, setIsLoadingTemplate] = useState(false);
const [templateError, setTemplateError] = useState<string | null>(null);
// 작업일지/중간검사용 WorkOrder 상태
const [workOrderData, setWorkOrderData] = useState<WorkOrder | null>(null);
const [isLoadingWorkOrder, setIsLoadingWorkOrder] = useState(false);
const [workOrderError, setWorkOrderError] = useState<string | null>(null);
// 수입검사 저장용 ref/상태
const importDocRef = useRef<ImportInspectionRef>(null);
const [isSaving, setIsSaving] = useState(false);
// 제품검사 성적서 FQC 상태
const [fqcTemplate, setFqcTemplate] = useState<FqcTemplate | null>(null);
const [fqcData, setFqcData] = useState<FqcDocumentData[]>([]);
const [fqcDocumentNo, setFqcDocumentNo] = useState<string>('');
const [isLoadingFqc, setIsLoadingFqc] = useState(false);
const [fqcError, setFqcError] = useState<string | null>(null);
// 레거시 inspection_data 기반 제품검사 성적서
const [legacyReportData, setLegacyReportData] = useState<InspectionReportDocumentType | null>(null);
// 수입검사 템플릿 로드 (모달 열릴 때)
useEffect(() => {
// itemId가 있으면 실제 API로 조회, 없으면 itemName/specification으로 mock 조회
@@ -285,9 +311,53 @@ export const InspectionModal = ({
setImportTemplate(null);
setImportInitialValues(undefined);
setTemplateError(null);
setFqcTemplate(null);
setFqcData([]);
setFqcDocumentNo('');
setFqcError(null);
setLegacyReportData(null);
setWorkOrderData(null);
setWorkOrderError(null);
}
}, [isOpen, doc?.type, itemId, itemName, specification]);
// 작업일지/중간검사 WorkOrder 로드 (모달 열릴 때)
// log: documentItem.id === work_order_id, report: documentItem.workOrderId로 전달
useEffect(() => {
if (isOpen && (doc?.type === 'log' || doc?.type === 'report')) {
const woId = documentItem?.workOrderId || (doc?.type === 'log' ? Number(documentItem?.id) : null);
if (woId) {
loadWorkOrderData(woId);
}
}
}, [isOpen, doc?.type, documentItem?.workOrderId, documentItem?.id]);
// 제품검사 성적서 FQC 로드 (모달 열릴 때)
useEffect(() => {
if (isOpen && doc?.type === 'product' && documentItem?.id) {
loadFqcDocument(documentItem.id);
}
}, [isOpen, doc?.type, documentItem?.id]);
const loadWorkOrderData = async (workOrderId: number) => {
setIsLoadingWorkOrder(true);
setWorkOrderError(null);
try {
const result = await getWorkOrderById(String(workOrderId));
if (result.success && result.data) {
setWorkOrderData(result.data);
} else {
setWorkOrderError(result.error || '작업지시 데이터를 불러올 수 없습니다.');
}
} catch (error) {
console.error('[InspectionModal] loadWorkOrderData error:', error);
setWorkOrderError('작업지시 데이터 로드 중 오류가 발생했습니다.');
} finally {
setIsLoadingWorkOrder(false);
}
};
const loadInspectionTemplate = async () => {
// itemId가 있으면 실제 API 호출, 없으면 itemName/specification 필요
if (!itemId && (!itemName || !specification)) return;
@@ -330,6 +400,73 @@ export const InspectionModal = ({
}
};
// 제품검사 성적서 문서 로드 (FQC 우선, inspection_data fallback)
const loadFqcDocument = async (locationId: string) => {
setIsLoadingFqc(true);
setFqcError(null);
setLegacyReportData(null);
try {
const result = await getDocumentDetail('product', locationId);
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) {
// FQC 문서가 있는 경우
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') {
// FQC 없지만 inspection_data가 있는 경우 → 레거시 리포트 생성
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 {
setFqcError('제품검사 성적서 문서가 아직 생성되지 않았습니다.');
}
} else {
setFqcError(result.error || '제품검사 성적서 조회에 실패했습니다.');
}
} catch (error) {
console.error('[InspectionModal] loadFqcDocument error:', error);
setFqcError('제품검사 성적서 로드 중 오류가 발생했습니다.');
} finally {
setIsLoadingFqc(false);
}
};
// 수입검사 저장 핸들러 (hooks는 early return 전에 호출해야 함)
const handleImportSave = useCallback(async () => {
if (!importDocRef.current) return;
@@ -360,39 +497,63 @@ export const InspectionModal = ({
const handleQualityFileDelete = () => {
};
// 작업일지/중간검사 공통: WorkOrder 데이터 로딩 상태 처리
const renderWorkOrderLoading = () => {
if (isLoadingWorkOrder) {
return (
<div className="bg-white shadow-sm p-16 w-full h-full rounded flex flex-col items-center justify-center text-center">
<Loader2 className="w-12 h-12 text-blue-500 animate-spin mb-4" />
<p className="text-gray-600 text-sm"> ...</p>
</div>
);
}
if (workOrderError) {
return <ErrorDocument message={workOrderError} onRetry={documentItem?.workOrderId ? () => loadWorkOrderData(documentItem.workOrderId!) : undefined} />;
}
return null;
};
// 작업일지 공정별 렌더링
const renderWorkLogDocument = () => {
const loadingEl = renderWorkOrderLoading();
if (loadingEl) return loadingEl;
const subType = documentItem?.subType;
const mockOrder = createQmsMockWorkOrder(subType);
// 실제 WorkOrder 데이터 사용, 없으면 fallback mock
const orderData = workOrderData || createQmsMockWorkOrder(subType);
switch (subType) {
case 'screen':
return <ScreenWorkLogContent data={mockOrder} />;
return <ScreenWorkLogContent data={orderData} />;
case 'slat':
return <SlatWorkLogContent data={mockOrder} />;
return <SlatWorkLogContent data={orderData} />;
case 'bending':
return <BendingWorkLogContent data={mockOrder} />;
return <BendingWorkLogContent data={orderData} />;
default:
// subType 미지정 시 스크린 기본
return <ScreenWorkLogContent data={mockOrder} />;
return <ScreenWorkLogContent data={orderData} />;
}
};
// 중간검사 성적서 서브타입에 따른 렌더링 (신규 버전 통일)
const renderReportDocument = () => {
const loadingEl = renderWorkOrderLoading();
if (loadingEl) return loadingEl;
const subType = documentItem?.subType;
const mockOrder = createQmsMockWorkOrder(subType || 'screen');
// 실제 WorkOrder 데이터 사용, 없으면 fallback mock
const orderData = workOrderData || createQmsMockWorkOrder(subType || 'screen');
switch (subType) {
case 'screen':
return <ScreenInspectionContent data={mockOrder} readOnly />;
return <ScreenInspectionContent data={orderData} readOnly />;
case 'bending':
return <BendingInspectionContent data={mockOrder} readOnly />;
return <BendingInspectionContent data={orderData} readOnly />;
case 'slat':
return <SlatInspectionContent data={mockOrder} readOnly />;
return <SlatInspectionContent data={orderData} readOnly />;
case 'jointbar':
return <JointbarInspectionDocument />;
default:
return <ScreenInspectionContent data={mockOrder} readOnly />;
return <ScreenInspectionContent data={orderData} readOnly />;
}
};
@@ -418,6 +579,36 @@ export const InspectionModal = ({
);
};
// 제품검사 성적서 렌더링 (FQC 우선, inspection_data fallback)
const renderProductDocument = () => {
if (isLoadingFqc) {
return <LoadingDocument />;
}
if (fqcError) {
return <ErrorDocument message={fqcError} onRetry={documentItem?.id ? () => loadFqcDocument(documentItem.id) : undefined} />;
}
// FQC 문서 기반 렌더링
if (fqcTemplate) {
return (
<FqcDocumentContent
template={fqcTemplate}
documentData={fqcData}
documentNo={fqcDocumentNo}
readonly
/>
);
}
// 레거시 inspection_data 기반 렌더링
if (legacyReportData) {
return <InspectionReportDocument data={legacyReportData} />;
}
return <PlaceholderDocument docType="product" docItem={documentItem} />;
};
// 문서 타입에 따른 컨텐츠 렌더링
const renderDocumentContent = () => {
switch (doc.type) {
@@ -453,7 +644,7 @@ export const InspectionModal = ({
case 'import':
return renderImportInspectionDocument();
case 'product':
return <InspectionReportDocument data={QMS_MOCK_REPORT_DATA} />;
return renderProductDocument();
case 'report':
return renderReportDocument();
case 'quality':

View File

@@ -477,14 +477,14 @@ export const mockReportInspectionItems: ReportInspectionItem[] = [
];
/** pass/fail → 적합/부적합 변환 */
const convertJudgment = (value: 'pass' | 'fail' | null): '적합' | '부적합' | undefined => {
export const convertJudgment = (value: 'pass' | 'fail' | null): '적합' | '부적합' | undefined => {
if (value === 'pass') return '적합';
if (value === 'fail') return '부적합';
return undefined;
};
/** 검사 데이터를 검사항목에 매핑 */
const mapInspectionDataToItems = (
export const mapInspectionDataToItems = (
items: ReportInspectionItem[],
inspectionData?: ProductInspectionData
): ReportInspectionItem[] => {