feat(WEB):중간검사 정규화 데이터 저장 및 조회

- TemplateInspectionContent: 정규화 형식(section_id/column_id/field_key) 저장/조회 지원
- InspectionReportModal: 문서 데이터 조회 연동
- actions: getMaterialInputLots API 호출 추가
- types: MaterialInputLot 타입 추가
This commit is contained in:
2026-02-12 00:01:09 +09:00
parent fcd5408052
commit 90e1d428c4
4 changed files with 204 additions and 34 deletions

View File

@@ -1040,3 +1040,42 @@ export async function getProcessOptions(): Promise<{
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
}
}
// ===== 자재 투입 LOT 번호 조회 =====
export interface MaterialInputLot {
lot_no: string;
item_code: string;
item_name: string;
total_qty: number;
input_count: number;
first_input_at: string;
}
export async function getMaterialInputLots(workOrderId: string): Promise<{
success: boolean;
data: MaterialInputLot[];
error?: string;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/material-input-lots`,
{ method: 'GET' }
);
if (error || !response) {
return { success: false, data: [], error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, data: [], error: result.message || '투입 LOT 조회에 실패했습니다.' };
}
return { success: true, data: result.data || [] };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[WorkOrderActions] getMaterialInputLots error:', error);
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
}
}

View File

@@ -262,22 +262,22 @@ export function InspectionReportModal({
const data = contentRef.current.getInspectionData();
setIsSaving(true);
try {
// 템플릿 모드: Document 기반 저장
// 템플릿 모드: 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;
records: Array<{
section_id: number | null;
column_id: number | null;
row_index: number;
field_key: string;
field_value: 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,
})),
data: inspData.records,
});
if (result.success) {
toast.success('검사 문서가 저장되었습니다.');

View File

@@ -56,6 +56,17 @@ interface TemplateInspectionContentProps {
// ===== 유틸 =====
/** API 저장소 이미지 URL 생성 (사원관리와 동일 패턴) */
function getImageUrl(path: string | null | undefined): string {
if (!path) return '';
if (path.startsWith('http://') || path.startsWith('https://')) return path;
const apiUrl = process.env.NEXT_PUBLIC_API_URL || '';
// tenant storage 경로 (숫자/로 시작: {tenant_id}/temp/...)
if (/^\d+\//.test(path)) return `${apiUrl}/storage/tenants/${path}`;
// 레거시 경로 (document-templates/xxx.jpg 등)
return `${apiUrl}/storage/${path}`;
}
/** field_values.reference_attribute에서 작업 아이템의 실제 치수를 resolve */
function resolveReferenceValue(
item: InspectionTemplateSectionItem,
@@ -307,32 +318,147 @@ export const TemplateInspectionContent = forwardRef<InspectionContentRef, Templa
return hasFail ? '부' : '적';
};
// ref로 데이터 수집 노출
// ref로 데이터 수집 노출 - 정규화된 document_data 레코드 형식
useImperativeHandle(ref, () => ({
getInspectionData: () => {
const items = effectiveWorkItems.map((wi, idx) => ({
id: wi.id,
apiItemId: wi.apiItemId,
judgment: getRowJudgment(idx),
values: template.columns.reduce((acc, col) => {
const sectionItem = columnItemMap.get(col.id);
if (!sectionItem) return acc;
const key = `${idx}-${col.id}`;
const sectionId = itemSectionMap.get(sectionItem.id);
const saveKey = sectionId != null
? `section_${sectionId}_item_${sectionItem.id}`
: `item_${sectionItem.id}`;
acc[saveKey] = cellValues[key] || null;
return acc;
}, {} as Record<string, CellValue | null>),
}));
const records: Array<{
section_id: number | null;
column_id: number | null;
row_index: number;
field_key: string;
field_value: string | null;
}> = [];
return {
template_id: template.id,
items,
inadequateContent,
overall_result: overallResult,
};
// ===== 1. 기본 필드 (bf_xxx) =====
if (template.basic_fields?.length > 0) {
for (const field of template.basic_fields) {
const val = resolveFieldValue(field);
if (val && val !== '-' && val !== '(입력)') {
records.push({
section_id: null, column_id: null, row_index: 0,
field_key: `bf_${field.id}`,
field_value: val,
});
}
}
}
// ===== 2. 행별 검사 데이터 =====
effectiveWorkItems.forEach((wi, rowIdx) => {
for (const col of template.columns) {
// 일련번호 컬럼 → 저장 (mng show에서 표시용)
if (isSerialColumn(col.label)) {
records.push({
section_id: null, column_id: col.id, row_index: rowIdx,
field_key: 'value', field_value: String(rowIdx + 1),
});
continue;
}
// 판정 컬럼 → 자동 계산 결과 저장
if (isJudgmentColumn(col.label)) {
const judgment = getRowJudgment(rowIdx);
if (judgment) {
records.push({
section_id: null, column_id: col.id, row_index: rowIdx,
field_key: 'value', field_value: judgment === '적' ? 'OK' : 'NG',
});
}
continue;
}
const sectionItem = columnItemMap.get(col.id);
if (!sectionItem) continue;
const sectionId = itemSectionMap.get(sectionItem.id) ?? null;
const key = `${rowIdx}-${col.id}`;
const cell = cellValues[key];
const mType = sectionItem.measurement_type || sectionItem.measurementType || '';
if (col.column_type === 'complex' && col.sub_labels) {
// 복합 컬럼: sub_label 유형별 처리
let inputIdx = 0;
for (const subLabel of col.sub_labels) {
const sl = subLabel.toLowerCase();
if (sl.includes('도면') || sl.includes('기준')) {
// 기준치 → formatStandard 결과 저장
const stdVal = formatStandard(sectionItem, wi);
records.push({
section_id: sectionId, column_id: col.id, row_index: rowIdx,
field_key: 'standard', field_value: stdVal || null,
});
} else if (sl.includes('ok') || sl.includes('ng')) {
// OK·NG → cell.status 저장
const n = inputIdx + 1;
if (mType === 'checkbox') {
records.push({
section_id: sectionId, column_id: col.id, row_index: rowIdx,
field_key: `n${n}_ok`,
field_value: cell?.status === 'good' ? 'OK' : '',
});
records.push({
section_id: sectionId, column_id: col.id, row_index: rowIdx,
field_key: `n${n}_ng`,
field_value: cell?.status === 'bad' ? 'NG' : '',
});
}
inputIdx++;
} else {
// 측정값
const n = inputIdx + 1;
const val = cell?.measurements?.[inputIdx] || null;
if (mType === 'checkbox') {
records.push({
section_id: sectionId, column_id: col.id, row_index: rowIdx,
field_key: `n${n}_ok`,
field_value: val?.toLowerCase() === 'ok' ? 'OK' : '',
});
records.push({
section_id: sectionId, column_id: col.id, row_index: rowIdx,
field_key: `n${n}_ng`,
field_value: val?.toLowerCase() === 'ng' ? 'NG' : '',
});
} else {
records.push({
section_id: sectionId, column_id: col.id, row_index: rowIdx,
field_key: `n${n}`,
field_value: val,
});
}
inputIdx++;
}
}
} else if (cell?.value !== undefined) {
records.push({
section_id: sectionId, column_id: col.id, row_index: rowIdx,
field_key: 'value', field_value: cell.value || null,
});
} else if (cell?.text !== undefined) {
records.push({
section_id: sectionId, column_id: col.id, row_index: rowIdx,
field_key: 'value', field_value: cell.text || null,
});
}
}
});
// ===== 3. 종합판정 =====
records.push({
section_id: null, column_id: null, row_index: 0,
field_key: 'overall_result', field_value: overallResult,
});
// ===== 4. 부적합 내용 (비고) =====
if (inadequateContent) {
records.push({
section_id: null, column_id: null, row_index: 0,
field_key: 'remark', field_value: inadequateContent,
});
}
return { template_id: template.id, records };
},
}));
@@ -518,9 +644,9 @@ export const TemplateInspectionContent = forwardRef<InspectionContentRef, Templa
<p className="text-sm font-bold mb-1"> {section.title || section.name}</p>
{section.image_path ? (
<img
src={section.image_path.startsWith('http') ? section.image_path : `/storage/${section.image_path}`}
src={getImageUrl(section.image_path)}
alt={section.title || section.name}
className="max-h-48 mx-auto border rounded"
className="w-full border rounded"
/>
) : (
<div className="border border-dashed border-gray-300 rounded p-6 text-center text-gray-400 text-xs">

View File

@@ -110,6 +110,8 @@ export interface WorkOrderItem {
productName: string;
floorCode: string; // 층/부호
specification: string; // 규격
width?: number; // 제작사이즈 가로 (mm) - options JSON
height?: number; // 제작사이즈 세로 (mm) - options JSON
quantity: number;
unit: string; // 단위
orderNodeId: number | null; // 개소 ID
@@ -253,6 +255,7 @@ export interface WorkOrderItemApi {
unit: string | null;
sort_order: number;
status: 'waiting' | 'in_progress' | 'completed';
options?: Record<string, unknown> | null;
created_at: string;
updated_at: string;
source_order_item?: {
@@ -456,6 +459,8 @@ export function transformApiToFrontend(api: WorkOrderApi): WorkOrder {
productName: item.item_name,
floorCode: [item.source_order_item?.floor_code, item.source_order_item?.symbol_code].filter(Boolean).join('/') || '-',
specification: item.specification || '-',
width: typeof item.options?.width === 'number' ? item.options.width : undefined,
height: typeof item.options?.height === 'number' ? item.options.height : undefined,
quantity: item.quantity,
unit: item.unit || '-',
orderNodeId: item.source_order_item?.order_node_id ?? null,