feat: [문서스냅샷] Lazy Snapshot - 중간검사/작업일지 조회 시 자동 스냅샷 캡처

- patchDocumentSnapshot() 서버 액션 추가
- InspectionReportModal: resolve 응답의 snapshot_document_id 기반 Lazy Snapshot
- WorkLogModal: getWorkLog으로 문서 확인 후 Lazy Snapshot
- 동작: rendered_html NULL → 500ms 후 innerHTML 캡처 → 백그라운드 PATCH
This commit is contained in:
2026-03-06 20:59:25 +09:00
parent 72a2a3e9a9
commit 8250eaf2b5
3 changed files with 88 additions and 4 deletions

View File

@@ -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

View File

@@ -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<number | null>(null);
const [savedDocumentStatus, setSavedDocumentStatus] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
// Lazy Snapshot 대상 문서 ID (rendered_html이 없는 문서)
const [snapshotDocumentId, setSnapshotDocumentId] = useState<number | null>(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<string, unknown>).existing_document as
const resolveData = resolveResult.data as Record<string, unknown>;
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;

View File

@@ -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<string | null>(null);
const contentWrapperRef = useRef<HTMLDivElement>(null);
// Lazy Snapshot 대상 문서 ID
const [snapshotDocumentId, setSnapshotDocumentId] = useState<number | null>(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;