import { type Table } from "@tanstack/react-table" import ExcelJS from "exceljs" /** * 컬럼 정의 인터페이스 */ export interface ExcelColumnDef { id: string header: string accessor: string | ((row: any) => any) group?: string } /** * `exportTableToExcel`: 기존 테이블 기반 내보내기 (페이지네이션된 데이터) * - filename: 다운로드할 엑셀 파일 이름(확장자 제외) * - onlySelected: 선택된 행만 내보낼지 여부 * - excludeColumns: 제외할 column id들의 배열 (e.g. ["select", "actions"]) * - useGroupHeader: 그룹화 헤더를 사용할지 여부 (기본 false) */ export async function exportTableToExcel( table: Table, { filename = "table", onlySelected = false, excludeColumns = [], useGroupHeader = true, }: { filename?: string onlySelected?: boolean excludeColumns?: string[] useGroupHeader?: boolean } = {} ): Promise { // 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 (meta 또는 직접 속성에서 찾기) const maybeExcelHeader = (col.columnDef.meta as any)?.excelHeader || (col.columnDef 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 || (col.columnDef 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로 파일 생성 및 다운로드 await createAndDownloadExcel(sheetData, columns.length, filename, useGroupHeader) } /** * `exportFullDataToExcel`: 전체 데이터를 Excel로 내보내기 * - data: 전체 데이터 배열 * - columns: 컬럼 정의 배열 * - filename: 다운로드할 엑셀 파일 이름(확장자 제외) * - useGroupHeader: 그룹화 헤더를 사용할지 여부 (기본 false) */ export async function exportFullDataToExcel( data: TData[], columns: ExcelColumnDef[], { filename = "export", useGroupHeader = true, }: { filename?: string useGroupHeader?: boolean } = {} ): Promise { let sheetData: any[][] if (useGroupHeader) { // ────────────── 2줄 헤더 (row1 = 그룹명, row2 = 컬럼헤더) ────────────── const row1: string[] = [] const row2: string[] = [] columns.forEach((col) => { // group row1.push(col.group ?? "") // header row2.push(col.header) }) // 데이터 행 생성 const dataRows = data.map((item) => columns.map((col) => { let val: any if (typeof col.accessor === "function") { val = col.accessor(item) } else { val = (item as any)[col.accessor] } if (val == null) return "" return typeof val === "object" ? JSON.stringify(val) : val }) ) // 최종 sheetData: [ [그룹들...], [헤더들...], ...데이터들 ] sheetData = [row1, row2, ...dataRows] } else { // ────────────── 기존 1줄 헤더 ────────────── const headerRow = columns.map((col) => col.header) // 데이터 행 생성 const dataRows = data.map((item) => columns.map((col) => { let val: any if (typeof col.accessor === "function") { val = col.accessor(item) } else { val = (item as any)[col.accessor] } if (val == null) return "" return typeof val === "object" ? JSON.stringify(val) : val }) ) sheetData = [headerRow, ...dataRows] } // ExcelJS로 파일 생성 및 다운로드 await createAndDownloadExcel(sheetData, columns.length, filename, useGroupHeader) } /** * 공통 Excel 파일 생성 및 다운로드 함수 */ async function createAndDownloadExcel( sheetData: any[][], columnCount: number, filename: string, useGroupHeader: boolean ): Promise { // ────────────── ExcelJS 워크북/시트 생성 ────────────── const workbook = new ExcelJS.Workbook() const worksheet = workbook.addWorksheet("Sheet1") // 칼럼별 최대 길이 추적 const maxColumnLengths = Array(columnCount).fill(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) = 그룹명 행 const groupRowIndex = 1 const groupRow = worksheet.getRow(groupRowIndex) // 같은 값이 연속되는 열을 병합 let start = 1 // 시작 열 인덱스 (1-based) let prevValue = groupRow.getCell(start).value for (let c = 2; c <= columnCount; 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, columnCount ) } } // ────────────── 칼럼 너비 자동 조정 ────────────── 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) }