From 8250eaf2b5174898a54dcad6be55adb5d2b7857b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Fri, 6 Mar 2026 20:59:25 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[=EB=AC=B8=EC=84=9C=EC=8A=A4=EB=83=85?= =?UTF-8?q?=EC=83=B7]=20Lazy=20Snapshot=20-=20=EC=A4=91=EA=B0=84=EA=B2=80?= =?UTF-8?q?=EC=82=AC/=EC=9E=91=EC=97=85=EC=9D=BC=EC=A7=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8B=9C=20=EC=9E=90=EB=8F=99=20=EC=8A=A4=EB=83=85?= =?UTF-8?q?=EC=83=B7=20=EC=BA=A1=EC=B2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - patchDocumentSnapshot() 서버 액션 추가 - InspectionReportModal: resolve 응답의 snapshot_document_id 기반 Lazy Snapshot - WorkLogModal: getWorkLog으로 문서 확인 후 Lazy Snapshot - 동작: rendered_html NULL → 500ms 후 innerHTML 캡처 → 백그라운드 PATCH --- .../production/WorkOrders/actions.ts | 28 +++++++++++++++ .../documents/InspectionReportModal.tsx | 29 ++++++++++++++- .../production/WorkerScreen/WorkLogModal.tsx | 35 +++++++++++++++++-- 3 files changed, 88 insertions(+), 4 deletions(-) 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;