fix: [문서스냅샷] 캡처 방식 보정 - 오프스크린 성적서 렌더링, readOnly 자동 캡처 제거

- ImportInspectionInputModal: 입력폼 캡처 → 오프스크린 성적서 문서 렌더링으로 변경
- InspectionReportModal: readOnly 자동 캡처 useEffect 제거 (불필요 PUT 방지)
- capture-rendered-html.tsx: 오프스크린 렌더링 유틸리티 신규 추가
This commit is contained in:
2026-03-06 20:35:30 +09:00
parent 31f523c88f
commit 72a2a3e9a9
3 changed files with 70 additions and 22 deletions

View File

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

View File

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

View 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;
}
}