파이프 길이 계산 및 엑셀 내보내기 버그 수정
- 자재 비교에서 파이프 길이 합산 로직 수정 - 프론트엔드에서 혼란스러운 '평균단위' 표시 제거 - 파이프 변경사항에 실제 이전/현재 총길이 표시 - 엑셀 내보내기에서 '초기화되지 않은 변수' 오류 수정 - 리비전 비교에서 파이프 길이 변화 계산 개선
This commit is contained in:
295
frontend/src/utils/excelExport.js
Normal file
295
frontend/src/utils/excelExport.js
Normal file
@@ -0,0 +1,295 @@
|
||||
import * as XLSX from 'xlsx';
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
/**
|
||||
* 자재 목록을 카테고리별로 그룹화
|
||||
*/
|
||||
const groupMaterialsByCategory = (materials) => {
|
||||
const groups = {};
|
||||
|
||||
materials.forEach(material => {
|
||||
const category = material.classified_category || material.category || 'UNCATEGORIZED';
|
||||
if (!groups[category]) {
|
||||
groups[category] = [];
|
||||
}
|
||||
groups[category].push(material);
|
||||
});
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
/**
|
||||
* 동일한 자재끼리 합치기 (자재 설명 + 사이즈 기준)
|
||||
*/
|
||||
const consolidateMaterials = (materials, isComparison = false) => {
|
||||
const consolidated = {};
|
||||
|
||||
materials.forEach(material => {
|
||||
const category = material.classified_category || material.category || 'UNCATEGORIZED';
|
||||
const description = material.original_description || material.description || '';
|
||||
const sizeSpec = material.size_spec || '';
|
||||
|
||||
// 그룹화 키: 카테고리 + 자재설명 + 사이즈
|
||||
const groupKey = `${category}|${description}|${sizeSpec}`;
|
||||
|
||||
if (!consolidated[groupKey]) {
|
||||
consolidated[groupKey] = {
|
||||
...material,
|
||||
quantity: 0,
|
||||
totalLength: 0, // 파이프용
|
||||
itemCount: 0, // 파이프 개수
|
||||
lineNumbers: [], // 라인 번호들
|
||||
// 비교 모드용
|
||||
previous_quantity: 0,
|
||||
current_quantity: 0,
|
||||
quantity_change: 0
|
||||
};
|
||||
}
|
||||
|
||||
const group = consolidated[groupKey];
|
||||
group.quantity += material.quantity || 0;
|
||||
|
||||
// 비교 모드인 경우 이전/현재 수량도 합산
|
||||
if (isComparison) {
|
||||
group.previous_quantity += material.previous_quantity || 0;
|
||||
group.current_quantity += material.current_quantity || 0;
|
||||
group.quantity_change += material.quantity_change || 0;
|
||||
}
|
||||
|
||||
// 파이프인 경우 길이 계산
|
||||
if (category === 'PIPE') {
|
||||
const lengthMm = material.pipe_details?.length_mm || 0;
|
||||
const lengthM = lengthMm / 1000;
|
||||
|
||||
if (isComparison) {
|
||||
// 비교 모드에서는 이전/현재 길이 계산
|
||||
group.totalLength += lengthM * (material.current_quantity || material.quantity || 0);
|
||||
group.itemCount += material.current_quantity || material.quantity || 0;
|
||||
|
||||
// 이전 길이도 저장
|
||||
if (!group.previousTotalLength) group.previousTotalLength = 0;
|
||||
group.previousTotalLength += lengthM * (material.previous_quantity || 0);
|
||||
} else {
|
||||
group.totalLength += lengthM * (material.quantity || 0);
|
||||
group.itemCount += material.quantity || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 라인 번호 수집
|
||||
if (material.line_number) {
|
||||
group.lineNumbers.push(material.line_number);
|
||||
}
|
||||
});
|
||||
|
||||
// 라인 번호를 문자열로 변환
|
||||
Object.values(consolidated).forEach(group => {
|
||||
group.line_number = group.lineNumbers.length > 0
|
||||
? group.lineNumbers.join(', ')
|
||||
: '-';
|
||||
});
|
||||
|
||||
return Object.values(consolidated);
|
||||
};
|
||||
|
||||
/**
|
||||
* 자재 데이터를 엑셀용 형태로 변환
|
||||
*/
|
||||
const formatMaterialForExcel = (material, includeComparison = false) => {
|
||||
const category = material.classified_category || material.category || '-';
|
||||
const isPipe = category === 'PIPE';
|
||||
|
||||
const base = {
|
||||
'카테고리': category,
|
||||
'자재 설명': material.original_description || material.description || '-',
|
||||
'사이즈': material.size_spec || '-',
|
||||
'라인 번호': material.line_number || '-'
|
||||
};
|
||||
|
||||
// 파이프인 경우 길이(m) 표시, 그 외는 수량
|
||||
if (isPipe) {
|
||||
// consolidateMaterials에서 이미 계산된 totalLength 사용
|
||||
const totalLength = material.totalLength || 0;
|
||||
const itemCount = material.itemCount || material.quantity || 0;
|
||||
|
||||
base['길이(m)'] = totalLength > 0 ? totalLength.toFixed(2) : 0;
|
||||
base['개수'] = itemCount;
|
||||
base['단위'] = 'M';
|
||||
} else {
|
||||
base['수량'] = material.quantity || 0;
|
||||
base['단위'] = material.unit || 'EA';
|
||||
}
|
||||
|
||||
// 비교 모드인 경우 추가 정보
|
||||
if (includeComparison) {
|
||||
if (material.previous_quantity !== undefined) {
|
||||
if (isPipe) {
|
||||
const prevTotalLength = material.previousTotalLength || 0;
|
||||
const currTotalLength = material.totalLength || 0;
|
||||
|
||||
base['이전 길이(m)'] = prevTotalLength > 0 ? prevTotalLength.toFixed(2) : 0;
|
||||
base['현재 길이(m)'] = currTotalLength > 0 ? currTotalLength.toFixed(2) : 0;
|
||||
base['길이 변경(m)'] = ((currTotalLength - prevTotalLength)).toFixed(2);
|
||||
base['이전 개수'] = material.previous_quantity;
|
||||
base['현재 개수'] = material.current_quantity;
|
||||
} else {
|
||||
base['이전 수량'] = material.previous_quantity;
|
||||
base['현재 수량'] = material.current_quantity;
|
||||
base['변경량'] = material.quantity_change;
|
||||
}
|
||||
}
|
||||
base['변경 유형'] = material.change_type || (
|
||||
material.previous_quantity !== undefined ? '수량 변경' :
|
||||
material.quantity_change === undefined ? '신규' : '변경'
|
||||
);
|
||||
}
|
||||
|
||||
return base;
|
||||
};
|
||||
|
||||
/**
|
||||
* 일반 자재 목록 엑셀 내보내기
|
||||
*/
|
||||
export const exportMaterialsToExcel = (materials, filename, additionalInfo = {}) => {
|
||||
try {
|
||||
// 카테고리별로 그룹화
|
||||
const categoryGroups = groupMaterialsByCategory(materials);
|
||||
|
||||
// 전체 자재 합치기 (먼저 계산)
|
||||
const consolidatedMaterials = consolidateMaterials(materials);
|
||||
|
||||
// 새 워크북 생성
|
||||
const workbook = XLSX.utils.book_new();
|
||||
|
||||
// 요약 시트 생성
|
||||
const summaryData = [
|
||||
['파일 정보', ''],
|
||||
['파일명', additionalInfo.filename || ''],
|
||||
['Job No', additionalInfo.jobNo || ''],
|
||||
['리비전', additionalInfo.revision || ''],
|
||||
['업로드일', additionalInfo.uploadDate || new Date().toLocaleDateString()],
|
||||
['총 자재 수', consolidatedMaterials.length],
|
||||
['', ''],
|
||||
['카테고리별 요약', ''],
|
||||
['카테고리', '수량']
|
||||
];
|
||||
|
||||
// 카테고리별 요약 추가 (합쳐진 자재 기준)
|
||||
Object.entries(categoryGroups).forEach(([category, items]) => {
|
||||
const consolidatedItems = consolidateMaterials(items);
|
||||
summaryData.push([category, consolidatedItems.length]);
|
||||
});
|
||||
|
||||
const summarySheet = XLSX.utils.aoa_to_sheet(summaryData);
|
||||
XLSX.utils.book_append_sheet(workbook, summarySheet, '요약');
|
||||
|
||||
// 전체 자재 시트 (합쳐진 자재)
|
||||
const allMaterialsFormatted = consolidatedMaterials.map(material => formatMaterialForExcel(material));
|
||||
const allSheet = XLSX.utils.json_to_sheet(allMaterialsFormatted);
|
||||
XLSX.utils.book_append_sheet(workbook, allSheet, '전체 자재');
|
||||
|
||||
// 카테고리별 시트 생성 (합쳐진 자재)
|
||||
Object.entries(categoryGroups).forEach(([category, items]) => {
|
||||
const consolidatedItems = consolidateMaterials(items);
|
||||
const formattedItems = consolidatedItems.map(material => formatMaterialForExcel(material));
|
||||
const categorySheet = XLSX.utils.json_to_sheet(formattedItems);
|
||||
|
||||
// 시트명에서 특수문자 제거 (엑셀 시트명 규칙)
|
||||
const sheetName = category.replace(/[\\\/\*\?\[\]]/g, '_').substring(0, 31);
|
||||
XLSX.utils.book_append_sheet(workbook, categorySheet, sheetName);
|
||||
});
|
||||
|
||||
// 파일 저장
|
||||
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
||||
const data = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
|
||||
const finalFilename = `${filename}_${new Date().toISOString().split('T')[0]}.xlsx`;
|
||||
saveAs(data, finalFilename);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('엑셀 내보내기 실패:', error);
|
||||
alert('엑셀 파일 생성에 실패했습니다: ' + error.message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 리비전 비교 결과 엑셀 내보내기
|
||||
*/
|
||||
export const exportComparisonToExcel = (comparisonData, filename, additionalInfo = {}) => {
|
||||
try {
|
||||
const { summary, new_items = [], modified_items = [], removed_items = [] } = comparisonData;
|
||||
|
||||
// 새 워크북 생성
|
||||
const workbook = XLSX.utils.book_new();
|
||||
|
||||
// 요약 시트
|
||||
const summaryData = [
|
||||
['리비전 비교 정보', ''],
|
||||
['Job No', additionalInfo.jobNo || ''],
|
||||
['현재 리비전', additionalInfo.currentRevision || ''],
|
||||
['이전 리비전', additionalInfo.previousRevision || ''],
|
||||
['비교일', new Date().toLocaleDateString()],
|
||||
['', ''],
|
||||
['비교 결과 요약', ''],
|
||||
['구분', '건수'],
|
||||
['총 현재 자재', summary?.total_current_items || 0],
|
||||
['총 이전 자재', summary?.total_previous_items || 0],
|
||||
['신규 자재', summary?.new_items_count || 0],
|
||||
['변경 자재', summary?.modified_items_count || 0],
|
||||
['삭제 자재', summary?.removed_items_count || 0]
|
||||
];
|
||||
|
||||
const summarySheet = XLSX.utils.aoa_to_sheet(summaryData);
|
||||
XLSX.utils.book_append_sheet(workbook, summarySheet, '비교 요약');
|
||||
|
||||
// 신규 자재 시트 (카테고리별, 합쳐진 자재)
|
||||
if (new_items.length > 0) {
|
||||
const newItemsGroups = groupMaterialsByCategory(new_items);
|
||||
Object.entries(newItemsGroups).forEach(([category, items]) => {
|
||||
const consolidatedItems = consolidateMaterials(items, true);
|
||||
const formattedItems = consolidatedItems.map(material => formatMaterialForExcel(material, true));
|
||||
const sheet = XLSX.utils.json_to_sheet(formattedItems);
|
||||
const sheetName = `신규_${category.replace(/[\\\/\*\?\[\]]/g, '_')}`.substring(0, 31);
|
||||
XLSX.utils.book_append_sheet(workbook, sheet, sheetName);
|
||||
});
|
||||
}
|
||||
|
||||
// 변경 자재 시트 (카테고리별, 합쳐진 자재)
|
||||
if (modified_items.length > 0) {
|
||||
const modifiedItemsGroups = groupMaterialsByCategory(modified_items);
|
||||
Object.entries(modifiedItemsGroups).forEach(([category, items]) => {
|
||||
const consolidatedItems = consolidateMaterials(items, true);
|
||||
const formattedItems = consolidatedItems.map(material => formatMaterialForExcel(material, true));
|
||||
const sheet = XLSX.utils.json_to_sheet(formattedItems);
|
||||
const sheetName = `변경_${category.replace(/[\\\/\*\?\[\]]/g, '_')}`.substring(0, 31);
|
||||
XLSX.utils.book_append_sheet(workbook, sheet, sheetName);
|
||||
});
|
||||
}
|
||||
|
||||
// 삭제 자재 시트 (카테고리별, 합쳐진 자재)
|
||||
if (removed_items.length > 0) {
|
||||
const removedItemsGroups = groupMaterialsByCategory(removed_items);
|
||||
Object.entries(removedItemsGroups).forEach(([category, items]) => {
|
||||
const consolidatedItems = consolidateMaterials(items, true);
|
||||
const formattedItems = consolidatedItems.map(material => formatMaterialForExcel(material, true));
|
||||
const sheet = XLSX.utils.json_to_sheet(formattedItems);
|
||||
const sheetName = `삭제_${category.replace(/[\\\/\*\?\[\]]/g, '_')}`.substring(0, 31);
|
||||
XLSX.utils.book_append_sheet(workbook, sheet, sheetName);
|
||||
});
|
||||
}
|
||||
|
||||
// 파일 저장
|
||||
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
||||
const data = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
|
||||
const finalFilename = `${filename}_비교결과_${new Date().toISOString().split('T')[0]}.xlsx`;
|
||||
saveAs(data, finalFilename);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('엑셀 내보내기 실패:', error);
|
||||
alert('엑셀 파일 생성에 실패했습니다: ' + error.message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user