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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user