fix: [문서스냅샷] 캡처 방식 보정 - 오프스크린 성적서 렌더링, readOnly 자동 캡처 제거
- ImportInspectionInputModal: 입력폼 캡처 → 오프스크린 성적서 문서 렌더링으로 변경 - InspectionReportModal: readOnly 자동 캡처 useEffect 제거 (불필요 PUT 방지) - capture-rendered-html.tsx: 오프스크린 렌더링 유틸리티 신규 추가
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
* - saveInspectionData()로 저장
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -35,6 +35,9 @@ import {
|
||||
type InspectionTemplateResponse,
|
||||
type DocumentResolveResponse,
|
||||
} from './actions';
|
||||
import { captureRenderedHtml } from '@/lib/utils/capture-rendered-html';
|
||||
import { ImportInspectionDocument } from '@/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument';
|
||||
import type { ImportInspectionTemplate, InspectionItemValue } from '@/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument';
|
||||
|
||||
// ===== Props =====
|
||||
interface ImportInspectionInputModalProps {
|
||||
@@ -216,9 +219,6 @@ export function ImportInspectionInputModal({
|
||||
receivingId,
|
||||
onSave,
|
||||
}: ImportInspectionInputModalProps) {
|
||||
// HTML 스냅샷 캡처 ref (MNG 출력용)
|
||||
const contentWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Template
|
||||
const [template, setTemplate] = useState<InspectionTemplateResponse | null>(null);
|
||||
const [resolveData, setResolveData] = useState<DocumentResolveResponse | null>(null);
|
||||
@@ -639,8 +639,35 @@ export function ImportInspectionInputModal({
|
||||
})),
|
||||
];
|
||||
|
||||
// 4. HTML 스냅샷 캡처 (MNG 출력용)
|
||||
const renderedHtml = contentWrapperRef.current?.innerHTML || undefined;
|
||||
// 4. 성적서 문서를 오프스크린 렌더링하여 HTML 스냅샷 캡처 (MNG 출력용)
|
||||
let renderedHtml: string | undefined;
|
||||
try {
|
||||
// 현재 입력값을 ImportInspectionDocument의 initialValues 형식으로 변환
|
||||
const docValues: InspectionItemValue[] = template.inspectionItems
|
||||
.filter(i => i.isFirstInItem !== false)
|
||||
.map(item => ({
|
||||
itemId: item.id,
|
||||
measurements: Array.from({ length: item.measurementCount }, (_, n) => {
|
||||
if (item.measurementType === 'okng') {
|
||||
const v = okngValues[item.id]?.[n];
|
||||
return v === 'ok' ? ('OK' as const) : v === 'ng' ? ('NG' as const) : null;
|
||||
}
|
||||
const v = measurements[item.id]?.[n];
|
||||
return v ? Number(v) : null;
|
||||
}),
|
||||
result: getItemResult(item) === 'ok' ? ('OK' as const) : getItemResult(item) === 'ng' ? ('NG' as const) : null,
|
||||
}));
|
||||
// 성적서 문서 컴포넌트를 오프스크린에서 렌더링
|
||||
renderedHtml = captureRenderedHtml(
|
||||
<ImportInspectionDocument
|
||||
template={template as unknown as ImportInspectionTemplate}
|
||||
initialValues={docValues}
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
} catch {
|
||||
// 캡처 실패 시 무시 — rendered_html 없이 저장 진행
|
||||
}
|
||||
|
||||
// 5. 저장 API 호출
|
||||
const result = await saveInspectionData({
|
||||
@@ -741,7 +768,7 @@ export function ImportInspectionInputModal({
|
||||
검사 템플릿을 불러올 수 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div ref={contentWrapperRef} className="flex-1 min-h-0 overflow-y-auto px-5 py-4 space-y-5">
|
||||
<div className="flex-1 min-h-0 overflow-y-auto px-5 py-4 space-y-5">
|
||||
{/* 기본 정보 (읽기전용 인풋 스타일) */}
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
|
||||
@@ -469,21 +469,6 @@ export function InspectionReportModal({
|
||||
}
|
||||
};
|
||||
|
||||
// readOnly 모드에서도 콘텐츠 렌더 후 rendered_html 자동 캡처 (MNG 출력용)
|
||||
useEffect(() => {
|
||||
if (!open || !order || isLoading || !readOnly || !workOrderId) return;
|
||||
// 렌더링 완료 후 캡처
|
||||
const timer = setTimeout(() => {
|
||||
const html = contentWrapperRef.current?.innerHTML;
|
||||
if (html && html.length > 100) {
|
||||
saveInspectionDocument(workOrderId, { rendered_html: html }).catch(() => {
|
||||
// 자동 캡처 실패는 무시 (template 미연결 시 404 가능)
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [open, order, isLoading, readOnly, workOrderId]);
|
||||
|
||||
// 결재 상신 가능 여부: 저장된 DRAFT 문서가 있을 때
|
||||
const canSubmitForApproval = savedDocumentId != null && savedDocumentStatus === 'DRAFT';
|
||||
|
||||
|
||||
36
src/lib/utils/capture-rendered-html.tsx
Normal file
36
src/lib/utils/capture-rendered-html.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 오프스크린 렌더링으로 React 컴포넌트의 HTML을 캡처하는 유틸리티
|
||||
*
|
||||
* 사용 사례: 입력 화면에서 저장 시, 해당 데이터의 "문서 뷰" HTML을 캡처하여
|
||||
* rendered_html로 저장 (MNG 출력용)
|
||||
*/
|
||||
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { flushSync } from 'react-dom';
|
||||
|
||||
/**
|
||||
* React 엘리먼트를 오프스크린에서 렌더링하고 innerHTML을 캡처합니다.
|
||||
*
|
||||
* @param element - 렌더링할 React 엘리먼트 (예: <ImportInspectionDocument template={...} readOnly />)
|
||||
* @returns 캡처된 HTML 문자열, 실패 시 undefined
|
||||
*/
|
||||
export function captureRenderedHtml(element: React.ReactElement): string | undefined {
|
||||
try {
|
||||
const container = document.createElement('div');
|
||||
container.style.cssText = 'position:fixed;left:-9999px;top:0;visibility:hidden;width:210mm;';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const root = createRoot(container);
|
||||
flushSync(() => {
|
||||
root.render(element);
|
||||
});
|
||||
|
||||
const html = container.innerHTML;
|
||||
root.unmount();
|
||||
document.body.removeChild(container);
|
||||
|
||||
return html && html.length > 50 ? html : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user