summaryrefslogtreecommitdiff
path: root/lib/export_all.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-03-26 00:37:41 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-03-26 00:37:41 +0000
commite0dfb55c5457aec489fc084c4567e791b4c65eb1 (patch)
tree68543a65d88f5afb3a0202925804103daa91bc6f /lib/export_all.ts
3/25 까지의 대표님 작업사항
Diffstat (limited to 'lib/export_all.ts')
-rw-r--r--lib/export_all.ts251
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