fix: 엑셀 내보내기 구조 개선 - 파이프 카테고리 우선 적용
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

- 모든 카테고리에서 '단위' 컬럼 제거 (수량만 사용)
- 파이프 카테고리: 납기일이 정확히 P열에 위치하도록 수정
- 파이프 전용 컬럼 구조: 크기, 스케줄, 재질, 제조방법, 사용자요구, 추가요청사항, 관리항목1~4
- createExcelBlob 함수에서 ExcelJS → XLSX 변경으로 오류 해결
- 백엔드 EXCEL_DIR 경로 수정 (exports → uploads/excel_exports)
- BOM에서 생성한 엑셀을 구매관리에서 동일하게 다운로드 가능

배포 버전: index-c08dc565.js
다음 단계: 피팅, 플랜지 카테고리 엑셀 구조 개선
This commit is contained in:
hyungi
2025-10-16 15:13:42 +09:00
parent a5bfeec9aa
commit 379af6b1e3

View File

@@ -730,6 +730,8 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
'단가': 1 // E열: 일괄 1로 설정 '단가': 1 // E열: 일괄 1로 설정
}; };
// 모든 카테고리에서 단위 컬럼 제거 (수량만 사용)
// F~O열: 카테고리별 전용 컬럼 구성 (10개 컬럼) // F~O열: 카테고리별 전용 컬럼 구성 (10개 컬럼)
if (category === 'PIPE') { if (category === 'PIPE') {
// 파이프 전용 컬럼 (F~O) - 끝단처리, 압력등급 제거 // 파이프 전용 컬럼 (F~O) - 끝단처리, 압력등급 제거
@@ -1062,71 +1064,64 @@ export const exportComparisonToExcel = (comparisonData, filename, additionalInfo
// 엑셀 Blob 생성 함수 (서버 업로드용) // 엑셀 Blob 생성 함수 (서버 업로드용)
export const createExcelBlob = async (materials, filename, options = {}) => { export const createExcelBlob = async (materials, filename, options = {}) => {
try { try {
// 기존 exportMaterialsToExcel과 동일한 로직이지만 Blob만 반환 console.log('📊 createExcelBlob 시작:', materials.length, '개 자료');
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Materials');
// 헤더 설정 // 기존 exportMaterialsToExcel 로직을 사용하되 Blob만 반환
const headers = [ const formattedData = materials.map(material => formatMaterialForExcel(material, options.category));
'TAGNO', '품목명', '수량', '통화구분', '단가'
];
// 카테고리별 추가 헤더 // 헤더 추출 및 순서 정의 (모든 카테고리에서 단위 제거)
if (options.category === 'PIPE') { const allHeaders = Array.from(new Set(formattedData.flatMap(Object.keys)));
headers.push('크기', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4', '납기일(YYYY-MM-DD)'); const fixedHeaders = ['TAGNO', '품목명', '수량', '통화구분', '단가'];
} else if (options.category === 'FITTING') { const categorySpecificHeadersOrder = {
headers.push('크기', '압력등급', '스케줄', '재질', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4', '납기일(YYYY-MM-DD)'); 'PIPE': ['크기', '스케줄', '재질', '제조방법', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
} else if (options.category === 'FLANGE') { 'FITTING': ['크기', '압력등급', '스케줄', '재질', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
headers.push('크기', '페이싱', '압력등급', '스케줄', '재질', '추가요구사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4', '납기일(YYYY-MM-DD)'); 'FLANGE': ['크기', '페이싱', '압력등급', '스케줄', '재질', '추가요구사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
} else { 'VALVE': ['크기', '압력등급', '재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
// 기본 헤더 'GASKET': ['크기', '압력등급', '구조', '재질', '두께', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
headers.push('크기', '압력등급', '재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4', '납기일(YYYY-MM-DD)'); 'BOLT': ['크기', '압력등급', '길이', '재질', '추가요구', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
} 'SUPPORT': ['크기', '압력등급', '재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
// 헤더 추가
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' };
// 데이터 추가 const deliveryDateHeader = '납기일(YYYY-MM-DD)';
materials.forEach(material => { let orderedHeaders = [...fixedHeaders];
const formattedData = formatMaterialForExcel(material, options.category || 'UNKNOWN'); if (categorySpecificHeadersOrder[options.category]) {
const rowData = headers.map(header => { orderedHeaders = orderedHeaders.concat(categorySpecificHeadersOrder[options.category]);
const key = Object.keys(formattedData).find(k => }
formattedData[k] !== undefined && orderedHeaders.push(deliveryDateHeader);
(header.includes(k) || k.includes(header.replace(/[()]/g, '').split(' ')[0]))
); // 데이터 정렬
return key ? formattedData[key] : ''; const finalData = formattedData.map(row => {
const newRow = {};
orderedHeaders.forEach(header => {
newRow[header] = row[header] !== undefined ? row[header] : '';
}); });
worksheet.addRow(rowData); return newRow;
}); });
// XLSX 워크북 생성
const workbook = XLSX.utils.book_new();
const worksheet = XLSX.utils.json_to_sheet(finalData, { header: orderedHeaders });
// 컬럼 너비 자동 조정 // 컬럼 너비 자동 조정
worksheet.columns.forEach(column => { const colWidths = orderedHeaders.map(header => ({
let maxLength = 0; wch: Math.max(
column.eachCell({ includeEmpty: true }, (cell) => { header.toString().length,
const columnLength = cell.value ? cell.value.toString().length : 10; ...finalData.map(row => (row[header] ? row[header].toString().length : 0))
if (columnLength > maxLength) { ) + 2
maxLength = columnLength; }));
} worksheet['!cols'] = colWidths;
});
column.width = Math.min(Math.max(maxLength + 2, 10), 50); XLSX.utils.book_append_sheet(workbook, worksheet, options.category || 'Materials');
});
// Blob 생성 // Blob 생성
const excelBuffer = await workbook.xlsx.writeBuffer(); const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
return new Blob([excelBuffer], { const blob = new Blob([excelBuffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
}); });
console.log('✅ createExcelBlob 완료:', blob.size, 'bytes');
return blob;
} catch (error) { } catch (error) {
console.error('엑셀 Blob 생성 실패:', error); console.error('엑셀 Blob 생성 실패:', error);
throw error; throw error;