diff --git a/src/components/production/WorkOrders/actions.ts b/src/components/production/WorkOrders/actions.ts index 82b21dc8..357d938d 100644 --- a/src/components/production/WorkOrders/actions.ts +++ b/src/components/production/WorkOrders/actions.ts @@ -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: '서버 오류가 발생했습니다.' }; + } +} diff --git a/src/components/production/WorkOrders/documents/InspectionReportModal.tsx b/src/components/production/WorkOrders/documents/InspectionReportModal.tsx index 78177374..7157921e 100644 --- a/src/components/production/WorkOrders/documents/InspectionReportModal.tsx +++ b/src/components/production/WorkOrders/documents/InspectionReportModal.tsx @@ -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 }[]; - 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('검사 문서가 저장되었습니다.'); diff --git a/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx b/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx index 3a15c93b..91af866d 100644 --- a/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx +++ b/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx @@ -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 ({ 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), - })); + 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■ {section.title || section.name}

{section.image_path ? ( {section.title ) : (
diff --git a/src/components/production/WorkOrders/types.ts b/src/components/production/WorkOrders/types.ts index b220ac41..270422a3 100644 --- a/src/components/production/WorkOrders/types.ts +++ b/src/components/production/WorkOrders/types.ts @@ -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 | 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,