Files
TK-BOM-Project/frontend/src/utils/excelExport.js
Hyungi Ahn 72126ef78d fix: 구매신청 엑셀 수량 표시 개선 및 FLANGE 품목명 개선
- 구매신청 관리 페이지 수량을 정수로 표시 (3.000 EA → 3 EA)
- JSON 저장 시 수량 정수 변환
- FLANGE 품목명 세분화: WN RF, SO RF, ORIFICE FLANGE, SPECTACLE BLIND 등
- 구매관리 페이지 엑셀 다운로드 데이터 구조 개선
- 디버그 로그 추가
2025-10-14 15:29:01 +09:00

748 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as XLSX from 'xlsx';
import { saveAs } from 'file-saver';
import { calculatePurchaseQuantity } from './purchaseCalculator';
/**
* 자재 목록을 카테고리별로 그룹화
*/
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;
};
/**
* 동일한 자재끼리 합치기 (자재 설명 + 사이즈 기준)
* 엑셀 내보내기용 특별 처리:
* - PIPE: 끝단 정보 제거 (BOE-POE, POE-TOE 등)
* - NIPPLE: 길이별 구분 (75mm, 100mm 등)
*/
const consolidateMaterials = (materials, isComparison = false) => {
const consolidated = {};
materials.forEach(material => {
const category = material.classified_category || material.category || 'UNCATEGORIZED';
let description = material.original_description || material.description || '';
const sizeSpec = material.size_spec || '';
// 파이프 끝단 정보 제거 (엑셀 내보내기용)
if (category === 'PIPE') {
description = description
.replace(/\s+(BOE-POE|POE-TOE|BOE-TOE|BOE-BBE|BBE-POE|BBE-BBE|POE-POE|TOE-TOE)\s*$/i, '')
.replace(/\s+(BOE|POE|TOE|BBE)\s*-\s*(BOE|POE|TOE|BBE)\s*$/i, '')
.trim();
}
// 니플의 경우 길이 정보를 그룹화 키에 포함
let lengthInfo = '';
if (category === 'FITTING' && description.toLowerCase().includes('nipple')) {
const lengthMatch = description.match(/(\d+)\s*mm/i);
if (lengthMatch) {
lengthInfo = `_${lengthMatch[1]}mm`;
}
}
// 그룹화 키: 카테고리 + 정제된자재설명 + 사이즈 + 길이정보
const groupKey = `${category}|${description}|${sizeSpec}${lengthInfo}`;
if (!consolidated[groupKey]) {
consolidated[groupKey] = {
...material,
// 정제된 설명으로 덮어쓰기
original_description: description,
description: description,
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 || '-';
// 엑셀용 자재 설명 정제
let cleanDescription = material.original_description || material.description || '-';
// 파이프 끝단 정보 제거
if (category === 'PIPE') {
cleanDescription = cleanDescription
.replace(/\s+(BOE-POE|POE-TOE|BOE-TOE|BOE-BBE|BBE-POE|BBE-BBE|POE-POE|TOE-TOE)\s*$/i, '')
.replace(/\s+(BOE|POE|TOE|BBE)\s*-\s*(BOE|POE|TOE|BBE)\s*$/i, '')
.trim();
}
// 니플의 경우 길이 정보 명시적 추가
if (category === 'FITTING' && cleanDescription.toLowerCase().includes('nipple')) {
if (material.fitting_details && material.fitting_details.length_mm) {
const lengthMm = Math.round(material.fitting_details.length_mm);
if (!cleanDescription.match(/\d+\s*mm/i)) {
cleanDescription += ` ${lengthMm}mm`;
}
} else {
const lengthMatch = material.original_description?.match(/(\d+)\s*mm/i);
if (lengthMatch && !cleanDescription.match(/\d+\s*mm/i)) {
cleanDescription += ` ${lengthMatch[1]}mm`;
}
}
}
// 구매 수량 계산
const purchaseInfo = calculatePurchaseQuantity(material);
// 품목명 생성 (카테고리별 상세 처리)
let itemName = '';
if (category === 'PIPE') {
itemName = material.pipe_details?.manufacturing_method || 'PIPE';
} else if (category === 'FITTING') {
itemName = material.fitting_details?.fitting_type || 'FITTING';
} else if (category === 'FLANGE') {
// 플랜지 타입 추출
const desc = cleanDescription.toUpperCase();
console.log('🔍 FLANGE 품목명 추출:', cleanDescription);
if (material.flange_details) {
console.log(' flange_details:', material.flange_details);
const flangeType = material.flange_details.flange_type || '';
const originalFlangeType = material.flange_details.original_flange_type || '';
const facingType = material.flange_details.facing_type || '';
// 특수 플랜지 타입 우선
if (desc.includes('ORIFICE')) {
itemName = 'ORIFICE FLANGE';
} else if (desc.includes('SPECTACLE')) {
itemName = 'SPECTACLE BLIND';
} else if (desc.includes('PADDLE')) {
itemName = 'PADDLE BLIND';
} else if (desc.includes('SPACER')) {
itemName = 'SPACER';
} else if (desc.includes('BLIND')) {
itemName = 'BLIND FLANGE';
} else {
// 일반 플랜지: flange_type 사용 (WN RF, SO RF 등)
itemName = flangeType || 'FLANGE';
}
} else {
console.log(' flange_details 없음, description에서 추출');
// flange_details가 없으면 description에서 추출
if (desc.includes('ORIFICE')) {
itemName = 'ORIFICE FLANGE';
} else if (desc.includes('SPECTACLE') || desc.includes('SPEC')) {
itemName = 'SPECTACLE BLIND';
} else if (desc.includes('PADDLE')) {
itemName = 'PADDLE BLIND';
} else if (desc.includes('SPACER')) {
itemName = 'SPACER';
} else if (desc.includes('BLIND')) {
itemName = 'BLIND FLANGE';
} else if (desc.includes(' SW') || desc.includes(',SW') || desc.includes(', SW')) {
itemName = 'FLANGE SW';
} else if (desc.includes(' BW') || desc.includes(',BW') || desc.includes(', BW')) {
itemName = 'FLANGE BW';
} else if (desc.includes('RTJ')) {
itemName = 'FLANGE RTJ';
} else if (desc.includes(' FF') || desc.includes('FULL FACE')) {
itemName = 'FLANGE FF';
} else if (desc.includes(' RF') || desc.includes('RAISED')) {
itemName = 'FLANGE RF';
} else {
itemName = 'FLANGE';
}
}
console.log(' → 품목명:', itemName);
} else if (category === 'VALVE') {
itemName = 'VALVE';
} else if (category === 'GASKET') {
// 가스켓 상세 타입 추출
if (material.gasket_details) {
const gasketType = material.gasket_details.gasket_type || '';
const gasketSubtype = material.gasket_details.gasket_subtype || '';
if (gasketSubtype && gasketSubtype !== gasketType) {
itemName = gasketSubtype;
} else if (gasketType) {
itemName = gasketType;
} else {
itemName = 'GASKET';
}
} else {
// gasket_details가 없으면 description에서 추출
if (cleanDescription.includes('SWG') || cleanDescription.includes('SPIRAL')) {
itemName = 'SWG';
} else if (cleanDescription.includes('RTJ') || cleanDescription.includes('RING')) {
itemName = 'RTJ';
} else if (cleanDescription.includes('FF') || cleanDescription.includes('FULL FACE')) {
itemName = 'FF';
} else if (cleanDescription.includes('RF') || cleanDescription.includes('RAISED')) {
itemName = 'RF';
} else {
itemName = 'GASKET';
}
}
} else if (category === 'BOLT') {
itemName = 'BOLT';
} else if (category === 'SUPPORT' || category === 'U_BOLT') {
// SUPPORT 카테고리: 타입별 구분
const desc = cleanDescription.toUpperCase();
if (desc.includes('URETHANE') || desc.includes('BLOCK SHOE')) {
itemName = 'URETHANE BLOCK SHOE';
} else if (desc.includes('CLAMP')) {
itemName = 'CLAMP';
} else if (desc.includes('U-BOLT') || desc.includes('U BOLT')) {
itemName = 'U-BOLT';
} else {
itemName = 'SUPPORT';
}
} else {
itemName = category || 'UNKNOWN';
}
// 압력 등급 추출 (카테고리별 처리)
let pressure = '-';
if (category === 'GASKET') {
// 가스켓의 경우 gasket_details에서 압력등급 추출
if (material.gasket_details && material.gasket_details.pressure_rating) {
pressure = material.gasket_details.pressure_rating;
} else {
// gasket_details가 없으면 description에서 추출
const pressureMatch = cleanDescription.match(/(\d+)LB/i);
if (pressureMatch) {
pressure = `${pressureMatch[1]}LB`;
}
}
} else {
// 다른 카테고리는 기존 방식
const pressureMatch = cleanDescription.match(/(\d+)LB/i);
if (pressureMatch) {
pressure = `${pressureMatch[1]}LB`;
}
}
// 스케줄/길이 추출 (카테고리별 처리)
let schedule = '-';
if (category === 'BOLT') {
// 볼트의 경우 길이 정보 추출
const lengthPatterns = [
/(\d+(?:\.\d+)?)\s*LG/i, // 145.0000 LG, 75 LG
/(\d+(?:\.\d+)?)\s*mm/i, // 50mm
/(\d+(?:\.\d+)?)\s*MM/i, // 50MM
/,\s*(\d+(?:\.\d+)?)\s*LG/i, // ", 145.0000 LG" 형태
/LG[,\s]*(\d+(?:\.\d+)?)/i // LG, 75 형태
];
for (const pattern of lengthPatterns) {
const match = cleanDescription.match(pattern);
if (match) {
let lengthValue = match[1];
// 소수점 제거 (145.0000 → 145)
if (lengthValue.includes('.') && lengthValue.endsWith('.0000')) {
lengthValue = lengthValue.split('.')[0];
} else if (lengthValue.includes('.') && /\.0+$/.test(lengthValue)) {
lengthValue = lengthValue.split('.')[0];
}
schedule = `${lengthValue}mm`;
break;
}
}
} else {
// 다른 카테고리는 스케줄 추출
const scheduleMatch = cleanDescription.match(/SCH\s*(\d+[A-Z]*(?:\s*[xX×]\s*SCH\s*\d+[A-Z]*)?)/i);
if (scheduleMatch) {
schedule = scheduleMatch[0];
}
}
// 재질 추출 (카테고리별 처리)
let grade = '-';
if (category === 'GASKET') {
// 가스켓의 경우 재질 필드에는 H/F/I/O 타입 정보 표시
const hfioMatch = cleanDescription.match(/H\/F\/I\/O/i);
if (hfioMatch) {
grade = 'H/F/I/O';
} else {
// 다른 가스켓 타입들
if (cleanDescription.includes('SWG') || cleanDescription.includes('SPIRAL')) {
grade = 'SWG';
} else if (cleanDescription.includes('RTJ') || cleanDescription.includes('RING')) {
grade = 'RTJ';
} else if (cleanDescription.includes('FF') || cleanDescription.includes('FULL FACE')) {
grade = 'FF';
} else if (cleanDescription.includes('RF') || cleanDescription.includes('RAISED')) {
grade = 'RF';
} else {
grade = 'GASKET';
}
}
} else if (category === 'BOLT') {
// 볼트 전용 재질 추출 (복합 ASTM 패턴 지원)
const boltGradePatterns = [
// 복합 ASTM 패턴 (A193/A194 GR B7/2H)
/(ASTM\s+A\d+\/A\d+\s+GR\s+[A-Z0-9\/]+)/i,
// 단일 ASTM 패턴 (ASTM A193 GR B7)
/(ASTM\s+A\d+\s+GR\s+[A-Z0-9]+)/i,
// ASTM 번호만 (ASTM A193/A194)
/(ASTM\s+A\d+(?:\/A\d+)?)/i,
// 일반 ASTM 패턴
/(ASTM\s+[A-Z0-9\s\/]+(?:TP\d+|GR\s*[A-Z0-9\/]+|WP\d+)?)/i
];
for (const pattern of boltGradePatterns) {
const match = cleanDescription.match(pattern);
if (match) {
grade = match[1].trim();
break;
}
}
// ASTM이 없는 경우 기본 재질 패턴 시도
if (grade === '-') {
const basicGradeMatch = cleanDescription.match(/(A\d+(?:\/A\d+)?\s+(?:GR\s+)?[A-Z0-9\/]+)/i);
if (basicGradeMatch) {
grade = basicGradeMatch[1].trim();
}
}
// 백엔드에서 제공하는 재질 정보 우선 사용
if (material.full_material_grade && material.full_material_grade !== '-') {
grade = material.full_material_grade;
} else if (material.material_grade && material.material_grade !== '-') {
grade = material.material_grade;
}
} else {
// 기존 ASTM 패턴 (다른 카테고리용)
const gradeMatch = cleanDescription.match(/(ASTM\s+[A-Z0-9\s]+(?:TP\d+|GR\s*[A-Z0-9]+|WP\d+)?)/i);
if (gradeMatch) {
grade = gradeMatch[1].trim();
}
}
// 카테고리별 상세 정보 추출
let detailInfo = '';
let gasketMaterial = '';
let gasketThickness = '';
if (category === 'BOLT') {
// 볼트의 경우 표면처리 정보 추출
const surfaceTreatments = [];
// 원본 설명에서 표면처리 패턴 확인
const surfacePatterns = {
'ELEC.GALV': 'ELEC.GALV',
'ELEC GALV': 'ELEC.GALV',
'GALVANIZED': 'GALVANIZED',
'GALV': 'GALV',
'HOT DIP GALV': 'HDG',
'HDG': 'HDG',
'ZINC PLATED': 'ZINC PLATED',
'ZINC': 'ZINC',
'PLAIN': 'PLAIN'
};
for (const [pattern, treatment] of Object.entries(surfacePatterns)) {
if (cleanDescription.includes(pattern)) {
surfaceTreatments.push(treatment);
break; // 첫 번째 매치만 사용
}
}
detailInfo = surfaceTreatments.join(', ') || '-';
} else if (category === 'GASKET') {
// 실제 재질 구성 정보 (SS304/GRAPHITE/SS304/SS304)
if (material.gasket_details) {
const materialType = material.gasket_details.material_type || '';
const fillerMaterial = material.gasket_details.filler_material || '';
if (materialType && fillerMaterial) {
// DB에서 가져온 정보로 구성
gasketMaterial = `${materialType}/${fillerMaterial}`;
}
}
// gasket_details가 없거나 불완전하면 description에서 추출
if (!gasketMaterial) {
// SS304/GRAPHITE/SS304/SS304 패턴 추출
const fullMaterialMatch = cleanDescription.match(/SS304\/GRAPHITE\/SS304\/SS304/i);
if (fullMaterialMatch) {
gasketMaterial = 'SS304/GRAPHITE/SS304/SS304';
} else {
// 간단한 패턴 (SS304/GRAPHITE)
const simpleMaterialMatch = cleanDescription.match(/(SS\d+|304|316)\s*[\/+]\s*(GRAPHITE|PTFE|VITON|EPDM)/i);
if (simpleMaterialMatch) {
gasketMaterial = `${simpleMaterialMatch[1]}/${simpleMaterialMatch[2]}`;
}
}
}
// 두께 정보 별도 추출
if (material.gasket_details && material.gasket_details.thickness) {
gasketThickness = `${material.gasket_details.thickness}mm`;
} else {
// description에서 두께 추출 (4.5mm 패턴)
const thicknessMatch = cleanDescription.match(/(\d+\.?\d*)\s*mm/i);
if (thicknessMatch) {
gasketThickness = `${thicknessMatch[1]}mm`;
}
}
// 기타 상세 정보 (Fire Safe 등)
const otherDetails = [];
if (material.gasket_details && material.gasket_details.fire_safe) {
otherDetails.push('Fire Safe');
}
detailInfo = otherDetails.join(', ');
}
// 수량 계산 (PIPE는 본 단위)
let quantity = purchaseInfo.purchaseQuantity || material.quantity || 0;
if (category === 'PIPE') {
// PIPE의 경우 본 단위로 계산
if (material.total_length) {
// 총 길이를 6000mm로 나누어 본 수 계산
quantity = Math.ceil(material.total_length / 6000);
} else if (material.pipe_details && material.pipe_details.total_length_mm) {
quantity = Math.ceil(material.pipe_details.total_length_mm / 6000);
} else if (material.pipe_count) {
quantity = material.pipe_count;
}
}
// 새로운 엑셀 양식에 맞춘 데이터 구조
const base = {
'TAGNO': '', // 비워둠
'품목명': itemName,
'수량': quantity,
'통화구분': 'KRW', // 기본값
'단가': 1, // 일괄 1로 설정
'크기': material.size_spec || '-',
'압력등급': pressure
};
// 카테고리별 전용 컬럼 구성
if (category === 'GASKET') {
// 가스켓 전용 컬럼 순서
base['타입/구조'] = grade; // H/F/I/O, SWG 등 (스케줄 대신)
base['재질'] = gasketMaterial || '-'; // SS304/GRAPHITE/SS304/SS304
base['두께'] = gasketThickness || '-'; // 4.5mm
base['사용자요구'] = material.user_requirement || '';
base['관리항목8'] = ''; // 빈칸
base['관리항목9'] = ''; // 빈칸
base['관리항목10'] = ''; // 빈칸
base['납기일(YYYY-MM-DD)'] = new Date().toISOString().split('T')[0]; // 가장 마지막
} else if (category === 'BOLT') {
// 볼트 전용 컬럼 순서 (스케줄 → 길이)
base['길이'] = schedule; // 볼트는 길이 정보
base['재질'] = grade;
base['추가요구'] = detailInfo || '-'; // 상세내역 → 추가요구로 변경
base['사용자요구'] = material.user_requirement || '';
base['관리항목1'] = ''; // 빈칸
base['관리항목7'] = ''; // 빈칸
base['관리항목8'] = ''; // 빈칸
base['관리항목9'] = ''; // 빈칸
base['관리항목10'] = ''; // 빈칸
base['납기일(YYYY-MM-DD)'] = new Date().toISOString().split('T')[0]; // 가장 마지막
} else {
// 다른 카테고리는 기존 방식
base['스케줄'] = schedule;
base['재질'] = grade;
base['상세내역'] = detailInfo || '-';
base['사용자요구'] = material.user_requirement || '';
base['관리항목1'] = ''; // 빈칸
base['관리항목7'] = ''; // 빈칸
base['관리항목8'] = ''; // 빈칸
base['관리항목9'] = ''; // 빈칸
base['관리항목10'] = ''; // 빈칸
base['납기일(YYYY-MM-DD)'] = new Date().toISOString().split('T')[0]; // 가장 마지막
}
// 비교 모드인 경우 추가 정보
if (includeComparison) {
if (material.previous_quantity !== undefined) {
const prevPurchaseInfo = calculatePurchaseQuantity({
...material,
quantity: material.previous_quantity,
totalLength: material.previousTotalLength || 0
});
base['이전수량'] = prevPurchaseInfo.purchaseQuantity || 0;
base['수량변경'] = (purchaseInfo.purchaseQuantity - prevPurchaseInfo.purchaseQuantity);
}
base['변경유형'] = material.change_type || (
material.previous_quantity !== undefined ? '수량 변경' :
material.quantity_change === undefined ? '신규' : '변경'
);
}
return base;
};
/**
* 일반 자재 목록 엑셀 내보내기
*/
export const exportMaterialsToExcel = (materials, filename, additionalInfo = {}) => {
try {
console.log('🔧 exportMaterialsToExcel 시작:', materials.length, '개 자재');
// 카테고리별로 그룹화
const categoryGroups = groupMaterialsByCategory(materials);
console.log('📁 카테고리별 그룹:', Object.keys(categoryGroups).map(k => `${k}: ${categoryGroups[k].length}`));
// 전체 자재 합치기 (먼저 계산)
const consolidatedMaterials = consolidateMaterials(materials);
console.log('📦 합쳐진 자재:', consolidatedMaterials.length, '개');
// 새 워크북 생성
const workbook = XLSX.utils.book_new();
// 카테고리별 시트 생성 (합쳐진 자재)
Object.entries(categoryGroups).forEach(([category, items]) => {
console.log(`📄 ${category} 시트 생성 중... (${items.length}개 자재)`);
const consolidatedItems = consolidateMaterials(items);
console.log(` → 합쳐진 결과: ${consolidatedItems.length}`);
const formattedItems = consolidatedItems.map(material => formatMaterialForExcel(material));
console.log(` → 포맷 완료: ${formattedItems.length}`);
if (formattedItems.length > 0) {
const categorySheet = XLSX.utils.json_to_sheet(formattedItems);
// 헤더 스타일링 (연하늘색 배경)
const range = XLSX.utils.decode_range(categorySheet['!ref']);
// 헤더 행에 스타일 적용 (첫 번째 행)
for (let col = range.s.c; col <= range.e.c; col++) {
const cellRef = XLSX.utils.encode_cell({ r: 0, c: col });
if (categorySheet[cellRef]) {
// 기존 셀 값 유지하면서 스타일만 추가
const cellValue = categorySheet[cellRef].v;
const cellType = categorySheet[cellRef].t;
categorySheet[cellRef] = {
v: cellValue,
t: cellType || 's',
s: {
fill: {
patternType: "solid",
fgColor: { rgb: "B3D9FF" }
},
font: {
bold: true,
color: { rgb: "000000" },
sz: 12,
name: "맑은 고딕"
},
alignment: {
horizontal: "center",
vertical: "center"
},
border: {
top: { style: "thin", color: { rgb: "666666" } },
bottom: { style: "thin", color: { rgb: "666666" } },
left: { style: "thin", color: { rgb: "666666" } },
right: { style: "thin", color: { rgb: "666666" } }
}
}
};
}
}
// 컬럼 너비 자동 조정
const colWidths = [];
if (formattedItems.length > 0) {
const headers = Object.keys(formattedItems[0]);
headers.forEach((header, index) => {
let maxWidth = header.length; // 헤더 길이
// 각 행의 데이터 길이 확인
formattedItems.forEach(item => {
const cellValue = String(item[header] || '');
maxWidth = Math.max(maxWidth, cellValue.length);
});
// 최소 10, 최대 50으로 제한
colWidths[index] = { wch: Math.min(Math.max(maxWidth + 2, 10), 50) };
});
}
categorySheet['!cols'] = colWidths;
// 시트명에서 특수문자 제거 (엑셀 시트명 규칙)
const sheetName = category.replace(/[\\\/\*\?\[\]]/g, '_').substring(0, 31);
XLSX.utils.book_append_sheet(workbook, categorySheet, sheetName);
}
});
// 워크북 속성 설정 (스타일 지원)
workbook.Props = {
Title: "자재 목록",
Subject: "TK-MP-Project 자재 관리",
Author: "TK-MP System",
CreatedDate: new Date()
};
// 파일 저장 (스타일 포함)
const excelBuffer = XLSX.write(workbook, {
bookType: 'xlsx',
type: 'array',
cellStyles: true // 스타일 활성화
});
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;
}
};