feat(WEB):중간검사 정규화 데이터 저장 및 조회
- TemplateInspectionContent: 정규화 형식(section_id/column_id/field_key) 저장/조회 지원 - InspectionReportModal: 문서 데이터 조회 연동 - actions: getMaterialInputLots API 호출 추가 - types: MaterialInputLot 타입 추가
This commit is contained in:
@@ -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: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('검사 문서가 저장되었습니다.');
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user