diff options
| author | joonhoekim <26rote@gmail.com> | 2025-03-25 15:55:45 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-03-25 15:55:45 +0900 |
| commit | 1a2241c40e10193c5ff7008a7b7b36cc1d855d96 (patch) | |
| tree | 8a5587f10ca55b162d7e3254cb088b323a34c41b /lib/export_all.ts | |
initial commit
Diffstat (limited to 'lib/export_all.ts')
| -rw-r--r-- | lib/export_all.ts | 251 |
1 files changed, 251 insertions, 0 deletions
diff --git a/lib/export_all.ts b/lib/export_all.ts new file mode 100644 index 00000000..6f925fbc --- /dev/null +++ b/lib/export_all.ts @@ -0,0 +1,251 @@ +import { type Table } from "@tanstack/react-table" +import ExcelJS from "exceljs" + +/** + * `exportTableToExcel`: + * - filename: 다운로드할 엑셀 파일 이름(확장자 제외) + * - onlySelected: 선택된 행만 내보낼지 여부 + * - excludeColumns: 제외할 column id들의 배열 (e.g. ["select", "actions"]) + * - useGroupHeader: 그룹화 헤더를 사용할지 여부 (기본 false) + * - allPages: true일 경우, 페이징 상관없이 모든 행을 내보냄 + * + * 추가: + * - i18n: (key: string) => string | undefined + * => excelHeader나 group 값이 'myKey'처럼 i18n 키라면 이 함수를 통해 번역 문자열 반환 + * - customHeaders: { [colId: string]: string } + * => 특정 col.id에 대해 강제로 헤더를 지정하고 싶을 때 사용 + */ +export async function exportTableToExcel<TData>( + table: Table<TData>, + { + filename = "table", + onlySelected = false, + excludeColumns = [], + useGroupHeader = true, + allPages = false, + /** 아래 2개가 새로 추가된 옵션 */ + i18n, + customHeaders = {}, + }: { + filename?: string + onlySelected?: boolean + excludeColumns?: string[] + useGroupHeader?: boolean + allPages?: boolean + /** excelHeader나 group 값이 i18n 키일 경우, 해당 함수를 통해 번역 */ + i18n?: (key: string) => string + /** 특정 col.id에 대한 강제 헤더 지정 */ + customHeaders?: Record<string, string> + } = {} +): Promise<void> { + // 1) tanstack에서 실제 사용 중인 leaf columns 가져오기 + const allColumns = table.getAllLeafColumns() + + // 2) excludeColumns 목록에 들어있는 col.id 제거 + const columns = allColumns.filter( + (col) => !excludeColumns.includes(col.id) + ) + + // 실제로 기록할 sheetData(배열 형식) + let sheetData: any[][] + + // ────────────── 2줄 헤더 (group + excelHeader) vs 1줄 헤더 ────────────── + if (useGroupHeader) { + // 2줄 헤더 (row1 = 그룹명, row2 = 컬럼헤더) + const row1: string[] = [] + const row2: string[] = [] + + columns.forEach((col) => { + const meta = col.columnDef.meta as any + // 1) group (그룹헤더) + const groupKey = meta?.group + let groupLabel = groupKey ?? "" + if (groupLabel && i18n) { + // groupKey가 i18n 키라면 번역 적용 + const maybeTranslated = i18n(groupLabel) + if (maybeTranslated) { + groupLabel = maybeTranslated + } + } + row1.push(groupLabel) + + // 2) excelHeader (실제 컬럼 헤더) + // (a) customHeaders[col.id]가 우선 + if (customHeaders[col.id]) { + row2.push(customHeaders[col.id]) + } else { + // (b) meta?.excelHeader가 있으면 그것을 사용 + const maybeExcelHeader = meta?.excelHeader + if (typeof maybeExcelHeader === "string") { + // i18n 함수가 있다면 i18n 키로 가정하고 번역 시도 + if (i18n) { + const maybeTranslated = i18n(maybeExcelHeader) + row2.push(maybeTranslated || maybeExcelHeader) + } else { + row2.push(maybeExcelHeader) + } + } else { + // 모두 없으면 col.id 사용 + row2.push(col.id) + } + } + }) + + // ───────────────────────────────────────────────── + // 필요한 데이터 행 추출 + const rowModel = onlySelected + ? table.getFilteredSelectedRowModel() + : allPages + ? table.getPrePaginationRowModel() + : 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 meta = col.columnDef.meta as any + + // 1) customHeaders[col.id]가 우선 + if (customHeaders[col.id]) { + return customHeaders[col.id] + } + + // 2) meta?.excelHeader가 문자열이면 + if (typeof meta?.excelHeader === "string") { + if (i18n) { + const maybeTranslated = i18n(meta.excelHeader) + return maybeTranslated || meta.excelHeader + } else { + return meta.excelHeader + } + } + + // 3) 모든 것이 없으면 col.id + return col.id + }) + + // 데이터 + const rowModel = onlySelected + ? table.getFilteredSelectedRowModel() + : allPages + ? table.getPrePaginationRowModel() + : 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 |
