203 lines
4.8 KiB
TypeScript
203 lines
4.8 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<void> {
|
||
|
|
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<void> {
|
||
|
|
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`);
|
||
|
|
}
|