/** * Excel 내보내기 유틸리티 * 피벗 테이블 데이터를 Excel 파일로 내보내기 * xlsx 라이브러리 사용 (브라우저 호환) */ import * as XLSX from "xlsx"; import { PivotResult, PivotFieldConfig, PivotTotalsConfig, } from "../types"; import { pathToKey } from "./pivotEngine"; // ==================== 타입 ==================== export interface ExportOptions { fileName?: string; sheetName?: string; title?: string; subtitle?: string; includeHeaders?: boolean; includeTotals?: boolean; } // ==================== 메인 함수 ==================== /** * 피벗 데이터를 Excel로 내보내기 */ export async function exportPivotToExcel( pivotResult: PivotResult, fields: PivotFieldConfig[], totals: PivotTotalsConfig, options: ExportOptions = {} ): Promise { const { fileName = "pivot_export", sheetName = "Pivot", title, includeHeaders = true, includeTotals = true, } = options; // 필드 분류 const rowFields = fields .filter((f) => f.area === "row" && f.visible !== false) .sort((a, b) => (a.areaIndex || 0) - (b.areaIndex || 0)); // 데이터 배열 생성 const data: any[][] = []; // 제목 추가 if (title) { data.push([title]); data.push([]); // 빈 행 } // 헤더 행 if (includeHeaders) { const headerRow: any[] = [ rowFields.map((f) => f.caption).join(" / ") || "항목", ]; // 열 헤더 for (const col of pivotResult.flatColumns) { headerRow.push(col.caption || "(전체)"); } // 총계 헤더 if (totals?.showRowGrandTotals && includeTotals) { headerRow.push("총계"); } data.push(headerRow); } // 데이터 행 for (const row of pivotResult.flatRows) { const excelRow: any[] = []; // 행 헤더 (들여쓰기 포함) const indent = " ".repeat(row.level); excelRow.push(indent + row.caption); // 데이터 셀 for (const col of pivotResult.flatColumns) { const cellKey = `${pathToKey(row.path)}|||${pathToKey(col.path)}`; const values = pivotResult.dataMatrix.get(cellKey); if (values && values.length > 0) { excelRow.push(values[0].value); } else { excelRow.push(""); } } // 행 총계 if (totals?.showRowGrandTotals && includeTotals) { const rowTotal = pivotResult.grandTotals.row.get(pathToKey(row.path)); if (rowTotal && rowTotal.length > 0) { excelRow.push(rowTotal[0].value); } else { excelRow.push(""); } } data.push(excelRow); } // 열 총계 행 if (totals?.showColumnGrandTotals && includeTotals) { const totalRow: any[] = ["총계"]; for (const col of pivotResult.flatColumns) { const colTotal = pivotResult.grandTotals.column.get(pathToKey(col.path)); if (colTotal && colTotal.length > 0) { totalRow.push(colTotal[0].value); } else { totalRow.push(""); } } // 대총합 if (totals?.showRowGrandTotals) { const grandTotal = pivotResult.grandTotals.grand; if (grandTotal && grandTotal.length > 0) { totalRow.push(grandTotal[0].value); } else { totalRow.push(""); } } data.push(totalRow); } // 워크시트 생성 const worksheet = XLSX.utils.aoa_to_sheet(data); // 컬럼 너비 설정 const colWidths: XLSX.ColInfo[] = []; const maxCols = data.reduce((max, row) => Math.max(max, row.length), 0); for (let i = 0; i < maxCols; i++) { colWidths.push({ wch: i === 0 ? 25 : 15 }); } worksheet["!cols"] = colWidths; // 워크북 생성 const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); // 파일 다운로드 XLSX.writeFile(workbook, `${fileName}.xlsx`); } /** * Drill Down 데이터를 Excel로 내보내기 */ export async function exportDrillDownToExcel( data: any[], columns: { field: string; caption: string }[], options: ExportOptions = {} ): Promise { const { fileName = "drilldown_export", sheetName = "Data", title, } = options; // 데이터 배열 생성 const sheetData: any[][] = []; // 제목 if (title) { sheetData.push([title]); sheetData.push([]); // 빈 행 } // 헤더 const headerRow = columns.map((col) => col.caption); sheetData.push(headerRow); // 데이터 for (const row of data) { const dataRow = columns.map((col) => row[col.field] ?? ""); sheetData.push(dataRow); } // 워크시트 생성 const worksheet = XLSX.utils.aoa_to_sheet(sheetData); // 컬럼 너비 설정 const colWidths: XLSX.ColInfo[] = columns.map(() => ({ wch: 15 })); worksheet["!cols"] = colWidths; // 워크북 생성 const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); // 파일 다운로드 XLSX.writeFile(workbook, `${fileName}.xlsx`); }