diff options
Diffstat (limited to 'lib/export.ts')
| -rw-r--r-- | lib/export.ts | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/lib/export.ts b/lib/export.ts new file mode 100644 index 00000000..d910ef6a --- /dev/null +++ b/lib/export.ts @@ -0,0 +1,198 @@ +import { type Table } from "@tanstack/react-table" +import ExcelJS from "exceljs" + +/** + * `exportTableToExcel`: + * - filename: 다운로드할 엑셀 파일 이름(확장자 제외) + * - onlySelected: 선택된 행만 내보낼지 여부 + * - excludeColumns: 제외할 column id들의 배열 (e.g. ["select", "actions"]) + * - useGroupHeader: 그룹화 헤더를 사용할지 여부 (기본 false) + */ +export async function exportTableToExcel<TData>( + table: Table<TData>, + { + filename = "table", + onlySelected = false, + excludeColumns = [], + useGroupHeader = true, + }: { + filename?: string + onlySelected?: boolean + excludeColumns?: string[] + useGroupHeader?: boolean + } = {} +): Promise<void> { + // 1) tanstack에서 실제 사용 중인 leaf columns 가져오기 + const allColumns = table.getAllLeafColumns() + + // 2) excludeColumns 목록에 들어있는 col.id 제거 + const columns = allColumns.filter( + (col) => !excludeColumns.includes(col.id) + ) + + let sheetData: any[][] + + if (useGroupHeader) { + // ────────────── 2줄 헤더 (row1 = 그룹명, row2 = 컬럼헤더) ────────────── + const row1: string[] = [] + const row2: string[] = [] + + columns.forEach((col) => { + // group + const maybeGroup = (col.columnDef.meta as any)?.group + row1.push(maybeGroup ?? "") + + // excelHeader + const maybeExcelHeader = (col.columnDef.meta as any)?.excelHeader + if (typeof maybeExcelHeader === "string") { + row2.push(maybeExcelHeader) + } else { + row2.push(col.id) + } + }) + + // 데이터 + const rowModel = onlySelected + ? table.getFilteredSelectedRowModel() + : table.getRowModel() + + const dataRows = rowModel.rows.map((row) => + columns.map((col) => { + const val = row.getValue(col.id) + if (val == null) return "" + return typeof val === "object" ? JSON.stringify(val) : val + }) + ) + + // 최종 sheetData: [ [그룹들...], [헤더들...], ...데이터들 ] + sheetData = [row1, row2, ...dataRows] + } else { + // ────────────── 기존 1줄 헤더 ────────────── + const headerRow = columns.map((col) => { + const maybeExcelHeader = (col.columnDef.meta as any)?.excelHeader + return typeof maybeExcelHeader === "string" ? maybeExcelHeader : col.id + }) + + // 데이터 + const rowModel = onlySelected + ? table.getFilteredSelectedRowModel() + : table.getRowModel() + + const dataRows = rowModel.rows.map((row) => + columns.map((col) => { + const val = row.getValue(col.id) + if (val == null) return "" + return typeof val === "object" ? JSON.stringify(val) : val + }) + ) + + sheetData = [headerRow, ...dataRows] + } + + // ────────────── ExcelJS 워크북/시트 생성 ────────────── + const workbook = new ExcelJS.Workbook() + const worksheet = workbook.addWorksheet("Sheet1") + + // (추가) 칼럼별 최대 길이 추적 + const maxColumnLengths = columns.map(() => 0) + sheetData.forEach((row) => { + row.forEach((cellValue, colIdx) => { + const cellText = cellValue?.toString() ?? "" + if (cellText.length > maxColumnLengths[colIdx]) { + maxColumnLengths[colIdx] = cellText.length + } + }) + }) + + // 시트에 데이터 추가 + 헤더 스타일 + sheetData.forEach((arr, idx) => { + const row = worksheet.addRow(arr) + + // 헤더 스타일 적용 + if (useGroupHeader) { + // 2줄 헤더 + if (idx < 2) { + row.font = { bold: true } + row.alignment = { horizontal: "center" } + row.eachCell((cell) => { + cell.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFCCCCCC" }, + } + }) + } + } else { + // 1줄 헤더 + if (idx === 0) { + row.font = { bold: true } + row.alignment = { horizontal: "center" } + row.eachCell((cell) => { + cell.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFCCCCCC" }, + } + }) + } + } + }) + + // ────────────── (핵심) 그룹 헤더 병합 로직 ────────────── + if (useGroupHeader) { + // row1 (인덱스 1) = 그룹명 행 + // row2 (인덱스 2) = 실제 컬럼 헤더 행 + const groupRowIndex = 1 + const groupRow = worksheet.getRow(groupRowIndex) + + // 같은 값이 연속되는 열을 병합 + let start = 1 // 시작 열 인덱스 (1-based) + let prevValue = groupRow.getCell(start).value + + for (let c = 2; c <= columns.length; c++) { + const cellVal = groupRow.getCell(c).value + if (cellVal !== prevValue) { + // 이전 그룹명이 빈 문자열이 아니면 병합 + if (prevValue && prevValue.toString().trim() !== "") { + worksheet.mergeCells( + groupRowIndex, + start, + groupRowIndex, + c - 1 + ) + } + // 다음 구간 시작 + start = c + prevValue = cellVal + } + } + + // 마지막 구간까지 병합 + if (prevValue && prevValue.toString().trim() !== "") { + worksheet.mergeCells( + groupRowIndex, + start, + groupRowIndex, + columns.length + ) + } + } + + // ────────────── (추가) 칼럼 너비 자동 조정 ────────────── + maxColumnLengths.forEach((len, idx) => { + // 최소 너비 10, +2 여백 + worksheet.getColumn(idx + 1).width = Math.max(len + 2, 10) + }) + + // ────────────── 최종 파일 다운로드 ────────────── + const buffer = await workbook.xlsx.writeBuffer() + const blob = new Blob([buffer], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }) + const url = URL.createObjectURL(blob) + const link = document.createElement("a") + link.href = url + link.download = `${filename}.xlsx` + link.click() + URL.revokeObjectURL(url) +}
\ No newline at end of file |
