From 379af6b1e333792efc34fb9f83496ed4192987c7 Mon Sep 17 00:00:00 2001 From: hyungi Date: Thu, 16 Oct 2025 15:13:42 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=97=91=EC=85=80=20=EB=82=B4=EB=B3=B4?= =?UTF-8?q?=EB=82=B4=EA=B8=B0=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?-=20=ED=8C=8C=EC=9D=B4=ED=94=84=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=9A=B0=EC=84=A0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 모든 카테고리에서 '단위' 컬럼 제거 (수량만 사용) - 파이프 카테고리: 납기일이 정확히 P열에 위치하도록 수정 - 파이프 전용 컬럼 구조: 크기, 스케줄, 재질, 제조방법, 사용자요구, 추가요청사항, 관리항목1~4 - createExcelBlob 함수에서 ExcelJS → XLSX 변경으로 오류 해결 - 백엔드 EXCEL_DIR 경로 수정 (exports → uploads/excel_exports) - BOM에서 생성한 엑셀을 구매관리에서 동일하게 다운로드 가능 배포 버전: index-c08dc565.js 다음 단계: 피팅, 플랜지 카테고리 엑셀 구조 개선 --- frontend/src/utils/excelExport.js | 103 ++++++++++++++---------------- 1 file changed, 49 insertions(+), 54 deletions(-) diff --git a/frontend/src/utils/excelExport.js b/frontend/src/utils/excelExport.js index 595cb91..75140d0 100644 --- a/frontend/src/utils/excelExport.js +++ b/frontend/src/utils/excelExport.js @@ -730,6 +730,8 @@ const formatMaterialForExcel = (material, includeComparison = false) => { '단가': 1 // E열: 일괄 1로 설정 }; + // 모든 카테고리에서 단위 컬럼 제거 (수량만 사용) + // F~O열: 카테고리별 전용 컬럼 구성 (10개 컬럼) if (category === 'PIPE') { // 파이프 전용 컬럼 (F~O) - 끝단처리, 압력등급 제거 @@ -1062,70 +1064,63 @@ export const exportComparisonToExcel = (comparisonData, filename, additionalInfo // 엑셀 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' } + console.log('📊 createExcelBlob 시작:', materials.length, '개 자료'); + + // 기존 exportMaterialsToExcel 로직을 사용하되 Blob만 반환 + const formattedData = materials.map(material => formatMaterialForExcel(material, options.category)); + + // 헤더 추출 및 순서 정의 (모든 카테고리에서 단위 제거) + const allHeaders = Array.from(new Set(formattedData.flatMap(Object.keys))); + const fixedHeaders = ['TAGNO', '품목명', '수량', '통화구분', '단가']; + const categorySpecificHeadersOrder = { + 'PIPE': ['크기', '스케줄', '재질', '제조방법', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'], + 'FITTING': ['크기', '압력등급', '스케줄', '재질', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'], + 'FLANGE': ['크기', '페이싱', '압력등급', '스케줄', '재질', '추가요구사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'], + 'VALVE': ['크기', '압력등급', '재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'], + 'GASKET': ['크기', '압력등급', '구조', '재질', '두께', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'], + 'BOLT': ['크기', '압력등급', '길이', '재질', '추가요구', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'], + 'SUPPORT': ['크기', '압력등급', '재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'], }; - 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] : ''; + const deliveryDateHeader = '납기일(YYYY-MM-DD)'; + let orderedHeaders = [...fixedHeaders]; + if (categorySpecificHeadersOrder[options.category]) { + orderedHeaders = orderedHeaders.concat(categorySpecificHeadersOrder[options.category]); + } + orderedHeaders.push(deliveryDateHeader); + + // 데이터 정렬 + 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 => { - 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); - }); + const colWidths = orderedHeaders.map(header => ({ + wch: Math.max( + header.toString().length, + ...finalData.map(row => (row[header] ? row[header].toString().length : 0)) + ) + 2 + })); + worksheet['!cols'] = colWidths; + + XLSX.utils.book_append_sheet(workbook, worksheet, options.category || 'Materials'); // Blob 생성 - const excelBuffer = await workbook.xlsx.writeBuffer(); - return new Blob([excelBuffer], { + const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' }); + const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); + + console.log('✅ createExcelBlob 완료:', blob.size, 'bytes'); + return blob; } catch (error) { console.error('엑셀 Blob 생성 실패:', error);