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( 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 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) }