fix(WEB): Turbopack use server 파일 간 export type 런타임 에러 수정
- 검사 템플릿 타입(InspectionTemplateData 등)을 WorkerScreen/types.ts로 분리 - use server 파일에서 export type 제거 (Turbopack 모듈 평가 시 값으로 처리되는 문제) - 모든 타입 import를 types.ts 직접 참조로 변경
This commit is contained in:
@@ -734,6 +734,108 @@ export async function saveInspectionData(
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 검사 문서 템플릿 조회 (document_template 기반) =====
|
||||
import type { InspectionTemplateData } from '@/components/production/WorkerScreen/types';
|
||||
|
||||
export async function getInspectionTemplate(
|
||||
workOrderId: string
|
||||
): Promise<{ success: boolean; data?: InspectionTemplateData; error?: string }> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/inspection-template`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
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, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[WorkOrderActions] getInspectionTemplate error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 검사 문서 저장 (Document + DocumentData) =====
|
||||
export async function saveInspectionDocument(
|
||||
workOrderId: string,
|
||||
data: {
|
||||
step_id?: number;
|
||||
title?: string;
|
||||
data: Record<string, unknown>[];
|
||||
approvers?: { role_name: string; user_id?: number }[];
|
||||
}
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
data?: { document_id: number; document_no: string; status: string };
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/inspection-document`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}
|
||||
);
|
||||
|
||||
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, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[WorkOrderActions] saveInspectionDocument error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 검사 문서 resolve (기존 문서/템플릿 조회) =====
|
||||
export async function resolveInspectionDocument(
|
||||
workOrderId: string,
|
||||
params?: { step_id?: number }
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
data?: { mode: 'existing' | 'new'; document?: Record<string, unknown>; template?: Record<string, unknown> };
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const query = params?.step_id ? `?step_id=${params.step_id}` : '';
|
||||
const { response, error } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/inspection-resolve${query}`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
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, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[WorkOrderActions] resolveInspectionDocument error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 수주 목록 조회 (작업지시 생성용) =====
|
||||
export interface SalesOrderForWorkOrder {
|
||||
id: number;
|
||||
|
||||
@@ -16,7 +16,13 @@ import { Loader2, Save } from 'lucide-react';
|
||||
import { DocumentViewer } from '@/components/document-system';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from 'sonner';
|
||||
import { getWorkOrderById, saveInspectionData, getInspectionReport } from '../actions';
|
||||
import {
|
||||
getWorkOrderById,
|
||||
saveInspectionData,
|
||||
getInspectionReport,
|
||||
getInspectionTemplate,
|
||||
saveInspectionDocument,
|
||||
} from '../actions';
|
||||
import type { WorkOrder, ProcessType } from '../types';
|
||||
import type { InspectionReportData } from '../actions';
|
||||
import { ScreenInspectionContent } from './ScreenInspectionContent';
|
||||
@@ -24,11 +30,12 @@ import { SlatInspectionContent } from './SlatInspectionContent';
|
||||
import { BendingInspectionContent } from './BendingInspectionContent';
|
||||
import { BendingWipInspectionContent } from './BendingWipInspectionContent';
|
||||
import { SlatJointBarInspectionContent } from './SlatJointBarInspectionContent';
|
||||
import { TemplateInspectionContent } from './TemplateInspectionContent';
|
||||
import type { InspectionContentRef } from './ScreenInspectionContent';
|
||||
import type { InspectionData } from '@/components/production/WorkerScreen/InspectionInputModal';
|
||||
import type { WorkItemData } from '@/components/production/WorkerScreen/types';
|
||||
import type { InspectionSetting } from '@/types/process';
|
||||
import type { InspectionTemplateData } from '@/components/production/WorkerScreen/actions';
|
||||
import type { InspectionTemplateData } from '@/components/production/WorkerScreen/types';
|
||||
|
||||
const PROCESS_LABELS: Record<ProcessType, string> = {
|
||||
screen: '스크린',
|
||||
@@ -124,6 +131,8 @@ export function InspectionReportModal({
|
||||
const [apiWorkItems, setApiWorkItems] = useState<WorkItemData[] | null>(null);
|
||||
const [apiInspectionDataMap, setApiInspectionDataMap] = useState<InspectionDataMap | null>(null);
|
||||
const [reportSummary, setReportSummary] = useState<InspectionReportData['summary'] | null>(null);
|
||||
// 자체 로딩한 템플릿 데이터 (prop으로 안 넘어올 때)
|
||||
const [selfTemplateData, setSelfTemplateData] = useState<InspectionTemplateData | null>(null);
|
||||
|
||||
// props에서 목업 제외한 실제 개소만 사용 (WorkerScreen에서 apiItems + mockItems가 합쳐져 전달됨)
|
||||
// React에서는 개소 미등록 시 성적서 버튼 자체가 노출되지 않으므로 API fallback 불필요
|
||||
@@ -180,12 +189,14 @@ export function InspectionReportModal({
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 작업지시 기본정보 + 검사 성적서 데이터 동시 로딩
|
||||
// 작업지시 기본정보 + 검사 성적서 + 템플릿 동시 로딩
|
||||
Promise.all([
|
||||
getWorkOrderById(workOrderId),
|
||||
getInspectionReport(workOrderId),
|
||||
// prop으로 템플릿이 안 넘어왔으면 자체 로딩
|
||||
!templateData ? getInspectionTemplate(workOrderId) : Promise.resolve(null),
|
||||
])
|
||||
.then(([orderResult, reportResult]) => {
|
||||
.then(([orderResult, reportResult, templateResult]) => {
|
||||
// 1) WorkOrder 기본정보
|
||||
if (orderResult.success && orderResult.data) {
|
||||
const orderData = orderResult.data;
|
||||
@@ -217,6 +228,11 @@ export function InspectionReportModal({
|
||||
setApiInspectionDataMap(null);
|
||||
setReportSummary(null);
|
||||
}
|
||||
|
||||
// 3) 템플릿 데이터 (자체 로딩한 경우)
|
||||
if (templateResult && templateResult.success && templateResult.data) {
|
||||
setSelfTemplateData(templateResult.data);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('서버 오류가 발생했습니다.');
|
||||
@@ -229,9 +245,15 @@ export function InspectionReportModal({
|
||||
setApiWorkItems(null);
|
||||
setApiInspectionDataMap(null);
|
||||
setReportSummary(null);
|
||||
setSelfTemplateData(null);
|
||||
setError(null);
|
||||
}
|
||||
}, [open, workOrderId, processType]);
|
||||
}, [open, workOrderId, processType, templateData]);
|
||||
|
||||
// 템플릿 결정: prop 우선, 없으면 자체 로딩 결과 사용
|
||||
const resolvedTemplateData = templateData || selfTemplateData;
|
||||
const activeTemplate = resolvedTemplateData?.has_template ? resolvedTemplateData.template : null;
|
||||
const activeStepId = resolvedTemplateData?.templates?.[0]?.step_id ?? null;
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!workOrderId || !contentRef.current) return;
|
||||
@@ -239,18 +261,43 @@ export function InspectionReportModal({
|
||||
const data = contentRef.current.getInspectionData();
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const result = await saveInspectionData(workOrderId, processType, data);
|
||||
if (result.success) {
|
||||
toast.success('검사 데이터가 저장되었습니다.');
|
||||
// 템플릿 모드: Document 기반 저장
|
||||
if (activeTemplate) {
|
||||
const inspData = data as {
|
||||
template_id: number;
|
||||
items: { id: string; apiItemId?: number; judgment: string | null; values: Record<string, unknown> }[];
|
||||
inadequateContent: string;
|
||||
overall_result: string | null;
|
||||
};
|
||||
const result = await saveInspectionDocument(workOrderId, {
|
||||
step_id: activeStepId ?? undefined,
|
||||
title: activeTemplate.title || activeTemplate.name,
|
||||
data: inspData.items.map(item => ({
|
||||
item_id: item.apiItemId || item.id,
|
||||
judgment: item.judgment,
|
||||
values: item.values,
|
||||
})),
|
||||
});
|
||||
if (result.success) {
|
||||
toast.success('검사 문서가 저장되었습니다.');
|
||||
} else {
|
||||
toast.error(result.error || '저장에 실패했습니다.');
|
||||
}
|
||||
} else {
|
||||
toast.error(result.error || '저장에 실패했습니다.');
|
||||
// 레거시 모드: 기존 저장
|
||||
const result = await saveInspectionData(workOrderId, processType, data);
|
||||
if (result.success) {
|
||||
toast.success('검사 데이터가 저장되었습니다.');
|
||||
} else {
|
||||
toast.error(result.error || '저장에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
toast.error('저장 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [workOrderId, processType]);
|
||||
}, [workOrderId, processType, activeTemplate, activeStepId]);
|
||||
|
||||
if (!workOrderId) return null;
|
||||
|
||||
@@ -262,139 +309,20 @@ export function InspectionReportModal({
|
||||
? '중간검사성적서 (조인트바)'
|
||||
: '중간검사 성적서';
|
||||
|
||||
// 템플릿 기반 동적 렌더링 여부
|
||||
const useTemplateMode = !!(templateData?.has_template && templateData.template);
|
||||
|
||||
const renderContent = () => {
|
||||
if (!order) return null;
|
||||
|
||||
// 템플릿 모드: 동적 렌더링
|
||||
if (useTemplateMode && templateData?.template) {
|
||||
const tpl = templateData.template;
|
||||
// 템플릿 모드: TemplateInspectionContent 사용
|
||||
if (activeTemplate) {
|
||||
return (
|
||||
<div className="p-6 bg-white space-y-6">
|
||||
{/* 기본 정보 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-bold mb-2">기본 정보</h3>
|
||||
<table className="w-full border-collapse border text-xs">
|
||||
<tbody>
|
||||
{tpl.basic_fields?.map((field) => (
|
||||
<tr key={field.id}>
|
||||
<td className="border px-3 py-1.5 bg-gray-50 font-medium w-32">{field.name}</td>
|
||||
<td className="border px-3 py-1.5">
|
||||
{field.field_key === 'product_name' ? order.items?.[0]?.productName || '-' :
|
||||
field.field_key === 'lot_no' ? (order.lotNo || '-') :
|
||||
field.field_key === 'quantity' ? String(order.items?.reduce((sum, i) => sum + (i.quantity || 0), 0) || 0) :
|
||||
'-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 검사 기준서 (sections) */}
|
||||
{tpl.sections?.map((section) => (
|
||||
<div key={section.id}>
|
||||
<h3 className="text-sm font-bold mb-2">{section.name}</h3>
|
||||
<table className="w-full border-collapse border text-xs">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th className="border px-3 py-1.5 text-left w-8">No</th>
|
||||
<th className="border px-3 py-1.5 text-left">검사항목</th>
|
||||
<th className="border px-3 py-1.5 text-left w-24">기준</th>
|
||||
<th className="border px-3 py-1.5 text-left w-24">허용오차</th>
|
||||
<th className="border px-3 py-1.5 text-left w-20">방법</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{section.items?.map((item, idx) => (
|
||||
<tr key={item.id}>
|
||||
<td className="border px-3 py-1.5">{idx + 1}</td>
|
||||
<td className="border px-3 py-1.5">{item.name}</td>
|
||||
<td className="border px-3 py-1.5">{item.standard_criteria || '-'}</td>
|
||||
<td className="border px-3 py-1.5">
|
||||
{item.tolerance ? (
|
||||
item.tolerance.type === 'symmetric' ? `± ${item.tolerance.value}` :
|
||||
item.tolerance.type === 'asymmetric' ? `+${item.tolerance.plus} / -${item.tolerance.minus}` :
|
||||
item.tolerance.type === 'range' ? `${item.tolerance.min} ~ ${item.tolerance.max}` : '-'
|
||||
) : '-'}
|
||||
</td>
|
||||
<td className="border px-3 py-1.5">{item.measurement_type || '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 검사 DATA (columns) */}
|
||||
{tpl.columns && tpl.columns.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-bold mb-2">검사 DATA</h3>
|
||||
<table className="w-full border-collapse border text-xs">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th className="border px-3 py-1.5 text-left w-8">No</th>
|
||||
{tpl.columns.map((col) => (
|
||||
<th key={col.id} className="border px-3 py-1.5 text-left">{col.name}</th>
|
||||
))}
|
||||
<th className="border px-3 py-1.5 text-left w-16">판정</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(effectiveWorkItems || []).map((item, idx) => {
|
||||
const itemData = effectiveInspectionDataMap?.get(item.id);
|
||||
return (
|
||||
<tr key={item.id}>
|
||||
<td className="border px-3 py-1.5">{idx + 1}</td>
|
||||
{tpl.columns.map((col) => (
|
||||
<td key={col.id} className="border px-3 py-1.5">
|
||||
{itemData?.templateValues
|
||||
? String(itemData.templateValues[`col_${col.id}`] ?? '-')
|
||||
: '-'}
|
||||
</td>
|
||||
))}
|
||||
<td className="border px-3 py-1.5 font-bold">
|
||||
{itemData?.judgment === 'pass' ? (
|
||||
<span className="text-green-600">적합</span>
|
||||
) : itemData?.judgment === 'fail' ? (
|
||||
<span className="text-red-600">부적합</span>
|
||||
) : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 결재라인 */}
|
||||
{tpl.approval_lines && tpl.approval_lines.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-bold mb-2">결재</h3>
|
||||
<table className="w-full border-collapse border text-xs">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
{tpl.approval_lines.map((line) => (
|
||||
<th key={line.id} className="border px-3 py-2 text-center w-24">{line.role_name}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
{tpl.approval_lines.map((line) => (
|
||||
<td key={line.id} className="border px-3 py-4 text-center text-gray-400">
|
||||
(서명)
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<TemplateInspectionContent
|
||||
ref={contentRef}
|
||||
data={order}
|
||||
template={activeTemplate}
|
||||
readOnly={readOnly}
|
||||
workItems={effectiveWorkItems}
|
||||
inspectionDataMap={effectiveInspectionDataMap}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,482 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 템플릿 기반 중간검사 성적서 콘텐츠
|
||||
*
|
||||
* DocumentTemplate의 sections/items에서 measurement_type별 입력 셀을 자동 생성:
|
||||
* - checkbox → 양호/불량 토글
|
||||
* - numeric → 기준값 표시 + 측정값 입력
|
||||
* - single_value → 단일값 입력
|
||||
* - substitute → 성적서 대체 배지
|
||||
* - text → 자유 텍스트 입력
|
||||
*/
|
||||
|
||||
import { useState, forwardRef, useImperativeHandle, useEffect, Fragment } from 'react';
|
||||
import type { WorkOrder } from '../types';
|
||||
import type { WorkItemData } from '@/components/production/WorkerScreen/types';
|
||||
import type { InspectionDataMap } from './InspectionReportModal';
|
||||
import type {
|
||||
InspectionTemplateFormat,
|
||||
InspectionTemplateSectionItem,
|
||||
InspectionTolerance,
|
||||
} from '@/components/production/WorkerScreen/types';
|
||||
import {
|
||||
type InspectionContentRef,
|
||||
InspectionCheckbox,
|
||||
InspectionLayout,
|
||||
InspectionFooter,
|
||||
JudgmentCell,
|
||||
calculateOverallResult,
|
||||
getFullDate,
|
||||
getOrderInfo,
|
||||
INPUT_CLASS,
|
||||
} from './inspection-shared';
|
||||
|
||||
export type { InspectionContentRef };
|
||||
|
||||
// ===== 셀 값 타입 =====
|
||||
interface CellValue {
|
||||
status?: 'good' | 'bad' | null;
|
||||
measurements?: [string, string, string];
|
||||
value?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
// ===== Props =====
|
||||
interface TemplateInspectionContentProps {
|
||||
data: WorkOrder;
|
||||
template: InspectionTemplateFormat;
|
||||
readOnly?: boolean;
|
||||
workItems?: WorkItemData[];
|
||||
inspectionDataMap?: InspectionDataMap;
|
||||
}
|
||||
|
||||
// ===== 유틸 =====
|
||||
|
||||
function formatTolerance(tol: InspectionTolerance | null): string {
|
||||
if (!tol) return '-';
|
||||
if (tol.type === 'symmetric') return `± ${tol.value}`;
|
||||
if (tol.type === 'asymmetric') return `+${tol.plus} / -${tol.minus}`;
|
||||
if (tol.type === 'range') return `${tol.min} ~ ${tol.max}`;
|
||||
return '-';
|
||||
}
|
||||
|
||||
function formatStandard(item: InspectionTemplateSectionItem): string {
|
||||
const sc = item.standard_criteria;
|
||||
if (!sc) return item.standard || '-';
|
||||
if (typeof sc === 'object') {
|
||||
if ('nominal' in sc) return String(sc.nominal);
|
||||
if ('min' in sc && 'max' in sc) return `${sc.min} ~ ${sc.max}`;
|
||||
if ('max' in sc) return `≤ ${sc.max}`;
|
||||
if ('min' in sc) return `≥ ${sc.min}`;
|
||||
}
|
||||
return String(sc);
|
||||
}
|
||||
|
||||
function getNominalValue(item: InspectionTemplateSectionItem): number | null {
|
||||
const sc = item.standard_criteria;
|
||||
if (!sc || typeof sc !== 'object') {
|
||||
if (typeof sc === 'string') {
|
||||
const v = parseFloat(sc);
|
||||
return isNaN(v) ? null : v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if ('nominal' in sc) return sc.nominal;
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatFrequency(item: InspectionTemplateSectionItem): string {
|
||||
if (item.frequency_n && item.frequency_c) return `n=${item.frequency_n}, c=${item.frequency_c}`;
|
||||
if (item.frequency) return item.frequency;
|
||||
return '-';
|
||||
}
|
||||
|
||||
function getMeasurementLabel(type: string | null): string {
|
||||
switch (type) {
|
||||
case 'checkbox': return 'OK/NG';
|
||||
case 'numeric': return '수치(3회)';
|
||||
case 'single_value': return '단일값';
|
||||
case 'substitute': return '대체';
|
||||
case 'text': return '자유입력';
|
||||
default: return type || '-';
|
||||
}
|
||||
}
|
||||
|
||||
/** 측정값이 공차 범위 내인지 판정 */
|
||||
function isWithinTolerance(measured: number, item: InspectionTemplateSectionItem): boolean {
|
||||
const nominal = getNominalValue(item);
|
||||
const tol = item.tolerance;
|
||||
if (nominal === null || !tol) return true; // 기준 없으면 pass
|
||||
|
||||
switch (tol.type) {
|
||||
case 'symmetric':
|
||||
return Math.abs(measured - nominal) <= (tol.value ?? 0);
|
||||
case 'asymmetric':
|
||||
return measured >= nominal - (tol.minus ?? 0) && measured <= nominal + (tol.plus ?? 0);
|
||||
case 'range':
|
||||
return measured >= (tol.min ?? -Infinity) && measured <= (tol.max ?? Infinity);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 컴포넌트 =====
|
||||
|
||||
export const TemplateInspectionContent = forwardRef<InspectionContentRef, TemplateInspectionContentProps>(
|
||||
function TemplateInspectionContent({ data: order, template, readOnly = false, workItems, inspectionDataMap }, ref) {
|
||||
const fullDate = getFullDate();
|
||||
const { documentNo, primaryAssignee } = getOrderInfo(order);
|
||||
|
||||
// 모든 섹션의 아이템을 평탄화 (DATA 테이블 컬럼용)
|
||||
const allItems = template.sections.flatMap(s => s.items);
|
||||
|
||||
// 셀 값 상태: key = `${rowIdx}-${itemId}`
|
||||
const [cellValues, setCellValues] = useState<Record<string, CellValue>>({});
|
||||
const [inadequateContent, setInadequateContent] = useState('');
|
||||
|
||||
// inspectionDataMap에서 초기값 복원
|
||||
useEffect(() => {
|
||||
if (!inspectionDataMap || !workItems) return;
|
||||
const initial: Record<string, CellValue> = {};
|
||||
workItems.forEach((wi, rowIdx) => {
|
||||
const itemData = inspectionDataMap.get(wi.id);
|
||||
if (!itemData?.templateValues) return;
|
||||
allItems.forEach(sectionItem => {
|
||||
const key = `${rowIdx}-${sectionItem.id}`;
|
||||
const val = itemData.templateValues?.[`item_${sectionItem.id}`];
|
||||
if (val && typeof val === 'object') {
|
||||
initial[key] = val as CellValue;
|
||||
}
|
||||
});
|
||||
});
|
||||
if (Object.keys(initial).length > 0) setCellValues(initial);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [inspectionDataMap, workItems]);
|
||||
|
||||
// ref로 데이터 수집 노출
|
||||
useImperativeHandle(ref, () => ({
|
||||
getInspectionData: () => {
|
||||
const items = effectiveWorkItems.map((wi, idx) => ({
|
||||
id: wi.id,
|
||||
apiItemId: wi.apiItemId,
|
||||
judgment: getRowJudgment(idx),
|
||||
values: allItems.reduce((acc, sItem) => {
|
||||
const key = `${idx}-${sItem.id}`;
|
||||
acc[`item_${sItem.id}`] = cellValues[key] || null;
|
||||
return acc;
|
||||
}, {} as Record<string, CellValue | null>),
|
||||
}));
|
||||
|
||||
return {
|
||||
template_id: template.id,
|
||||
items,
|
||||
inadequateContent,
|
||||
overall_result: overallResult,
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
const updateCell = (key: string, update: Partial<CellValue>) => {
|
||||
setCellValues(prev => ({
|
||||
...prev,
|
||||
[key]: { ...prev[key], ...update },
|
||||
}));
|
||||
};
|
||||
|
||||
// 행별 판정 계산
|
||||
const getRowJudgment = (rowIdx: number): '적' | '부' | null => {
|
||||
let hasAnyValue = false;
|
||||
let hasFail = false;
|
||||
|
||||
for (const item of allItems) {
|
||||
const key = `${rowIdx}-${item.id}`;
|
||||
const cell = cellValues[key];
|
||||
if (!cell) continue;
|
||||
|
||||
if (item.measurement_type === 'checkbox') {
|
||||
if (cell.status === 'bad') hasFail = true;
|
||||
if (cell.status) hasAnyValue = true;
|
||||
} else if (item.measurement_type === 'numeric') {
|
||||
const measurements = cell.measurements || ['', '', ''];
|
||||
for (const m of measurements) {
|
||||
if (m) {
|
||||
hasAnyValue = true;
|
||||
const val = parseFloat(m);
|
||||
if (!isNaN(val) && !isWithinTolerance(val, item)) hasFail = true;
|
||||
}
|
||||
}
|
||||
} else if (item.measurement_type === 'single_value') {
|
||||
if (cell.value) {
|
||||
hasAnyValue = true;
|
||||
const val = parseFloat(cell.value);
|
||||
if (!isNaN(val) && !isWithinTolerance(val, item)) hasFail = true;
|
||||
}
|
||||
} else if (item.measurement_type === 'substitute') {
|
||||
// 성적서 대체는 항상 적합 취급
|
||||
hasAnyValue = true;
|
||||
} else if (cell.value || cell.text) {
|
||||
hasAnyValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAnyValue) return null;
|
||||
return hasFail ? '부' : '적';
|
||||
};
|
||||
|
||||
const effectiveWorkItems = workItems || [];
|
||||
|
||||
// 종합판정
|
||||
const judgments = effectiveWorkItems.map((_, idx) => getRowJudgment(idx));
|
||||
const overallResult = calculateOverallResult(judgments);
|
||||
|
||||
// numeric 아이템의 DATA 열 colspan 계산
|
||||
const getItemColSpan = (item: InspectionTemplateSectionItem) => {
|
||||
if (item.measurement_type === 'numeric') return 2; // 기준 + 측정
|
||||
return 1;
|
||||
};
|
||||
const totalDataCols = allItems.reduce((sum, item) => sum + getItemColSpan(item), 0);
|
||||
|
||||
return (
|
||||
<InspectionLayout
|
||||
title={template.title || template.name || '중간검사 성적서'}
|
||||
documentNo={documentNo}
|
||||
fullDate={fullDate}
|
||||
primaryAssignee={primaryAssignee}
|
||||
>
|
||||
{/* 기본 정보 */}
|
||||
{template.basic_fields?.length > 0 && (
|
||||
<table className="w-full border-collapse text-xs mb-4">
|
||||
<tbody>
|
||||
{template.basic_fields.map(field => (
|
||||
<tr key={field.id}>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-1.5 font-medium w-32">{field.label}</td>
|
||||
<td className="border border-gray-400 px-3 py-1.5">
|
||||
{field.field_key === 'product_name' ? order.items?.[0]?.productName || '-' :
|
||||
field.field_key === 'lot_no' ? (order.lotNo || '-') :
|
||||
field.field_key === 'quantity' ? String(order.items?.reduce((sum, i) => sum + (i.quantity || 0), 0) || 0) :
|
||||
field.field_key === 'inspection_date' ? fullDate :
|
||||
field.default_value || '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
{/* 검사 기준서 - 섹션별 */}
|
||||
{template.sections.map(section => (
|
||||
<div key={section.id} className="mb-4">
|
||||
<div className="mb-1 font-bold text-sm">■ {section.name}</div>
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 px-2 py-1.5 w-8">No</th>
|
||||
<th className="border border-gray-400 px-2 py-1.5">검사항목</th>
|
||||
<th className="border border-gray-400 px-2 py-1.5 w-20">검사기준</th>
|
||||
<th className="border border-gray-400 px-2 py-1.5 w-20">허용오차</th>
|
||||
<th className="border border-gray-400 px-2 py-1.5 w-16">검사방식</th>
|
||||
<th className="border border-gray-400 px-2 py-1.5 w-16">측정유형</th>
|
||||
<th className="border border-gray-400 px-2 py-1.5 w-20">빈도</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{section.items.map((item, idx) => (
|
||||
<tr key={item.id}>
|
||||
<td className="border border-gray-400 px-2 py-1.5 text-center">{idx + 1}</td>
|
||||
<td className="border border-gray-400 px-2 py-1.5">{item.item || item.category || '-'}</td>
|
||||
<td className="border border-gray-400 px-2 py-1.5 text-center">{formatStandard(item)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1.5 text-center">{formatTolerance(item.tolerance)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1.5 text-center">{item.method_name || item.method || '-'}</td>
|
||||
<td className="border border-gray-400 px-2 py-1.5 text-center">{getMeasurementLabel(item.measurement_type)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1.5 text-center">{formatFrequency(item)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 검사 DATA 테이블 */}
|
||||
{allItems.length > 0 && effectiveWorkItems.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-1 font-bold text-sm">■ 검사 DATA</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
{/* 상위 헤더: 항목 그룹 */}
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 px-2 py-1.5 w-8" rowSpan={2}>No</th>
|
||||
<th className="border border-gray-400 px-2 py-1.5 min-w-[80px]" rowSpan={2}>품명</th>
|
||||
{allItems.map(item => (
|
||||
<th
|
||||
key={item.id}
|
||||
className="border border-gray-400 px-2 py-1 text-center"
|
||||
colSpan={getItemColSpan(item)}
|
||||
>
|
||||
{item.item || item.category || '-'}
|
||||
</th>
|
||||
))}
|
||||
<th className="border border-gray-400 px-2 py-1.5 w-10" rowSpan={2}>판정</th>
|
||||
</tr>
|
||||
{/* 하위 헤더: numeric 아이템만 기준/측정 분할 */}
|
||||
<tr className="bg-gray-50">
|
||||
{allItems.map(item => {
|
||||
if (item.measurement_type === 'numeric') {
|
||||
return (
|
||||
<Fragment key={item.id}>
|
||||
<th className="border border-gray-400 px-1 py-1 text-center text-[10px]">기준</th>
|
||||
<th className="border border-gray-400 px-1 py-1 text-center text-[10px]">측정</th>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
// checkbox, single_value, text, substitute: 단일 열
|
||||
return (
|
||||
<th key={item.id} className="border border-gray-400 px-1 py-1 text-center text-[10px]">
|
||||
{item.measurement_type === 'checkbox' ? '양호/불량' :
|
||||
item.measurement_type === 'substitute' ? '대체' :
|
||||
'입력'}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{effectiveWorkItems.map((wi, rowIdx) => (
|
||||
<tr key={wi.id}>
|
||||
<td className="border border-gray-400 px-2 py-1.5 text-center">{rowIdx + 1}</td>
|
||||
<td className="border border-gray-400 px-2 py-1.5 whitespace-nowrap">{wi.itemName || '-'}</td>
|
||||
{allItems.map(item => {
|
||||
const key = `${rowIdx}-${item.id}`;
|
||||
const cell = cellValues[key];
|
||||
|
||||
// checkbox → 양호/불량 토글
|
||||
if (item.measurement_type === 'checkbox') {
|
||||
return (
|
||||
<td key={item.id} className="border border-gray-400 p-1">
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<label className="flex items-center gap-0.5 cursor-pointer text-xs whitespace-nowrap">
|
||||
<InspectionCheckbox
|
||||
checked={cell?.status === 'good'}
|
||||
onClick={() => updateCell(key, { status: cell?.status === 'good' ? null : 'good' })}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
양호
|
||||
</label>
|
||||
<label className="flex items-center gap-0.5 cursor-pointer text-xs whitespace-nowrap">
|
||||
<InspectionCheckbox
|
||||
checked={cell?.status === 'bad'}
|
||||
onClick={() => updateCell(key, { status: cell?.status === 'bad' ? null : 'bad' })}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
불량
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
// numeric → 기준값 + 측정값 입력
|
||||
if (item.measurement_type === 'numeric') {
|
||||
return (
|
||||
<Fragment key={item.id}>
|
||||
<td className="border border-gray-400 px-2 py-1.5 text-center text-gray-500">
|
||||
{formatStandard(item)}
|
||||
</td>
|
||||
<td className="border border-gray-400 p-0.5">
|
||||
<input
|
||||
type="text"
|
||||
className={INPUT_CLASS}
|
||||
value={cell?.measurements?.[0] || ''}
|
||||
onChange={e => {
|
||||
const m: [string, string, string] = [
|
||||
...(cell?.measurements || ['', '', '']),
|
||||
] as [string, string, string];
|
||||
m[0] = e.target.value;
|
||||
updateCell(key, { measurements: m });
|
||||
}}
|
||||
readOnly={readOnly}
|
||||
placeholder="측정값"
|
||||
/>
|
||||
</td>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
// substitute → 성적서 대체 표시
|
||||
if (item.measurement_type === 'substitute') {
|
||||
return (
|
||||
<td key={item.id} className="border border-gray-400 px-2 py-1.5 text-center">
|
||||
<span className="inline-block px-1.5 py-0.5 bg-gray-100 text-gray-600 rounded text-[10px]">
|
||||
대체
|
||||
</span>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
// single_value, text, default → 입력 필드
|
||||
return (
|
||||
<td key={item.id} className="border border-gray-400 p-0.5">
|
||||
<input
|
||||
type="text"
|
||||
className={INPUT_CLASS}
|
||||
value={cell?.value || cell?.text || ''}
|
||||
onChange={e => updateCell(key, { value: e.target.value })}
|
||||
readOnly={readOnly}
|
||||
placeholder="-"
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<JudgmentCell judgment={getRowJudgment(rowIdx)} />
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 부적합 내용 + 종합판정 */}
|
||||
<InspectionFooter
|
||||
readOnly={readOnly}
|
||||
overallResult={overallResult}
|
||||
inadequateContent={inadequateContent}
|
||||
onInadequateContentChange={setInadequateContent}
|
||||
/>
|
||||
|
||||
{/* 결재라인 */}
|
||||
{template.approval_lines?.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<table className="border-collapse text-sm ml-auto">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-4 py-1 text-center align-middle" rowSpan={3}>결<br/>재</td>
|
||||
{template.approval_lines.map(line => (
|
||||
<td key={`role-${line.id}`} className="border border-gray-400 px-6 py-1 text-center">
|
||||
{line.role || line.name}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
{template.approval_lines.map(line => (
|
||||
<td key={`name-${line.id}`} className="border border-gray-400 px-6 py-3 text-center text-gray-400">
|
||||
{line.name || '이름'}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
{template.approval_lines.map(line => (
|
||||
<td key={`dept-${line.id}`} className="border border-gray-400 px-6 py-1 text-center whitespace-nowrap">
|
||||
{line.dept || '부서명'}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</InspectionLayout>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -22,7 +22,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { InspectionTemplateData } from './actions';
|
||||
import type { InspectionTemplateData } from './types';
|
||||
|
||||
// 중간검사 공정 타입
|
||||
export type InspectionProcessType =
|
||||
@@ -281,7 +281,7 @@ function DynamicInspectionForm({
|
||||
// 양호/불량 토글
|
||||
return (
|
||||
<div key={item.id} className="space-y-1.5">
|
||||
<span className="text-sm font-bold">{item.name}</span>
|
||||
<span className="text-sm font-bold">{item.item || item.name}</span>
|
||||
<div className="flex gap-2 flex-1">
|
||||
<button
|
||||
type="button"
|
||||
@@ -318,17 +318,24 @@ function DynamicInspectionForm({
|
||||
// 판정 표시
|
||||
let itemJudgment: 'pass' | 'fail' | null = null;
|
||||
if (item.tolerance && numValue != null && item.standard_criteria) {
|
||||
const design = parseFloat(item.standard_criteria);
|
||||
const designStr = typeof item.standard_criteria === 'object'
|
||||
? String((item.standard_criteria as Record<string, number>).nominal ?? '')
|
||||
: String(item.standard_criteria);
|
||||
const design = parseFloat(designStr);
|
||||
if (!isNaN(design)) {
|
||||
itemJudgment = evaluateTolerance(numValue, design, item.tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
const placeholderStr = typeof item.standard_criteria === 'object'
|
||||
? String((item.standard_criteria as Record<string, number>).nominal ?? '입력')
|
||||
: (item.standard_criteria || '입력');
|
||||
|
||||
return (
|
||||
<div key={item.id} className="space-y-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-bold">
|
||||
{item.name}{toleranceLabel}
|
||||
{item.item || item.name}{toleranceLabel}
|
||||
</span>
|
||||
{itemJudgment && (
|
||||
<span className={cn(
|
||||
@@ -341,7 +348,7 @@ function DynamicInspectionForm({
|
||||
</div>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={item.standard_criteria || '입력'}
|
||||
placeholder={placeholderStr}
|
||||
value={numValue ?? ''}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value === '' ? null : parseFloat(e.target.value);
|
||||
@@ -378,7 +385,10 @@ function computeDynamicJudgment(
|
||||
const numValue = value as number | null | undefined;
|
||||
if (numValue != null) {
|
||||
hasAnyValue = true;
|
||||
const design = parseFloat(item.standard_criteria);
|
||||
const designStr = typeof item.standard_criteria === 'object'
|
||||
? String((item.standard_criteria as Record<string, number>).nominal ?? '')
|
||||
: String(item.standard_criteria);
|
||||
const design = parseFloat(designStr);
|
||||
if (!isNaN(design)) {
|
||||
const result = evaluateTolerance(numValue, design, item.tolerance);
|
||||
if (result === 'fail') hasFail = true;
|
||||
|
||||
@@ -515,69 +515,8 @@ export async function getWorkOrderInspectionData(
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
// ===== 검사 문서 템플릿 조회 (document_template 기반) =====
|
||||
export interface InspectionTemplateData {
|
||||
work_order_id: number;
|
||||
has_template: boolean;
|
||||
template?: {
|
||||
id: number;
|
||||
name: string;
|
||||
category: string;
|
||||
description: string | null;
|
||||
sections: {
|
||||
id: number;
|
||||
name: string;
|
||||
sort_order: number;
|
||||
items: {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
is_required: boolean;
|
||||
sort_order: number;
|
||||
options: Record<string, unknown> | null;
|
||||
tolerance: {
|
||||
type: 'symmetric' | 'asymmetric' | 'range';
|
||||
value?: number;
|
||||
plus?: number;
|
||||
minus?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
} | null;
|
||||
standard_criteria: string | null;
|
||||
measurement_type: string | null;
|
||||
}[];
|
||||
}[];
|
||||
columns: {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
sort_order: number;
|
||||
is_required: boolean;
|
||||
options: Record<string, unknown> | null;
|
||||
}[];
|
||||
approval_lines: {
|
||||
id: number;
|
||||
role_name: string;
|
||||
sort_order: number;
|
||||
is_required: boolean;
|
||||
}[];
|
||||
basic_fields: {
|
||||
id: number;
|
||||
name: string;
|
||||
field_key: string;
|
||||
field_type: string;
|
||||
is_required: boolean;
|
||||
sort_order: number;
|
||||
options: Record<string, unknown> | null;
|
||||
}[];
|
||||
};
|
||||
work_order_info?: {
|
||||
work_order_no: string;
|
||||
project_name: string | null;
|
||||
process_name: string | null;
|
||||
scheduled_date: string | null;
|
||||
};
|
||||
}
|
||||
// ===== 검사 문서 템플릿 타입 (types.ts에서 import) =====
|
||||
import type { InspectionTemplateData } from './types';
|
||||
|
||||
export async function getInspectionTemplate(
|
||||
workOrderId: string
|
||||
|
||||
@@ -34,7 +34,7 @@ import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
import { getMyWorkOrders, completeWorkOrder, saveItemInspection, getWorkOrderInspectionData, saveInspectionDocument, getInspectionTemplate } from './actions';
|
||||
import type { InspectionTemplateData } from './actions';
|
||||
import type { InspectionTemplateData } from './types';
|
||||
import { getProcessList } from '@/components/process-management/actions';
|
||||
import type { InspectionSetting, Process } from '@/types/process';
|
||||
import type { WorkOrder } from '../ProductionDashboard/types';
|
||||
|
||||
@@ -193,3 +193,118 @@ export interface CompletionToastInfo {
|
||||
quantity: number;
|
||||
lotNo: string;
|
||||
}
|
||||
|
||||
// ===== 검사 템플릿 타입 =====
|
||||
|
||||
export interface InspectionTolerance {
|
||||
type: 'symmetric' | 'asymmetric' | 'range';
|
||||
value?: number;
|
||||
plus?: number;
|
||||
minus?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
/** 섹션 아이템 (검사항목) - API formatTemplateForReact 응답 기준 */
|
||||
export interface InspectionTemplateSectionItem {
|
||||
id: number;
|
||||
field_values: Record<string, unknown> | null;
|
||||
category: string | null;
|
||||
item: string | null;
|
||||
standard: string | null;
|
||||
standard_criteria: Record<string, number> | string | null;
|
||||
tolerance: InspectionTolerance | null;
|
||||
method: string | null;
|
||||
method_name: string | null;
|
||||
measurement_type: string | null;
|
||||
frequency: string | null;
|
||||
frequency_n: number | null;
|
||||
frequency_c: number | null;
|
||||
regulation: string | null;
|
||||
sort_order: number;
|
||||
// backward compat (InspectionInputModal uses these)
|
||||
name?: string;
|
||||
type?: string;
|
||||
is_required?: boolean;
|
||||
options?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
/** 템플릿 전체 구조 (formatTemplateForReact 응답) */
|
||||
export interface InspectionTemplateFormat {
|
||||
id: number;
|
||||
name: string;
|
||||
category: string;
|
||||
title?: string;
|
||||
company_name?: string;
|
||||
company_address?: string;
|
||||
company_contact?: string;
|
||||
footer_remark_label?: string;
|
||||
footer_judgement_label?: string;
|
||||
footer_judgement_options?: string[];
|
||||
sections: {
|
||||
id: number;
|
||||
name: string;
|
||||
sort_order: number;
|
||||
items: InspectionTemplateSectionItem[];
|
||||
}[];
|
||||
section_fields: {
|
||||
id: number;
|
||||
field_key: string;
|
||||
label: string;
|
||||
field_type: string;
|
||||
options: Record<string, unknown> | null;
|
||||
width: string | null;
|
||||
is_required: boolean;
|
||||
sort_order: number;
|
||||
}[];
|
||||
columns: {
|
||||
id: number;
|
||||
label: string;
|
||||
input_type: string;
|
||||
options: Record<string, unknown> | null;
|
||||
width: string | null;
|
||||
is_required: boolean;
|
||||
sort_order: number;
|
||||
}[];
|
||||
approval_lines: {
|
||||
id: number;
|
||||
name: string;
|
||||
dept: string | null;
|
||||
role: string | null;
|
||||
user_id: number | null;
|
||||
sort_order: number;
|
||||
}[];
|
||||
basic_fields: {
|
||||
id: number;
|
||||
field_key: string;
|
||||
label: string;
|
||||
input_type: string;
|
||||
options: Record<string, unknown> | null;
|
||||
default_value: string | null;
|
||||
is_required: boolean;
|
||||
sort_order: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
/** 검사 단계별 템플릿 */
|
||||
export interface InspectionTemplateStep {
|
||||
step_id: number;
|
||||
step_name: string;
|
||||
step_code: string;
|
||||
sort_order: number;
|
||||
template: InspectionTemplateFormat;
|
||||
}
|
||||
|
||||
/** 검사 문서 템플릿 조회 응답 */
|
||||
export interface InspectionTemplateData {
|
||||
work_order_id: number;
|
||||
has_template: boolean;
|
||||
templates?: InspectionTemplateStep[];
|
||||
template?: InspectionTemplateFormat; // backward compat (첫 번째 템플릿)
|
||||
work_order_info?: {
|
||||
work_order_no: string;
|
||||
project_name: string | null;
|
||||
process_name: string | null;
|
||||
scheduled_date: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user