49b4cdf562
5 viewMode 통합 세번째 단계 — Codex 권고 (Pivot 통째 흡수, 회귀 위험 차단) 그대로 적용. v2-pivot-grid 의 본체 + utils + components + hooks + 보조 타입을 모두 table/ 하위로 이전. 리팩토링 X (V2Date picker 패턴 동일). table/types.ts 보조 타입 흡수 - PivotResult, PivotHeaderNode, PivotFlatRow, PivotFlatColumn, PivotCellValue, PivotCellData, PivotGridState, PivotGridProps - PivotDataSourceType, PivotFilterCondition, PivotJoinConfig, PivotDataSourceConfig, PivotTotalsConfig, PivotFieldChooserConfig, PivotChartConfig, PivotConditionalFormatRule, PivotStyleConfig, PivotExportConfig - PivotFieldConfig 에 filterValues / filterType / isCalculated / calculateFormula 누락 속성 추가 - TableConfig 에 pivot 보조 키 (pivotTotals / pivotStyle / pivotFieldChooser / pivotChart / pivotExportConfig) table/utils/pivot/ 4개 파일 이전 (1505줄) - pivotEngine.ts (812) — processPivotData / pathToKey / 헤더 트리 / 매트릭스 / 10종 displayMode (runningTotal, percentDifferenceFromPrevious 등) - aggregation.ts (180) — sum/count/avg/min/max/countDistinct + 포맷 - conditionalFormat.ts (311) — colorScale/dataBar/iconSet/cellValue 4 종 - exportExcel.ts (202) — Excel 내보내기 (xlsx) - 옛 prefix(AggregationType 등) → Pivot prefix 일괄 정리 table/internals/pivot/components/ 7개 파일 이전 (2347줄) - ContextMenu / DrillDownModal / FieldChooser / FieldPanel / FilterPopup / PivotChart / index table/internals/pivot/hooks/ 3개 파일 이전 (570줄) - usePivotState / useVirtualScroll / index table/views/PivotView.tsx 신규 (PivotGridComponent.tsx 1963줄 통째 흡수) - import 경로 일괄 정정 (../../types → ../types, ./utils → ../utils/pivot, ./components → ../internals/pivot/components, ./hooks → ../internals/pivot/hooks) - 컴포넌트 이름 PivotGridComponent → PivotView - 본체 로직 그대로 (리팩토링 X) TableComponent.switch - pivot 분기 placeholder 제거 → PivotView 호출 - DOM filter 에 pivotTotals/pivotStyle/pivotFieldChooser/pivotChart/ pivotExportConfig 추가 13 files, +5400+ insertions. v2-pivot-grid/ 폴더 자체는 Phase T5 dead code 일괄 삭제에서 정리 예정. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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`);
|
|
}
|