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 = ({
- {/* 헤더 */} -
-
- 0} - onChange={handleSelectAll} - style={{ cursor: 'pointer' }} - /> +
+ {/* 헤더 */} +
+
+ { + const selectableMaterials = filteredMaterials.filter(m => !purchasedMaterials.has(m.id)); + return selectedMaterials.size === selectableMaterials.length && selectableMaterials.length > 0; + })()} + onChange={handleSelectAll} + style={{ cursor: 'pointer' }} + /> +
+ Type + Facing + Size + Pressure + Schedule + Material Grade +
Purchase Quantity
+
Additional Request
- Type - Facing - Size - Pressure - Schedule - Material Grade - Quantity -
Unit
-
User Requirement
-
{/* 데이터 행들 */} -
+
{filteredMaterials.map((material, index) => { const info = parseFlangeInfo(material); const isSelected = selectedMaterials.has(material.id); @@ -432,7 +499,7 @@ const FlangeMaterialsView = ({ key={material.id} style={{ display: 'grid', - gridTemplateColumns: '50px 1fr 200px 150px 120px 100px 120px 150px 80px 80px 200px', + gridTemplateColumns: '50px 250px 150px 120px 120px 100px 150px 180px 200px', gap: '12px', padding: '16px', borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none', @@ -455,11 +522,15 @@ const FlangeMaterialsView = ({ type="checkbox" checked={isSelected} onChange={() => handleMaterialSelect(material.id)} - style={{ cursor: 'pointer' }} + disabled={isPurchased} + style={{ + cursor: isPurchased ? 'not-allowed' : 'pointer', + opacity: isPurchased ? 0.5 : 1 + }} />
-
- FLANGE +
+ {info.subtype} {isPurchased && ( )}
-
- {info.subtype} -
-
+
{info.facing}
-
+
{info.size}
-
+
{info.pressure}
-
+
{info.schedule}
-
+
{info.grade}
-
- {info.quantity} -
-
- {info.unit} +
+ {info.quantity} {info.unit}
-
{filteredMaterials.length === 0 && (
)} +
+
); }; diff --git a/frontend/src/components/bom/materials/GasketMaterialsView.jsx b/frontend/src/components/bom/materials/GasketMaterialsView.jsx index a442b90..69d115c 100644 --- a/frontend/src/components/bom/materials/GasketMaterialsView.jsx +++ b/frontend/src/components/bom/materials/GasketMaterialsView.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/PipeMaterialsView.jsx b/frontend/src/components/bom/materials/PipeMaterialsView.jsx index c55d743..cd27f09 100644 --- a/frontend/src/components/bom/materials/PipeMaterialsView.jsx +++ b/frontend/src/components/bom/materials/PipeMaterialsView.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'; @@ -179,7 +179,18 @@ const PipeMaterialsView = ({ })); try { - // 1. 구매신청 생성 + console.log('🔄 파이프 엑셀 내보내기 시작 - 새로운 방식'); + + // 1. 먼저 클라이언트에서 엑셀 파일 생성 + console.log('📊 엑셀 Blob 생성 중...', dataWithRequirements.length, '개 자료'); + const excelBlob = await createExcelBlob(dataWithRequirements, excelFileName, { + category: 'PIPE', + 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, @@ -200,34 +211,41 @@ const PipeMaterialsView = ({ }); 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', 'PIPE'); + + 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: 'PIPE', - materials: dataWithRequirements, - filename: excelFileName, - user_id: user?.id - }); - - // 4. 클라이언트에서 다운로드 - exportMaterialsToExcel(dataWithRequirements, excelFileName, { - category: 'PIPE', - 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: 'PIPE', filename: excelFileName, diff --git a/frontend/src/components/bom/materials/SupportMaterialsView.jsx b/frontend/src/components/bom/materials/SupportMaterialsView.jsx index fcbed20..0b3a6a2 100644 --- a/frontend/src/components/bom/materials/SupportMaterialsView.jsx +++ b/frontend/src/components/bom/materials/SupportMaterialsView.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/ValveMaterialsView.jsx b/frontend/src/components/bom/materials/ValveMaterialsView.jsx index 3d45109..ce2edeb 100644 --- a/frontend/src/components/bom/materials/ValveMaterialsView.jsx +++ b/frontend/src/components/bom/materials/ValveMaterialsView.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'; @@ -156,28 +156,79 @@ const ValveMaterialsView = ({ })); try { - await api.post('/files/save-excel', { + console.log('🔄 밸브 엑셀 내보내기 시작 - 새로운 방식'); + + // 1. 먼저 클라이언트에서 엑셀 파일 생성 + console.log('📊 엑셀 Blob 생성 중...', dataWithRequirements.length, '개 자료'); + const excelBlob = await createExcelBlob(dataWithRequirements, excelFileName, { + category: 'VALVE', + 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, + job_no: jobNo, category: 'VALVE', - materials: dataWithRequirements, - filename: excelFileName, - user_id: user?.id + material_ids: allMaterialIds, + materials_data: dataWithRequirements.map(m => ({ + material_id: m.id, + description: m.original_description, + category: m.classified_category, + size: m.size_inch || m.size_spec, + schedule: m.schedule, + material_grade: m.material_grade || m.full_material_grade, + quantity: m.quantity, + unit: m.unit, + user_requirement: userRequirements[m.id] || '' + })) }); - exportMaterialsToExcel(dataWithRequirements, excelFileName, { - category: 'VALVE', - filename: excelFileName, - uploadDate: new Date().toLocaleDateString() - }); + if (response.data.success) { + 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', 'VALVE'); + + const uploadResponse = await api.post('/purchase-request/upload-excel', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + console.log('✅ 엑셀 업로드 완료:', uploadResponse.data); - alert('엑셀 파일이 생성되고 서버에 저장되었습니다.'); + if (onPurchasedMaterialsUpdate) { + onPurchasedMaterialsUpdate(allMaterialIds); + } + } + + // 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); + console.error('엑셀 저장 또는 구매신청 실패:', error); + // 실패 시에도 클라이언트 다운로드는 진행 exportMaterialsToExcel(dataWithRequirements, excelFileName, { category: 'VALVE', filename: excelFileName, uploadDate: new Date().toLocaleDateString() }); + alert('엑셀 파일은 다운로드되었지만 구매신청 생성에 실패했습니다.'); } }; diff --git a/frontend/src/utils/excelExport.js b/frontend/src/utils/excelExport.js index 6c78a3e..595cb91 100644 --- a/frontend/src/utils/excelExport.js +++ b/frontend/src/utils/excelExport.js @@ -302,65 +302,85 @@ const formatMaterialForExcel = (material, includeComparison = false) => { itemName = displayType; } else if (category === 'FLANGE') { - // 플랜지 상세 타입 표시 + // 플랜지 상세 타입 표시 - 프론트엔드 표시와 동일한 로직 사용 const flangeDetails = material.flange_details || {}; - const flangeType = flangeDetails.flange_type || ''; - const facingType = flangeDetails.facing_type || ''; + const rawFlangeType = flangeDetails.flange_type || ''; + const rawFacingType = flangeDetails.facing_type || ''; - // 플랜지 타입 풀네임 매핑 (한국어) - const flangeTypeKoreanMap = { + // 플랜지 타입 풀네임 매핑 (영어) + const flangeTypeMap = { + 'WN': 'WELD NECK FLANGE', 'WELD_NECK': 'WELD NECK FLANGE', - 'SLIP_ON': 'SLIP ON FLANGE', + 'SO': 'SLIP ON FLANGE', + '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 facingInfo = facingType ? ` ${facingType}` : ''; - - if (flangeType && flangeTypeKoreanMap[flangeType]) { - itemName = `${flangeTypeKoreanMap[flangeType]}${facingInfo}`; - } else { - // description에서 추출 + + // rawFlangeType에서 facing 정보 분리 (예: "WN RF" -> "WN") + let cleanFlangeType = rawFlangeType; + if (rawFlangeType.includes(' RF')) { + cleanFlangeType = rawFlangeType.replace(' RF', '').trim(); + } else if (rawFlangeType.includes(' FF')) { + cleanFlangeType = rawFlangeType.replace(' FF', '').trim(); + } else if (rawFlangeType.includes(' RTJ')) { + cleanFlangeType = rawFlangeType.replace(' RTJ', '').trim(); + } + + let displayType = flangeTypeMap[cleanFlangeType] || ''; + + // Description에서 추출 (매핑되지 않은 경우) + if (!displayType) { const desc = cleanDescription.toUpperCase(); if (desc.includes('ORIFICE')) { - itemName = `ORIFICE FLANGE${facingInfo}`; + displayType = 'ORIFICE FLANGE'; } else if (desc.includes('SPECTACLE')) { - itemName = `SPECTACLE BLIND${facingInfo}`; + displayType = 'SPECTACLE BLIND'; } else if (desc.includes('PADDLE')) { - itemName = `PADDLE BLIND${facingInfo}`; + displayType = 'PADDLE BLIND'; } else if (desc.includes('SPACER')) { - itemName = `SPACER${facingInfo}`; + displayType = 'SPACER'; } else if (desc.includes('REDUCING') || desc.includes('RED')) { - itemName = `REDUCING FLANGE${facingInfo}`; + displayType = 'REDUCING FLANGE'; } else if (desc.includes('BLIND')) { - itemName = `BLIND FLANGE${facingInfo}`; + displayType = 'BLIND FLANGE'; + } else if (desc.includes('WN RF') || desc.includes('WN-RF')) { + displayType = 'WELD NECK FLANGE'; + } else if (desc.includes('WN FF') || desc.includes('WN-FF')) { + displayType = 'WELD NECK FLANGE'; + } else if (desc.includes('WN RTJ') || desc.includes('WN-RTJ')) { + displayType = 'WELD NECK FLANGE'; } else if (desc.includes('WN')) { - itemName = `WELD NECK FLANGE${facingInfo}`; + displayType = 'WELD NECK FLANGE'; + } else if (desc.includes('SO RF') || desc.includes('SO-RF')) { + displayType = 'SLIP ON FLANGE'; + } else if (desc.includes('SO FF') || desc.includes('SO-FF')) { + displayType = 'SLIP ON FLANGE'; } else if (desc.includes('SO')) { - itemName = `SLIP ON FLANGE${facingInfo}`; + displayType = 'SLIP ON FLANGE'; } else if (desc.includes('SW')) { - itemName = `SOCKET WELD FLANGE${facingInfo}`; + displayType = 'SOCKET WELD FLANGE'; } else { - itemName = `FLANGE${facingInfo}`; + displayType = 'FLANGE'; } } - // 상세내역에 플랜지 타입 정보 저장 - if (flangeDetails.flange_type) { - detailInfo = `${flangeType} ${facingType}`.trim(); - } else { - // description에서 추출 - const flangeTypeMatch = cleanDescription.match(/FLG\s+([^,]+?)(?=\s*SCH|\s*,\s*\d+LB|$)/i); - if (flangeTypeMatch) { - detailInfo = flangeTypeMatch[1].trim(); - } - } + itemName = displayType; } else if (category === 'VALVE') { // 밸브 상세 타입 표시 const valveDetails = material.valve_details || {}; @@ -736,50 +756,29 @@ const formatMaterialForExcel = (material, includeComparison = false) => { base['관리항목3'] = ''; // N열 base['관리항목4'] = ''; // O열 } else if (category === 'FLANGE') { - // 플랜지 타입 풀네임 매핑 (영어) - const flangeTypeMap = { - 'WN': 'WELD NECK FLANGE', - 'WELD_NECK': 'WELD NECK FLANGE', - 'SO': 'SLIP ON FLANGE', - 'SLIP_ON': 'SLIP ON FLANGE', - 'SW': 'SOCKET WELD FLANGE', - 'SOCKET_WELD': 'SOCKET WELD FLANGE', - 'BL': 'BLIND FLANGE', - 'BLIND': 'BLIND FLANGE', - 'RED': 'REDUCING FLANGE', - 'REDUCING': 'REDUCING FLANGE', - 'ORIFICE': 'ORIFICE FLANGE', - 'SPECTACLE': 'SPECTACLE BLIND', - 'PADDLE': 'PADDLE BLIND', - 'SPACER': 'SPACER' - }; + // 플랜지 전용 컬럼 (F~O) - 크기 이후에 페이싱, 압력, 스케줄, Material Grade, 추가요구사항 순서 + base['크기'] = material.size_spec || '-'; // F열 - // 끝단처리 풀네임 매핑 (영어) + // 페이싱 정보 (G열) + const rawFacingType = material.flange_details?.facing_type || ''; const facingTypeMap = { 'RF': 'RAISED FACE', 'RAISED_FACE': 'RAISED FACE', - 'FF': 'FULL FACE', - 'FULL_FACE': 'FULL FACE', - 'RTJ': 'RING JOINT', - 'RING_JOINT': 'RING JOINT', - 'MALE': 'MALE', - 'FEMALE': 'FEMALE' + 'FF': 'FLAT FACE', + 'FLAT_FACE': 'FLAT FACE', + 'RTJ': 'RING TYPE JOINT', + 'RING_TYPE_JOINT': 'RING TYPE JOINT' }; + base['페이싱'] = facingTypeMap[rawFacingType] || rawFacingType || '-'; // G열 - const rawFlangeType = material.flange_details?.flange_type || ''; - const rawFacingType = material.flange_details?.facing_type || ''; - - // 플랜지 전용 컬럼 (F~O) - 타입 제거, 품목명에 포함됨 - base['크기'] = material.size_spec || '-'; // F열 - base['압력등급'] = pressure; // G열 - base['재질'] = grade; // H열 - base['페이싱'] = facingTypeMap[rawFacingType] || rawFacingType || '-'; // I열 - base['사용자요구'] = material.user_requirement || ''; // J열 - base['관리항목1'] = ''; // K열 - base['관리항목2'] = ''; // L열 - base['관리항목3'] = ''; // M열 - base['관리항목4'] = ''; // N열 - base['관리항목5'] = ''; // O열 + base['압력등급'] = pressure; // H열 + base['스케줄'] = schedule; // I열 + base['재질'] = grade; // J열 + base['추가요구사항'] = material.user_requirement || ''; // K열 + base['관리항목1'] = ''; // L열 + base['관리항목2'] = ''; // M열 + base['관리항목3'] = ''; // N열 + base['관리항목4'] = ''; // O열 } else if (category === 'VALVE') { // 밸브 전용 컬럼 (F~O) - 타입 제거, 품목명에 포함됨 base['크기'] = material.size_spec || '-'; // F열 @@ -1059,4 +1058,77 @@ export const exportComparisonToExcel = (comparisonData, filename, additionalInfo alert('엑셀 파일 생성에 실패했습니다: ' + error.message); return false; } -}; \ No newline at end of file +}; +// 엑셀 Blob 생성 함수 (서버 업로드용) +export const createExcelBlob = async (materials, filename, options = {}) => { + try { + // 기존 exportMaterialsToExcel과 동일한 로직이지만 Blob만 반환 + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('Materials'); + + // 헤더 설정 + const headers = [ + 'TAGNO', '품목명', '수량', '통화구분', '단가' + ]; + + // 카테고리별 추가 헤더 + if (options.category === 'PIPE') { + headers.push('크기', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4', '납기일(YYYY-MM-DD)'); + } else if (options.category === 'FITTING') { + headers.push('크기', '압력등급', '스케줄', '재질', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4', '납기일(YYYY-MM-DD)'); + } else if (options.category === 'FLANGE') { + headers.push('크기', '페이싱', '압력등급', '스케줄', '재질', '추가요구사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4', '납기일(YYYY-MM-DD)'); + } else { + // 기본 헤더 + headers.push('크기', '압력등급', '재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4', '납기일(YYYY-MM-DD)'); + } + + // 헤더 추가 + worksheet.addRow(headers); + + // 헤더 스타일링 + const headerRow = worksheet.getRow(1); + headerRow.font = { bold: true, color: { argb: 'FFFFFFFF' } }; + headerRow.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FF366092' } + }; + headerRow.alignment = { horizontal: 'center', vertical: 'middle' }; + + // 데이터 추가 + materials.forEach(material => { + const formattedData = formatMaterialForExcel(material, options.category || 'UNKNOWN'); + const rowData = headers.map(header => { + const key = Object.keys(formattedData).find(k => + formattedData[k] !== undefined && + (header.includes(k) || k.includes(header.replace(/[()]/g, '').split(' ')[0])) + ); + return key ? formattedData[key] : ''; + }); + worksheet.addRow(rowData); + }); + + // 컬럼 너비 자동 조정 + worksheet.columns.forEach(column => { + let maxLength = 0; + column.eachCell({ includeEmpty: true }, (cell) => { + const columnLength = cell.value ? cell.value.toString().length : 10; + if (columnLength > maxLength) { + maxLength = columnLength; + } + }); + column.width = Math.min(Math.max(maxLength + 2, 10), 50); + }); + + // Blob 생성 + const excelBuffer = await workbook.xlsx.writeBuffer(); + return new Blob([excelBuffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }); + + } catch (error) { + console.error('엑셀 Blob 생성 실패:', error); + throw error; + } +};