'use client'; /** * 작업일지 모달 * * document-system 통합 버전 (2026-01-22) * 공정별 작업일지 지원 (2026-01-29) * 공정관리 양식 매핑 연동 (2026-02-11) * - DocumentViewer 사용 * - 공정관리에서 매핑된 workLogTemplateId/Name 기반으로 콘텐츠 분기 * - 양식 미매핑 시 processType 폴백 */ import { useState, useEffect, useCallback, useRef } from 'react'; import { Loader2, Save } from 'lucide-react'; import { DocumentViewer } from '@/components/document-system'; import { Button } from '@/components/ui/button'; import { toast } from 'sonner'; import { getWorkOrderById, getMaterialInputLots, patchDocumentSnapshot } from '../WorkOrders/actions'; import { saveWorkLog, getWorkLog } from './actions'; import type { MaterialInputLot } from '../WorkOrders/actions'; import type { WorkOrder, ProcessType } from '../WorkOrders/types'; import { WorkLogContent } from './WorkLogContent'; import { ScreenWorkLogContent, SlatWorkLogContent, BendingWorkLogContent, } from '../WorkOrders/documents'; interface WorkLogModalProps { open: boolean; onOpenChange: (open: boolean) => void; workOrderId: string | null; processType?: ProcessType; /** 공정관리에서 매핑된 작업일지 양식 ID */ workLogTemplateId?: number; /** 공정관리에서 매핑된 작업일지 양식명 (예: '스크린 작업일지') */ workLogTemplateName?: string; } /** * 양식명 → 공정 타입 매핑 * 공정관리에서 매핑된 양식명을 기반으로 콘텐츠 컴포넌트를 결정 */ function resolveProcessTypeFromTemplate(templateName?: string): ProcessType | undefined { if (!templateName) return undefined; if (templateName.includes('스크린')) return 'screen'; if (templateName.includes('슬랫')) return 'slat'; if (templateName.includes('절곡')) return 'bending'; return undefined; } export function WorkLogModal({ open, onOpenChange, workOrderId, processType, workLogTemplateId, workLogTemplateName, }: WorkLogModalProps) { const [order, setOrder] = useState(null); const [materialLots, setMaterialLots] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); const contentWrapperRef = useRef(null); // Lazy Snapshot 대상 문서 ID const [snapshotDocumentId, setSnapshotDocumentId] = useState(null); // 목업 WorkOrder 생성 const createMockOrder = (id: string, pType?: ProcessType): WorkOrder => ({ id, workOrderNo: 'KD-WO-260129-01', lotNo: 'KD-SA-260129-01', processId: 1, processName: pType === 'slat' ? '슬랫' : pType === 'bending' ? '절곡' : '스크린', processCode: pType || 'screen', processType: pType || 'screen', status: 'in_progress', client: '(주)경동', projectName: '서울 강남 현장', dueDate: '2026-02-05', assignee: '홍길동', assignees: [{ id: '1', name: '홍길동', isPrimary: true }], orderDate: '2026-01-20', scheduledDate: '2026-01-29', shipmentDate: '2026-02-05', salesOrderDate: '2026-01-15', isAssigned: true, isStarted: true, priority: 3, priorityLabel: '긴급', shutterCount: 12, department: '생산부', items: [ { id: '1', no: 1, status: 'in_progress', productName: '와이어 스크린', floorCode: '1층/FSS-01', specification: '8,260 X 8,350', quantity: 2, unit: 'EA', orderNodeId: null, orderNodeName: '' }, { id: '2', no: 2, status: 'waiting', productName: '메쉬 스크린', floorCode: '2층/FSS-03', specification: '6,400 X 5,200', quantity: 4, unit: 'EA', orderNodeId: null, orderNodeName: '' }, { id: '3', no: 3, status: 'completed', productName: '광폭 와이어', floorCode: '3층/FSS-05', specification: '12,000 X 4,500', quantity: 1, unit: 'EA', orderNodeId: null, orderNodeName: '' }, ], currentStep: 2, issues: [], note: '', }); // 모달 열릴 때 데이터 fetch useEffect(() => { if (open && workOrderId) { // 목업 ID인 경우 API 호출 생략 if (workOrderId.startsWith('mock-')) { setOrder(createMockOrder(workOrderId, processType)); setError(null); return; } setIsLoading(true); setError(null); Promise.all([ getWorkOrderById(workOrderId), getMaterialInputLots(workOrderId), getWorkLog(workOrderId), ]) .then(([orderResult, lotsResult, workLogResult]) => { if (orderResult.success && orderResult.data) { setOrder(orderResult.data); } else { setError(orderResult.error || '데이터를 불러올 수 없습니다.'); } if (lotsResult.success) { setMaterialLots(lotsResult.data); } // Lazy Snapshot: 문서가 있고 rendered_html이 없으면 스냅샷 대상 if (workLogResult.success && workLogResult.data?.document) { const doc = workLogResult.data.document as { id?: number; rendered_html?: string | null }; if (doc.id && !doc.rendered_html) { setSnapshotDocumentId(doc.id); } } }) .catch(() => { setError('서버 오류가 발생했습니다.'); }) .finally(() => { setIsLoading(false); }); } else if (!open) { // 모달 닫힐 때 상태 초기화 setOrder(null); setMaterialLots([]); setSnapshotDocumentId(null); setError(null); } }, [open, workOrderId, processType]); // Lazy Snapshot: 콘텐츠 렌더링 완료 후 rendered_html이 없는 문서에 스냅샷 저장 useEffect(() => { if (!snapshotDocumentId || isLoading || !order) return; const timer = setTimeout(() => { const html = contentWrapperRef.current?.innerHTML; if (html && html.length > 50) { patchDocumentSnapshot(snapshotDocumentId, html).then((result) => { if (result.success) { setSnapshotDocumentId(null); } }); } }, 500); return () => clearTimeout(timer); }, [snapshotDocumentId, isLoading, order]); // 저장 핸들러 const handleSave = useCallback(async () => { if (!workOrderId || !order) return; setIsSaving(true); try { // 현재 아이템 데이터를 table_data로 변환 const tableData = (order.items || []).map((item) => ({ id: item.id, item_name: item.productName, specification: item.specification || item.floorCode, quantity: item.quantity, unit: item.unit || 'EA', })); // HTML 스냅샷 캡처 (MNG 출력용) const renderedHtml = contentWrapperRef.current?.innerHTML || undefined; const result = await saveWorkLog(workOrderId, { table_data: tableData, title: workLogTemplateName || '작업일지', rendered_html: renderedHtml, }); if (result.success) { toast.success('작업일지가 저장되었습니다.'); } else { toast.error(result.error || '저장에 실패했습니다.'); } } catch { toast.error('저장 중 오류가 발생했습니다.'); } finally { setIsSaving(false); } }, [workOrderId, order, workLogTemplateName]); if (!workOrderId) return null; // 로딩/에러 상태는 DocumentViewer 내부에서 처리 const subtitle = order ? `${order.processName} 생산부서` : undefined; // 양식 미매핑 안내 const renderNoTemplate = () => (

이 공정에 작업일지 양식이 매핑되지 않았습니다.

공정관리에서 작업일지 양식을 설정해주세요.

); // 공정관리 양식 매핑 기반 콘텐츠 분기 const renderContent = () => { if (!order) return null; // 1순위: 공정관리에서 매핑된 양식명으로 결정 const templateType = resolveProcessTypeFromTemplate(workLogTemplateName); // 2순위: processType 폴백 (양식 미매핑 시) const type = templateType || processType || order.processType; // 양식이 매핑되어 있지 않은 경우 안내 if (!workLogTemplateId && !processType) { return renderNoTemplate(); } switch (type) { case 'screen': return ; case 'slat': return ; case 'bending': { const lotNoMap: Record = {}; for (const lot of materialLots) { if (lot.item_code.startsWith('BD-')) { lotNoMap[lot.item_code] = lot.lot_no; } } return ; } default: return ; } }; // 양식명으로 문서 제목 결정 const documentTitle = workLogTemplateName || '작업일지'; const toolbarExtra = ( ); return ( {isLoading ? (
) : error || !order ? (

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

) : (
{renderContent()}
)}
); }