diff --git a/src/components/production/WorkOrders/actions.ts b/src/components/production/WorkOrders/actions.ts index 4549f4b8..a41d8c23 100644 --- a/src/components/production/WorkOrders/actions.ts +++ b/src/components/production/WorkOrders/actions.ts @@ -922,6 +922,34 @@ export async function resolveInspectionDocument( } } +// ===== 문서 스냅샷 저장 (Lazy Snapshot) ===== +export async function patchDocumentSnapshot( + documentId: number, + renderedHtml: string +): Promise<{ success: boolean; error?: string }> { + try { + const { response, error } = await serverFetch( + buildApiUrl(`/api/v1/documents/${documentId}/snapshot`), + { method: 'PATCH', body: JSON.stringify({ rendered_html: renderedHtml }) } + ); + + if (error || !response) { + return { success: false, error: error?.message || 'API 요청 실패' }; + } + + const result = await response.json(); + if (!response.ok || !result.success) { + return { success: false, error: result.message || '스냅샷 저장에 실패했습니다.' }; + } + + return { success: true }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] patchDocumentSnapshot error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + // ===== 문서 결재 상신 ===== export async function submitDocumentForApproval( documentId: number diff --git a/src/components/production/WorkOrders/documents/InspectionReportModal.tsx b/src/components/production/WorkOrders/documents/InspectionReportModal.tsx index 8593cf84..20009f81 100644 --- a/src/components/production/WorkOrders/documents/InspectionReportModal.tsx +++ b/src/components/production/WorkOrders/documents/InspectionReportModal.tsx @@ -24,6 +24,7 @@ import { saveInspectionDocument, resolveInspectionDocument, submitDocumentForApproval, + patchDocumentSnapshot, } from '../actions'; import type { WorkOrder, ProcessType } from '../types'; import type { InspectionReportData, InspectionReportNodeGroup } from '../actions'; @@ -184,6 +185,8 @@ export function InspectionReportModal({ const [savedDocumentId, setSavedDocumentId] = useState(null); const [savedDocumentStatus, setSavedDocumentStatus] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); + // Lazy Snapshot 대상 문서 ID (rendered_html이 없는 문서) + const [snapshotDocumentId, setSnapshotDocumentId] = useState(null); // props에서 목업 제외한 실제 개소만 사용 (WorkerScreen에서 apiItems + mockItems가 합쳐져 전달됨) // ★ 반드시 workItems와 inspectionDataMap을 같은 소스에서 가져와야 key 포맷이 일치함 @@ -297,7 +300,8 @@ export function InspectionReportModal({ // 4) 기존 문서의 document_data EAV 레코드 + ID/상태 추출 if (resolveResult?.success && resolveResult.data) { - const existingDoc = (resolveResult.data as Record).existing_document as + const resolveData = resolveResult.data as Record; + const existingDoc = resolveData.existing_document as | { id?: number; status?: string; data?: Array<{ section_id: number | null; column_id: number | null; row_index: number; field_key: string; field_value: string | null }> } | null; if (existingDoc?.data && existingDoc.data.length > 0) { @@ -308,10 +312,13 @@ export function InspectionReportModal({ // 문서 ID/상태 저장 (결재 상신용) setSavedDocumentId(existingDoc?.id ?? null); setSavedDocumentStatus(existingDoc?.status ?? null); + // Lazy Snapshot 대상 문서 ID + setSnapshotDocumentId((resolveData.snapshot_document_id as number) ?? null); } else { setDocumentRecords(null); setSavedDocumentId(null); setSavedDocumentStatus(null); + setSnapshotDocumentId(null); } }) .catch(() => { @@ -329,10 +336,30 @@ export function InspectionReportModal({ setDocumentRecords(null); setSavedDocumentId(null); setSavedDocumentStatus(null); + setSnapshotDocumentId(null); setError(null); } }, [open, workOrderId, processType, templateData]); + // 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); // DOM 렌더링 완료 대기 + + return () => clearTimeout(timer); + }, [snapshotDocumentId, isLoading, order]); + // 템플릿 결정: prop 우선, 없으면 자체 로딩 결과 사용 const resolvedTemplateData = templateData || selfTemplateData; const activeTemplate = resolvedTemplateData?.has_template ? resolvedTemplateData.template : null; diff --git a/src/components/production/WorkerScreen/WorkLogModal.tsx b/src/components/production/WorkerScreen/WorkLogModal.tsx index 0c90d77c..6723d4a7 100644 --- a/src/components/production/WorkerScreen/WorkLogModal.tsx +++ b/src/components/production/WorkerScreen/WorkLogModal.tsx @@ -16,8 +16,8 @@ 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 } from '../WorkOrders/actions'; -import { saveWorkLog } from './actions'; +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'; @@ -64,6 +64,8 @@ export function WorkLogModal({ 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 => ({ @@ -116,8 +118,9 @@ export function WorkLogModal({ Promise.all([ getWorkOrderById(workOrderId), getMaterialInputLots(workOrderId), + getWorkLog(workOrderId), ]) - .then(([orderResult, lotsResult]) => { + .then(([orderResult, lotsResult, workLogResult]) => { if (orderResult.success && orderResult.data) { setOrder(orderResult.data); } else { @@ -126,6 +129,13 @@ export function WorkLogModal({ 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('서버 오류가 발생했습니다.'); @@ -137,10 +147,29 @@ export function WorkLogModal({ // 모달 닫힐 때 상태 초기화 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;