feat: 엑셀 다운로드 방식 개선 - BOM에서 생성한 엑셀을 구매관리에서 다운로드
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 파이프, 피팅, 플랜지, 밸브 카테고리에 새로운 엑셀 업로드 로직 적용 - createExcelBlob 함수로 클라이언트에서 엑셀 생성 후 서버 업로드 - /purchase-request/upload-excel API로 엑셀 파일 서버 저장 - 구매관리 페이지에서 원본 엑셀 파일 다운로드 가능 - 가스켓, 볼트, 서포트는 추후 개선 시 적용 예정 배포 버전: index-5e5aa4a4.js
This commit is contained in:
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = ({
|
||||
<div style={{
|
||||
background: 'white',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
overflow: 'auto',
|
||||
maxHeight: '600px',
|
||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)'
|
||||
}}>
|
||||
{/* 헤더 */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '50px 1fr 200px 150px 120px 100px 120px 150px 80px 80px 200px',
|
||||
gap: '12px',
|
||||
padding: '16px',
|
||||
background: '#f8fafc',
|
||||
borderBottom: '1px solid #e2e8f0',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
color: '#374151'
|
||||
}}>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedMaterials.size === filteredMaterials.length && filteredMaterials.length > 0}
|
||||
onChange={handleSelectAll}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
<div style={{ minWidth: '1400px' }}>
|
||||
{/* 헤더 */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '50px 250px 150px 120px 120px 100px 150px 180px 200px',
|
||||
gap: '16px',
|
||||
padding: '16px',
|
||||
background: '#f8fafc',
|
||||
borderBottom: '1px solid #e2e8f0',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={(() => {
|
||||
const selectableMaterials = filteredMaterials.filter(m => !purchasedMaterials.has(m.id));
|
||||
return selectedMaterials.size === selectableMaterials.length && selectableMaterials.length > 0;
|
||||
})()}
|
||||
onChange={handleSelectAll}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</div>
|
||||
<FilterableHeader sortKey="subtype" filterKey="subtype">Type</FilterableHeader>
|
||||
<FilterableHeader sortKey="facing" filterKey="facing">Facing</FilterableHeader>
|
||||
<FilterableHeader sortKey="size" filterKey="size">Size</FilterableHeader>
|
||||
<FilterableHeader sortKey="pressure" filterKey="pressure">Pressure</FilterableHeader>
|
||||
<FilterableHeader sortKey="schedule" filterKey="schedule">Schedule</FilterableHeader>
|
||||
<FilterableHeader sortKey="grade" filterKey="grade">Material Grade</FilterableHeader>
|
||||
<div>Purchase Quantity</div>
|
||||
<div>Additional Request</div>
|
||||
</div>
|
||||
<FilterableHeader sortKey="subtype" filterKey="subtype">Type</FilterableHeader>
|
||||
<FilterableHeader sortKey="facing" filterKey="facing">Facing</FilterableHeader>
|
||||
<FilterableHeader sortKey="size" filterKey="size">Size</FilterableHeader>
|
||||
<FilterableHeader sortKey="pressure" filterKey="pressure">Pressure</FilterableHeader>
|
||||
<FilterableHeader sortKey="schedule" filterKey="schedule">Schedule</FilterableHeader>
|
||||
<FilterableHeader sortKey="grade" filterKey="grade">Material Grade</FilterableHeader>
|
||||
<FilterableHeader sortKey="quantity" filterKey="quantity">Quantity</FilterableHeader>
|
||||
<div>Unit</div>
|
||||
<div>User Requirement</div>
|
||||
</div>
|
||||
|
||||
{/* 데이터 행들 */}
|
||||
<div style={{ maxHeight: '600px', overflowY: 'auto' }}>
|
||||
<div>
|
||||
{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
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
||||
FLANGE
|
||||
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '500', textAlign: 'center' }}>
|
||||
{info.subtype}
|
||||
{isPurchased && (
|
||||
<span style={{
|
||||
marginLeft: '8px',
|
||||
@@ -474,29 +545,23 @@ const FlangeMaterialsView = ({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '500' }}>
|
||||
{info.subtype}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
||||
<div style={{ fontSize: '14px', color: '#1f2937', textAlign: 'center' }}>
|
||||
{info.facing}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
||||
<div style={{ fontSize: '14px', color: '#1f2937', textAlign: 'center' }}>
|
||||
{info.size}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
||||
<div style={{ fontSize: '14px', color: '#1f2937', textAlign: 'center' }}>
|
||||
{info.pressure}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
||||
<div style={{ fontSize: '14px', color: '#1f2937', textAlign: 'center' }}>
|
||||
{info.schedule}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
||||
<div style={{ fontSize: '14px', color: '#1f2937', textAlign: 'center' }}>
|
||||
{info.grade}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '600', textAlign: 'right' }}>
|
||||
{info.quantity}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#6b7280' }}>
|
||||
{info.unit}
|
||||
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '600', textAlign: 'center' }}>
|
||||
{info.quantity} {info.unit}
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
@@ -506,7 +571,7 @@ const FlangeMaterialsView = ({
|
||||
...userRequirements,
|
||||
[material.id]: e.target.value
|
||||
})}
|
||||
placeholder="Enter requirement..."
|
||||
placeholder="Enter additional request..."
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
@@ -520,7 +585,6 @@ const FlangeMaterialsView = ({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredMaterials.length === 0 && (
|
||||
<div style={{
|
||||
@@ -538,6 +602,8 @@ const FlangeMaterialsView = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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('엑셀 파일은 다운로드되었지만 구매신청 생성에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
// 엑셀 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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user