diff --git a/frontend/src/components/bom/materials/BoltMaterialsView.jsx b/frontend/src/components/bom/materials/BoltMaterialsView.jsx index df83a54..c3b6a7b 100644 --- a/frontend/src/components/bom/materials/BoltMaterialsView.jsx +++ b/frontend/src/components/bom/materials/BoltMaterialsView.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { exportMaterialsToExcel } from '../../../utils/excelExport'; +import { exportMaterialsToExcel, createExcelBlob } from '../../../utils/excelExport'; import api from '../../../api'; import { FilterableHeader } from '../shared'; diff --git a/frontend/src/components/bom/materials/FittingMaterialsView.jsx b/frontend/src/components/bom/materials/FittingMaterialsView.jsx index 89bc56f..6e0f7ee 100644 --- a/frontend/src/components/bom/materials/FittingMaterialsView.jsx +++ b/frontend/src/components/bom/materials/FittingMaterialsView.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { exportMaterialsToExcel } from '../../../utils/excelExport'; +import { exportMaterialsToExcel, createExcelBlob } from '../../../utils/excelExport'; import api from '../../../api'; import { FilterableHeader, MaterialTable } from '../shared'; @@ -412,7 +412,18 @@ const FittingMaterialsView = ({ })); try { - // 1. 구매신청 생성 + console.log('🔄 피팅 엑셀 내보내기 시작 - 새로운 방식'); + + // 1. 먼저 클라이언트에서 엑셀 파일 생성 + console.log('📊 엑셀 Blob 생성 중...', dataWithRequirements.length, '개 자료'); + const excelBlob = await createExcelBlob(dataWithRequirements, excelFileName, { + category: 'FITTING', + filename: excelFileName, + uploadDate: new Date().toLocaleDateString() + }); + console.log('✅ 엑셀 Blob 생성 완료:', excelBlob.size, 'bytes'); + + // 2. 구매신청 생성 const allMaterialIds = selectedMaterialsData.map(m => m.id); const response = await api.post('/purchase-request/create', { file_id: fileId, @@ -433,34 +444,41 @@ const FittingMaterialsView = ({ }); if (response.data.success) { - console.log(`✅ 구매신청 완료: ${response.data.request_no}`); + console.log(`✅ 구매신청 완료: ${response.data.request_no}, request_id: ${response.data.request_id}`); - // 2. 구매신청된 자재 ID를 purchasedMaterials에 추가 + // 3. 생성된 엑셀 파일을 서버에 업로드 + console.log('📤 서버에 엑셀 파일 업로드 중...'); + const formData = new FormData(); + formData.append('excel_file', excelBlob, excelFileName); + formData.append('request_id', response.data.request_id); + formData.append('category', 'FITTING'); + + const uploadResponse = await api.post('/purchase-request/upload-excel', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + console.log('✅ 엑셀 업로드 완료:', uploadResponse.data); + if (onPurchasedMaterialsUpdate) { onPurchasedMaterialsUpdate(allMaterialIds); } } - // 3. 서버에 엑셀 파일 저장 요청 - await api.post('/files/save-excel', { - file_id: fileId, - category: 'FITTING', - materials: dataWithRequirements, - filename: excelFileName, - user_id: user?.id - }); - - // 4. 클라이언트에서 다운로드 - exportMaterialsToExcel(dataWithRequirements, excelFileName, { - category: 'FITTING', - filename: excelFileName, - uploadDate: new Date().toLocaleDateString() - }); + // 4. 클라이언트 다운로드 + const url = window.URL.createObjectURL(excelBlob); + const link = document.createElement('a'); + link.href = url; + link.download = excelFileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); alert(`구매신청 ${response.data?.request_no || ''}이 생성되고 엑셀 파일이 저장되었습니다.`); } catch (error) { console.error('엑셀 저장 또는 구매신청 실패:', error); - // 실패해도 다운로드는 진행 + // 실패 시에도 클라이언트 다운로드는 진행 exportMaterialsToExcel(dataWithRequirements, excelFileName, { category: 'FITTING', filename: excelFileName, diff --git a/frontend/src/components/bom/materials/FlangeMaterialsView.jsx b/frontend/src/components/bom/materials/FlangeMaterialsView.jsx index dfb7e21..2b52695 100644 --- a/frontend/src/components/bom/materials/FlangeMaterialsView.jsx +++ b/frontend/src/components/bom/materials/FlangeMaterialsView.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { exportMaterialsToExcel } from '../../../utils/excelExport'; +import { exportMaterialsToExcel, createExcelBlob } from '../../../utils/excelExport'; import api from '../../../api'; import { FilterableHeader, MaterialTable } from '../shared'; @@ -32,12 +32,21 @@ const FlangeMaterialsView = ({ 'SLIP_ON': 'SLIP ON FLANGE', 'SW': 'SOCKET WELD FLANGE', 'SOCKET_WELD': 'SOCKET WELD FLANGE', + 'THREADED': 'THREADED FLANGE', + 'THD': 'THREADED FLANGE', 'BLIND': 'BLIND FLANGE', + 'LAP_JOINT': 'LAP JOINT FLANGE', + 'LJ': 'LAP JOINT FLANGE', 'REDUCING': 'REDUCING FLANGE', 'ORIFICE': 'ORIFICE FLANGE', 'SPECTACLE': 'SPECTACLE BLIND', + 'SPECTACLE_BLIND': 'SPECTACLE BLIND', 'PADDLE': 'PADDLE BLIND', - 'SPACER': 'SPACER' + 'PADDLE_BLIND': 'PADDLE BLIND', + 'SPACER': 'SPACER', + 'SWIVEL': 'SWIVEL FLANGE', + 'DRIP_RING': 'DRIP RING', + 'NOZZLE': 'NOZZLE FLANGE' }; const facingTypeMap = { @@ -52,8 +61,25 @@ const FlangeMaterialsView = ({ const rawFlangeType = flangeDetails.flange_type || ''; const rawFacingType = flangeDetails.facing_type || ''; - let displayType = flangeTypeMap[rawFlangeType] || rawFlangeType || '-'; - let facingType = facingTypeMap[rawFacingType] || rawFacingType || '-'; + + // rawFlangeType에서 facing 정보 분리 (예: "WN RF" -> "WN") + let cleanFlangeType = rawFlangeType; + let extractedFacing = rawFacingType; + + // facing 정보가 flange_type에 포함된 경우 분리 + if (rawFlangeType.includes(' RF')) { + cleanFlangeType = rawFlangeType.replace(' RF', '').trim(); + if (!extractedFacing || extractedFacing === '-') extractedFacing = 'RAISED_FACE'; + } else if (rawFlangeType.includes(' FF')) { + cleanFlangeType = rawFlangeType.replace(' FF', '').trim(); + if (!extractedFacing || extractedFacing === '-') extractedFacing = 'FLAT_FACE'; + } else if (rawFlangeType.includes(' RTJ')) { + cleanFlangeType = rawFlangeType.replace(' RTJ', '').trim(); + if (!extractedFacing || extractedFacing === '-') extractedFacing = 'RING_TYPE_JOINT'; + } + + let displayType = flangeTypeMap[cleanFlangeType] || '-'; + let facingType = facingTypeMap[extractedFacing] || '-'; // Description에서 추출 if (displayType === '-') { @@ -70,8 +96,23 @@ const FlangeMaterialsView = ({ displayType = 'REDUCING FLANGE'; } else if (desc.includes('BLIND')) { displayType = 'BLIND FLANGE'; + } else if (desc.includes('WN RF') || desc.includes('WN-RF')) { + displayType = 'WELD NECK FLANGE'; + if (facingType === '-') facingType = 'RAISED FACE'; + } else if (desc.includes('WN FF') || desc.includes('WN-FF')) { + displayType = 'WELD NECK FLANGE'; + if (facingType === '-') facingType = 'FLAT FACE'; + } else if (desc.includes('WN RTJ') || desc.includes('WN-RTJ')) { + displayType = 'WELD NECK FLANGE'; + if (facingType === '-') facingType = 'RING TYPE JOINT'; } else if (desc.includes('WN')) { displayType = 'WELD NECK FLANGE'; + } else if (desc.includes('SO RF') || desc.includes('SO-RF')) { + displayType = 'SLIP ON FLANGE'; + if (facingType === '-') facingType = 'RAISED FACE'; + } else if (desc.includes('SO FF') || desc.includes('SO-FF')) { + displayType = 'SLIP ON FLANGE'; + if (facingType === '-') facingType = 'FLAT FACE'; } else if (desc.includes('SO')) { displayType = 'SLIP ON FLANGE'; } else if (desc.includes('SW')) { @@ -201,7 +242,18 @@ const FlangeMaterialsView = ({ })); try { - // 1. 구매신청 생성 + console.log('🔄 엑셀 내보내기 시작 - 새로운 방식'); + + // 1. 먼저 클라이언트에서 엑셀 파일 생성 + console.log('📊 엑셀 Blob 생성 중...', dataWithRequirements.length, '개 자료'); + const excelBlob = await createExcelBlob(dataWithRequirements, excelFileName, { + category: 'FLANGE', + filename: excelFileName, + uploadDate: new Date().toLocaleDateString() + }); + console.log('✅ 엑셀 Blob 생성 완료:', excelBlob.size, 'bytes'); + + // 2. 구매신청 생성 const allMaterialIds = selectedMaterialsData.map(m => m.id); const response = await api.post('/purchase-request/create', { file_id: fileId, @@ -222,31 +274,41 @@ const FlangeMaterialsView = ({ }); if (response.data.success) { - console.log(`✅ 구매신청 완료: ${response.data.request_no}`); + console.log(`✅ 구매신청 완료: ${response.data.request_no}, request_id: ${response.data.request_id}`); + + // 3. 생성된 엑셀 파일을 서버에 업로드 + console.log('📤 서버에 엑셀 파일 업로드 중...'); + const formData = new FormData(); + formData.append('excel_file', excelBlob, excelFileName); + formData.append('request_id', response.data.request_id); + formData.append('category', 'FLANGE'); + + const uploadResponse = await api.post('/purchase-request/upload-excel', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + console.log('✅ 엑셀 업로드 완료:', uploadResponse.data); + if (onPurchasedMaterialsUpdate) { onPurchasedMaterialsUpdate(allMaterialIds); } } - // 2. 서버에 엑셀 파일 저장 - await api.post('/files/save-excel', { - file_id: fileId, - category: 'FLANGE', - materials: dataWithRequirements, - filename: excelFileName, - user_id: user?.id - }); - - // 3. 클라이언트 다운로드 - exportMaterialsToExcel(dataWithRequirements, excelFileName, { - category: 'FLANGE', - filename: excelFileName, - uploadDate: new Date().toLocaleDateString() - }); + // 4. 클라이언트 다운로드 + const url = window.URL.createObjectURL(excelBlob); + const link = document.createElement('a'); + link.href = url; + link.download = excelFileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); alert(`구매신청 ${response.data?.request_no || ''}이 생성되고 엑셀 파일이 저장되었습니다.`); } catch (error) { console.error('엑셀 저장 또는 구매신청 실패:', error); + // 실패 시에도 클라이언트 다운로드는 진행 exportMaterialsToExcel(dataWithRequirements, excelFileName, { category: 'FLANGE', filename: excelFileName, @@ -386,42 +448,47 @@ const FlangeMaterialsView = ({