feat: 제품검사 요청서 양식 기반 렌더링 + Lazy Snapshot

- FqcRequestDocumentContent: template 66 기반 동적 렌더링 컴포넌트
  - 결재라인, 기본정보, 입력사항(4섹션), 사전고지 테이블
  - group_name 기반 3단 헤더 (오픈사이즈 발주/시공 병합)
- InspectionRequestModal: FQC 모드 전환 + EAV 문서 로드 + Lazy Snapshot
- fqcActions: getFqcRequestTemplate, patchDocumentSnapshot, description/groupName 타입
- types/actions: requestDocumentId 필드 추가 및 매핑
- InspectionDetail: requestDocumentId prop 전달
This commit is contained in:
2026-03-06 21:43:01 +09:00
parent e7263feecf
commit 295585d8b6
7 changed files with 631 additions and 7 deletions

View File

@@ -1247,6 +1247,7 @@ export function InspectionDetail({ id }: InspectionDetailProps) {
open={requestDocOpen}
onOpenChange={setRequestDocOpen}
data={inspection ? buildRequestDocumentData(inspection) : null}
requestDocumentId={inspection?.requestDocumentId}
/>
{/* 제품검사성적서 모달 */}

View File

@@ -96,6 +96,7 @@ interface ProductInspectionApi {
construction_height: number;
change_reason: string;
}>;
request_document_id: number | null;
created_at: string;
updated_at: string;
}
@@ -233,6 +234,7 @@ function transformApiToFrontend(api: ProductInspectionApi): ProductInspection {
changeReason: item.change_reason,
inspectionData: item.inspection_data || undefined,
})),
requestDocumentId: api.request_document_id ?? null,
};
}

View File

@@ -0,0 +1,461 @@
'use client';
/**
* 제품검사 요청서 - 양식 기반 동적 렌더링
*
* template (ID 66) 구조를 기반으로 렌더링:
* - approvalLines: 결재라인 (작성/승인)
* - basicFields: 기본 정보 필드 (수주처, 업체명 등)
* - sections[0-3]: 입력사항 (건축공사장, 자재유통업자, 공사시공자, 공사감리자)
* - sections[4]: 검사대상 사전 고지 정보 (description + columns 테이블)
* - columns: 사전 고지 테이블 컬럼 (8개, group_name으로 병합 헤더)
*/
import {
ConstructionApprovalTable,
DocumentWrapper,
DocumentTable,
DOC_STYLES,
} from '@/components/document-system';
import type { FqcTemplate, FqcDocumentData } from '../fqcActions';
interface FqcRequestDocumentContentProps {
template: FqcTemplate;
documentData?: FqcDocumentData[];
documentNo?: string;
createdDate?: string;
readonly?: boolean;
}
/** 라벨 셀 */
const lbl = `${DOC_STYLES.label} w-28`;
/** 서브 라벨 셀 */
const subLbl = 'bg-gray-50 px-2 py-1 font-medium border-r border-gray-300 w-28';
/** 값 셀 */
const val = DOC_STYLES.value;
/** EAV 데이터에서 field_key로 값 조회 */
function getFieldValue(
data: FqcDocumentData[] | undefined,
fieldKey: string,
): string {
if (!data) return '';
const found = data.find(d => d.fieldKey === fieldKey && d.sectionId === null);
return found?.fieldValue || '';
}
/** EAV 데이터에서 섹션 아이템 값 조회 */
function getSectionItemValue(
data: FqcDocumentData[] | undefined,
sectionId: number,
fieldKey: string,
): string {
if (!data) return '';
const found = data.find(
d => d.sectionId === sectionId && d.fieldKey === fieldKey
);
return found?.fieldValue || '';
}
/** EAV 데이터에서 테이블 행 데이터 조회 */
function getTableRows(
data: FqcDocumentData[] | undefined,
columns: FqcTemplate['columns'],
): Array<Record<string, string>> {
if (!data) return [];
// column_id가 있는 데이터만 필터 → row_index로 그룹핑
const columnData = data.filter(d => d.columnId !== null);
if (columnData.length === 0) return [];
const rowMap = new Map<number, Record<string, string>>();
for (const d of columnData) {
if (!rowMap.has(d.rowIndex)) rowMap.set(d.rowIndex, {});
const row = rowMap.get(d.rowIndex)!;
row[d.fieldKey] = d.fieldValue || '';
}
return Array.from(rowMap.entries())
.sort(([a], [b]) => a - b)
.map(([, row]) => row);
}
export function FqcRequestDocumentContent({
template,
documentData,
documentNo,
createdDate,
}: FqcRequestDocumentContentProps) {
const { approvalLines, basicFields, sections, columns } = template;
// 섹션 분리: 입력사항 섹션 (items 있는 것) vs 사전 고지 섹션 (items 없는 것)
const inputSections = sections.filter(s => s.items.length > 0);
const noticeSections = sections.filter(s => s.items.length === 0);
const noticeSection = noticeSections[0]; // 검사대상 사전 고지 정보
// 기본필드를 2열로 배치하기 위한 페어링
const sortedFields = [...basicFields].sort((a, b) => a.sortOrder - b.sortOrder);
const fieldPairs: Array<[typeof sortedFields[0], typeof sortedFields[0] | undefined]> = [];
for (let i = 0; i < sortedFields.length; i += 2) {
fieldPairs.push([sortedFields[i], sortedFields[i + 1]]);
}
// 테이블 행 데이터
const tableRows = getTableRows(documentData, columns);
const sortedColumns = [...columns].sort((a, b) => a.sortOrder - b.sortOrder);
// group_name으로 컬럼 그룹 분석 (병합 헤더용)
const groupInfo = buildGroupInfo(sortedColumns);
return (
<DocumentWrapper fontSize="text-[11px]">
{/* 헤더: 제목 + 결재란 */}
<div className="flex justify-between items-start mb-4">
<div>
<h1 className="text-2xl font-bold tracking-widest mb-2">
{template.title || template.name}
</h1>
<div className="text-[10px] space-y-1">
<div className="flex gap-4">
{documentNo && <span>: <strong>{documentNo}</strong></span>}
{createdDate && <span>: <strong>{createdDate}</strong></span>}
</div>
</div>
</div>
<ConstructionApprovalTable
approvers={{
writer: approvalLines[0]
? { name: '', department: approvalLines[0].department }
: undefined,
approver1: approvalLines[1]
? { name: '', department: approvalLines[1].department }
: undefined,
approver2: approvalLines[2]
? { name: '', department: approvalLines[2].department }
: undefined,
approver3: approvalLines[3]
? { name: '', department: approvalLines[3].department }
: undefined,
}}
/>
</div>
{/* 기본 정보 */}
<DocumentTable header="기본 정보" headerVariant="light" spacing="mb-4">
<tbody>
{fieldPairs.map(([left, right], idx) => (
<tr
key={idx}
className={idx < fieldPairs.length - 1 ? 'border-b border-gray-300' : ''}
>
<td className={lbl}>{left.label}</td>
<td className={right ? `${val} border-r border-gray-300` : val} colSpan={right ? 1 : 3}>
{getFieldValue(documentData, left.fieldKey) || '-'}
</td>
{right && (
<>
<td className={lbl}>{right.label}</td>
<td className={val}>
{getFieldValue(documentData, right.fieldKey) || '-'}
</td>
</>
)}
</tr>
))}
</tbody>
</DocumentTable>
{/* 입력사항: 동적 섹션 */}
{inputSections.length > 0 && (
<div className="border border-gray-400 mb-4">
<div className="bg-gray-200 text-center py-1 font-bold border-b border-gray-400">
</div>
{inputSections.map((section, sIdx) => (
<div
key={section.id}
className={sIdx < inputSections.length - 1 ? 'border-b border-gray-300' : ''}
>
<div className="bg-gray-100 px-2 py-1 font-medium border-b border-gray-300">
{section.title || section.name}
</div>
<table className="w-full">
<tbody>
{buildSectionRows(section, documentData).map((row, rIdx) => (
<tr
key={rIdx}
className={rIdx < buildSectionRows(section, documentData).length - 1 ? 'border-b border-gray-300' : ''}
>
{row.map((cell, cIdx) => (
<td
key={cIdx}
className={
cell.isLabel
? `${subLbl}${cell.width ? ` ${cell.width}` : ''}`
: cIdx < row.length - 1
? `${val} border-r border-gray-300`
: val
}
>
{cell.value || '-'}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
)}
{/* 검사 요청 시 필독 (사전 고지 섹션의 description) */}
{noticeSection?.description && (
<DocumentTable header="검사 요청 시 필독" headerVariant="dark" spacing="mb-4">
<tbody>
<tr>
<td className="px-4 py-3 text-[11px] leading-relaxed text-center">
<p>{noticeSection.description}</p>
</td>
</tr>
</tbody>
</DocumentTable>
)}
{/* 검사대상 사전 고지 정보 테이블 */}
{sortedColumns.length > 0 && (
<DocumentTable
header={noticeSection?.title || '검사대상 사전 고지 정보'}
headerVariant="dark"
spacing="mb-4"
>
<thead>
{/* 그룹 헤더가 있으면 3단 헤더 */}
{groupInfo.hasGroups ? (
<>
<tr className="bg-gray-100 border-b border-gray-300">
{groupInfo.topRow.map((cell, i) => (
<th
key={i}
className={`${DOC_STYLES.th}${i === groupInfo.topRow.length - 1 ? ' border-r-0' : ''}`}
colSpan={cell.colSpan}
rowSpan={cell.rowSpan}
style={cell.width ? { width: cell.width } : undefined}
>
{cell.label}
</th>
))}
</tr>
<tr className="bg-gray-100 border-b border-gray-300">
{groupInfo.midRow.map((cell, i) => (
<th key={i} className={DOC_STYLES.th} colSpan={cell.colSpan}>
{cell.label}
</th>
))}
</tr>
<tr className="bg-gray-100 border-b border-gray-400">
{groupInfo.botRow.map((cell, i) => (
<th
key={i}
className={DOC_STYLES.th}
style={cell.width ? { width: cell.width } : undefined}
>
{cell.label}
</th>
))}
</tr>
</>
) : (
<tr className="bg-gray-100 border-b border-gray-400">
{sortedColumns.map((col, i) => (
<th
key={col.id}
className={`${DOC_STYLES.th}${i === sortedColumns.length - 1 ? ' border-r-0' : ''}`}
style={col.width ? { width: col.width } : undefined}
>
{col.label}
</th>
))}
</tr>
)}
</thead>
<tbody>
{tableRows.length > 0 ? (
tableRows.map((row, rIdx) => (
<tr key={rIdx} className="border-b border-gray-300">
{sortedColumns.map((col, cIdx) => (
<td
key={col.id}
className={
cIdx === sortedColumns.length - 1
? DOC_STYLES.td
: DOC_STYLES.tdCenter
}
>
{col.label === 'No.' ? rIdx + 1 : (row[col.label] || '-')}
</td>
))}
</tr>
))
) : (
<tr>
<td colSpan={sortedColumns.length} className="px-2 py-4 text-center text-gray-400">
.
</td>
</tr>
)}
</tbody>
</DocumentTable>
)}
{/* 서명 영역 */}
<div className="mt-8 text-center text-[10px]">
<p> .</p>
<div className="mt-6">
<p>{createdDate || ''}</p>
</div>
</div>
</DocumentWrapper>
);
}
// ===== 유틸 함수 =====
interface CellInfo {
isLabel: boolean;
value: string;
width?: string;
}
/** 섹션 아이템을 2열 레이아웃의 행으로 변환 */
function buildSectionRows(
section: FqcTemplate['sections'][0],
data?: FqcDocumentData[],
): CellInfo[][] {
const items = [...section.items].sort((a, b) => a.sortOrder - b.sortOrder);
const rows: CellInfo[][] = [];
// 3개 아이템이면 한 행에 3개, 그 외 2개씩
if (items.length === 3) {
rows.push(
items.map((item, i) => [
{ isLabel: true, value: item.itemName, width: i === 2 ? 'w-20' : undefined },
{ isLabel: false, value: getSectionItemValue(data, section.id, item.itemName) },
]).flat()
);
} else {
for (let i = 0; i < items.length; i += 2) {
const left = items[i];
const right = items[i + 1];
const row: CellInfo[] = [
{ isLabel: true, value: left.itemName },
{ isLabel: false, value: getSectionItemValue(data, section.id, left.itemName) },
];
if (right) {
row.push(
{ isLabel: true, value: right.itemName },
{ isLabel: false, value: getSectionItemValue(data, section.id, right.itemName) },
);
}
rows.push(row);
}
}
return rows;
}
interface HeaderCell {
label: string;
colSpan?: number;
rowSpan?: number;
width?: string;
}
interface GroupInfo {
hasGroups: boolean;
topRow: HeaderCell[];
midRow: HeaderCell[];
botRow: HeaderCell[];
}
/** 컬럼 group_name을 분석하여 3단 헤더 구조 생성 */
function buildGroupInfo(columns: FqcTemplate['columns']): GroupInfo {
const groups = columns.filter(c => c.groupName);
if (groups.length === 0) return { hasGroups: false, topRow: [], midRow: [], botRow: [] };
// group_name별로 그룹핑
const groupMap = new Map<string, typeof columns>();
for (const col of columns) {
if (col.groupName) {
if (!groupMap.has(col.groupName)) groupMap.set(col.groupName, []);
groupMap.get(col.groupName)!.push(col);
}
}
// 오픈사이즈(발주규격), 오픈사이즈(시공후규격) 패턴 감지
// group_name 패턴: "오픈사이즈(발주규격)", "오픈사이즈(시공후규격)"
const parentGroups = new Map<string, string[]>();
for (const gName of groupMap.keys()) {
const match = gName.match(/^(.+?)\((.+?)\)$/);
if (match) {
const parent = match[1];
if (!parentGroups.has(parent)) parentGroups.set(parent, []);
parentGroups.get(parent)!.push(gName);
}
}
const topRow: HeaderCell[] = [];
const midRow: HeaderCell[] = [];
const botRow: HeaderCell[] = [];
let colIdx = 0;
while (colIdx < columns.length) {
const col = columns[colIdx];
if (!col.groupName) {
// 그룹이 없는 독립 컬럼 → rowSpan=3
topRow.push({ label: col.label, rowSpan: 3, width: col.width || undefined });
colIdx++;
} else {
// 그룹 컬럼 → 상위 그룹 확인
const match = col.groupName.match(/^(.+?)\((.+?)\)$/);
if (match) {
const parentName = match[1];
const subGroups = parentGroups.get(parentName) || [];
// 상위 그룹의 모든 하위 컬럼 수
let totalCols = 0;
for (const sg of subGroups) {
totalCols += groupMap.get(sg)!.length;
}
topRow.push({ label: parentName, colSpan: totalCols });
// 중간행: 각 하위 그룹
for (const sg of subGroups) {
const subMatch = sg.match(/\((.+?)\)$/);
const subLabel = subMatch ? subMatch[1] : sg;
const subCols = groupMap.get(sg)!;
midRow.push({ label: subLabel, colSpan: subCols.length });
// 하단행: 실제 컬럼 라벨
for (const sc of subCols) {
// 라벨에서 그룹 프리픽스 제거 (발주 가로 → 가로)
const cleanLabel = sc.label.replace(/^(발주|시공)\s*/, '');
botRow.push({ label: cleanLabel, width: sc.width || undefined });
}
}
// 이 그룹에 속한 컬럼 수만큼 건너뛰기
colIdx += totalCols;
} else {
// 단순 그룹 (parentGroup 없이)
const gCols = groupMap.get(col.groupName)!;
topRow.push({ label: col.groupName, colSpan: gCols.length, rowSpan: 2 });
for (const gc of gCols) {
botRow.push({ label: gc.label, width: gc.width || undefined });
}
colIdx += gCols.length;
}
}
}
return { hasGroups: true, topRow, midRow, botRow };
}

View File

@@ -2,25 +2,115 @@
/**
* 제품검사요청서 모달
* DocumentViewer를 사용하여 문서 표시 + 인쇄/PDF 기능 제공
*
* 양식 기반 전환:
* - getFqcRequestTemplate()로 template 66 조회
* - requestDocumentId가 있으면 EAV 문서 로드 → FqcRequestDocumentContent로 렌더링
* - Lazy Snapshot: rendered_html이 없으면 렌더링 완료 후 자동 캡처/저장
* - Fallback: template 로드 실패 시 기존 InspectionRequestDocument 사용
*/
import { useState, useEffect, useRef, useCallback } from 'react';
import { Loader2, AlertCircle } from 'lucide-react';
import { DocumentViewer } from '@/components/document-system';
import { FqcRequestDocumentContent } from './FqcRequestDocumentContent';
import { InspectionRequestDocument } from './InspectionRequestDocument';
import { getFqcRequestTemplate, getFqcDocument, patchDocumentSnapshot } from '../fqcActions';
import type { InspectionRequestDocument as InspectionRequestDocumentType } from '../types';
import type { FqcTemplate, FqcDocument } from '../fqcActions';
interface InspectionRequestModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
data: InspectionRequestDocumentType | null;
/** EAV 요청서 문서 ID (API에서 자동생성된 Document ID) */
requestDocumentId?: number | null;
}
export function InspectionRequestModal({
open,
onOpenChange,
data,
requestDocumentId,
}: InspectionRequestModalProps) {
if (!data) return null;
const contentWrapperRef = useRef<HTMLDivElement>(null);
const snapshotSentRef = useRef(false);
// FQC 양식/문서 상태
const [fqcTemplate, setFqcTemplate] = useState<FqcTemplate | null>(null);
const [fqcDocument, setFqcDocument] = useState<FqcDocument | null>(null);
const [templateLoadFailed, setTemplateLoadFailed] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const useFqcMode = !templateLoadFailed && !!fqcTemplate;
// 모달 열릴 때 초기화
useEffect(() => {
if (open) {
setTemplateLoadFailed(false);
setFqcDocument(null);
snapshotSentRef.current = false;
}
}, [open]);
// 양식 로드
useEffect(() => {
if (!open || fqcTemplate || templateLoadFailed) return;
setIsLoading(true);
getFqcRequestTemplate()
.then(result => {
if (result.success && result.data) {
setFqcTemplate(result.data);
} else {
setTemplateLoadFailed(true);
}
})
.finally(() => setIsLoading(false));
}, [open, fqcTemplate, templateLoadFailed]);
// EAV 문서 로드 (requestDocumentId가 있는 경우)
useEffect(() => {
if (!open || !requestDocumentId || !fqcTemplate) return;
setIsLoading(true);
getFqcDocument(requestDocumentId)
.then(result => {
if (result.success && result.data) {
setFqcDocument(result.data);
}
})
.finally(() => setIsLoading(false));
}, [open, requestDocumentId, fqcTemplate]);
// Lazy Snapshot: FQC 모드 렌더링 완료 후 rendered_html 캡처
const captureSnapshot = useCallback(() => {
if (snapshotSentRef.current || !requestDocumentId || !contentWrapperRef.current) return;
// 렌더링 완료 대기 (다음 프레임)
requestAnimationFrame(() => {
const html = contentWrapperRef.current?.innerHTML;
if (html && html.length > 50) {
snapshotSentRef.current = true;
patchDocumentSnapshot(requestDocumentId, html).catch(() => {
// 실패해도 UI에 영향 없음
});
}
});
}, [requestDocumentId]);
// FQC 문서 렌더링 완료 시 스냅샷 캡처
useEffect(() => {
if (!useFqcMode || isLoading || !fqcDocument) return;
captureSnapshot();
}, [useFqcMode, isLoading, fqcDocument, captureSnapshot]);
if (!data && !useFqcMode) return null;
const documentNo = fqcDocument?.documentNo ?? data?.documentNumber;
const createdDate = data?.createdDate;
const pdfMeta = documentNo
? { documentNumber: documentNo, createdDate: createdDate ?? '' }
: undefined;
return (
<DocumentViewer
@@ -28,12 +118,31 @@ export function InspectionRequestModal({
preset="readonly"
open={open}
onOpenChange={onOpenChange}
pdfMeta={{
documentNumber: data.documentNumber,
createdDate: data.createdDate,
}}
pdfMeta={pdfMeta}
>
<InspectionRequestDocument data={data} />
{isLoading ? (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-muted-foreground"> ...</span>
</div>
) : useFqcMode ? (
<div ref={contentWrapperRef}>
<FqcRequestDocumentContent
template={fqcDocument?.template ?? fqcTemplate}
documentData={fqcDocument?.data}
documentNo={documentNo}
createdDate={createdDate}
readonly={true}
/>
</div>
) : data ? (
<InspectionRequestDocument data={data} />
) : (
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
<AlertCircle className="w-8 h-8 mb-2" />
<p> .</p>
</div>
)}
</DocumentViewer>
);
}

View File

@@ -3,3 +3,4 @@ export { InspectionRequestModal } from './InspectionRequestModal';
export { InspectionReportDocument } from './InspectionReportDocument';
export { InspectionReportModal } from './InspectionReportModal';
export { FqcDocumentContent } from './FqcDocumentContent';
export { FqcRequestDocumentContent } from './FqcRequestDocumentContent';

View File

@@ -26,6 +26,8 @@ interface TemplateItemApi {
measurement_type: string;
frequency: string;
sort_order: number;
category: string | null;
method: string | null;
}
/** 양식 섹션 */
@@ -45,6 +47,7 @@ interface TemplateColumnApi {
label: string;
column_type: string;
width: string | null;
group_name: string | null;
sort_order: number;
}
@@ -158,12 +161,15 @@ export interface FqcTemplateItem {
measurementType: string;
frequency: string;
sortOrder: number;
category: string;
method: string;
}
export interface FqcTemplateSection {
id: number;
name: string;
title: string | null;
description: string | null;
imagePath: string | null;
sortOrder: number;
items: FqcTemplateItem[];
@@ -174,6 +180,7 @@ export interface FqcTemplateColumn {
label: string;
columnType: string;
width: string | null;
groupName: string | null;
sortOrder: number;
}
@@ -276,6 +283,7 @@ function transformTemplate(api: DocumentTemplateApi): FqcTemplate {
id: s.id,
name: s.name,
title: s.title,
description: s.description ?? null,
imagePath: s.image_path,
sortOrder: s.sort_order,
items: (s.items || []).map(item => ({
@@ -287,6 +295,8 @@ function transformTemplate(api: DocumentTemplateApi): FqcTemplate {
measurementType: item.measurement_type,
frequency: item.frequency,
sortOrder: item.sort_order,
category: item.category || '',
method: item.method || '',
})),
})),
columns: (api.columns || []).map(c => ({
@@ -294,6 +304,7 @@ function transformTemplate(api: DocumentTemplateApi): FqcTemplate {
label: c.label,
columnType: c.column_type,
width: c.width,
groupName: c.group_name ?? null,
sortOrder: c.sort_order,
})),
};
@@ -346,6 +357,7 @@ function transformFqcStatus(api: FqcStatusResponse): FqcStatus {
// ===== Server Actions =====
const FQC_TEMPLATE_ID = 65;
const FQC_REQUEST_TEMPLATE_ID = 66;
/**
* FQC 양식 상세 조회
@@ -364,6 +376,23 @@ export async function getFqcTemplate(): Promise<{
});
}
/**
* 제품검사 요청서 양식 상세 조회
* GET /v1/document-templates/{id}
*/
export async function getFqcRequestTemplate(): Promise<{
success: boolean;
data?: FqcTemplate;
error?: string;
__authError?: boolean;
}> {
return executeServerAction<DocumentTemplateApi, FqcTemplate>({
url: buildApiUrl(`/api/v1/document-templates/${FQC_REQUEST_TEMPLATE_ID}`),
transform: transformTemplate,
errorMessage: '제품검사 요청서 양식 조회에 실패했습니다.',
});
}
/**
* FQC 문서 일괄생성
* POST /v1/documents/bulk-create-fqc
@@ -431,6 +460,7 @@ export async function saveFqcDocument(params: {
templateId?: number;
itemId?: number;
title?: string;
renderedHtml?: string;
data: Array<{
section_id?: number | null;
column_id?: number | null;
@@ -451,6 +481,7 @@ export async function saveFqcDocument(params: {
if (params.documentId) body.document_id = params.documentId;
if (params.itemId) body.item_id = params.itemId;
if (params.title) body.title = params.title;
if (params.renderedHtml) body.rendered_html = params.renderedHtml;
return executeServerAction({
url: buildApiUrl('/api/v1/documents/upsert'),
@@ -458,4 +489,20 @@ export async function saveFqcDocument(params: {
body,
errorMessage: 'FQC 검사 데이터 저장에 실패했습니다.',
});
}
/**
* 문서 rendered_html 스냅샷 저장 (Lazy Snapshot)
* PATCH /v1/documents/{id}/snapshot
*/
export async function patchDocumentSnapshot(
documentId: number,
renderedHtml: string,
): Promise<{ success: boolean; error?: string }> {
return executeServerAction({
url: buildApiUrl(`/api/v1/documents/${documentId}/snapshot`),
method: 'PATCH',
body: { rendered_html: renderedHtml },
errorMessage: '스냅샷 저장에 실패했습니다.',
});
}

View File

@@ -154,6 +154,9 @@ export interface ProductInspection {
// 수주 설정
orderItems: OrderSettingItem[];
// EAV 요청서 문서 ID (자동생성)
requestDocumentId?: number | null;
}
// ===== 통계 =====