diff options
| -rw-r--r-- | lib/menu-list/table/excel-export.ts | 107 | ||||
| -rw-r--r-- | lib/menu-list/table/export-button.tsx | 64 | ||||
| -rw-r--r-- | lib/menu-list/table/menu-list-table.tsx | 12 |
3 files changed, 179 insertions, 4 deletions
diff --git a/lib/menu-list/table/excel-export.ts b/lib/menu-list/table/excel-export.ts new file mode 100644 index 00000000..feba5310 --- /dev/null +++ b/lib/menu-list/table/excel-export.ts @@ -0,0 +1,107 @@ +// app/evcp/menu-list/utils/excel-export.ts + +import ExcelJS from 'exceljs'; + +interface MenuExportData { + menuPath: string; + menuTitle: string; + menuDescription?: string | null; + menuGroup?: string | null; + sectionTitle: string; + domain: string; + isActive: boolean; + manager1Name?: string | null; + manager1Email?: string | null; + manager2Name?: string | null; + manager2Email?: string | null; +} + +export async function exportMenusToExcel( + menus: MenuExportData[], + translate: (key: string) => string +) { + // 워크북 생성 + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('메뉴 목록'); + + // 헤더 정의 + worksheet.columns = [ + { header: '메뉴 경로', key: 'menuPath', width: 40 }, + { header: '메뉴명', key: 'menuTitle', width: 30 }, + { header: '설명', key: 'menuDescription', width: 40 }, + { header: '그룹', key: 'menuGroup', width: 20 }, + { header: '섹션', key: 'sectionTitle', width: 20 }, + { header: '도메인', key: 'domain', width: 15 }, + { header: '상태', key: 'isActive', width: 10 }, + { header: '담당자1 이름', key: 'manager1Name', width: 20 }, + { header: '담당자1 이메일', key: 'manager1Email', width: 30 }, + { header: '담당자2 이름', key: 'manager2Name', width: 20 }, + { header: '담당자2 이메일', key: 'manager2Email', width: 30 }, + ]; + + // 헤더 스타일 설정 + worksheet.getRow(1).font = { bold: true }; + worksheet.getRow(1).fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFE0E0E0' } + }; + worksheet.getRow(1).alignment = { vertical: 'middle', horizontal: 'center' }; + + // 데이터 추가 (번역 적용) + menus.forEach((menu) => { + worksheet.addRow({ + menuPath: menu.menuPath, + menuTitle: translate(menu.menuTitle), + menuDescription: menu.menuDescription ? translate(menu.menuDescription) : '', + menuGroup: menu.menuGroup ? translate(menu.menuGroup) : '', + sectionTitle: translate(menu.sectionTitle), + domain: menu.domain.toUpperCase(), + isActive: menu.isActive ? '활성' : '비활성', + manager1Name: menu.manager1Name || '', + manager1Email: menu.manager1Email || '', + manager2Name: menu.manager2Name || '', + manager2Email: menu.manager2Email || '', + }); + }); + + // 모든 셀에 테두리 추가 + worksheet.eachRow((row, rowNumber) => { + row.eachCell((cell) => { + cell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' } + }; + // 데이터 행은 세로 가운데 정렬 + if (rowNumber > 1) { + cell.alignment = { vertical: 'middle', wrapText: true }; + } + }); + }); + + // 자동 필터 추가 + worksheet.autoFilter = { + from: 'A1', + to: `K${menus.length + 1}` + }; + + // 파일 다운로드 + 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'); + const fileName = `메뉴목록_${new Date().toISOString().slice(0, 10)}.xlsx`; + + link.href = url; + link.download = fileName; + link.click(); + + // 메모리 정리 + URL.revokeObjectURL(url); +}
\ No newline at end of file diff --git a/lib/menu-list/table/export-button.tsx b/lib/menu-list/table/export-button.tsx new file mode 100644 index 00000000..320e495f --- /dev/null +++ b/lib/menu-list/table/export-button.tsx @@ -0,0 +1,64 @@ +// app/evcp/menu-list/components/export-button.tsx + +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Download } from "lucide-react"; +import { toast } from "sonner"; +import { exportMenusToExcel } from "./excel-export"; + +interface MenuAssignment { + id: number; + menuPath: string; + menuTitle: string; + menuDescription?: string | null; + menuGroup?: string | null; + sectionTitle: string; + domain: string; + isActive: boolean; + manager1Name?: string | null; + manager1Email?: string | null; + manager2Name?: string | null; + manager2Email?: string | null; +} + +interface ExportButtonProps { + menus: MenuAssignment[]; + translate: (key: string) => string; +} + +export function ExportButton({ menus, translate }: ExportButtonProps) { + const [isExporting, setIsExporting] = useState(false); + + const handleExport = async () => { + if (menus.length === 0) { + toast.error("내보낼 데이터가 없습니다."); + return; + } + + setIsExporting(true); + + try { + await exportMenusToExcel(menus, translate); + toast.success(`${menus.length}개의 메뉴 데이터를 엑셀로 내보냈습니다.`); + } catch (error) { + console.error("Export error:", error); + toast.error("엑셀 파일 생성 중 오류가 발생했습니다."); + } finally { + setIsExporting(false); + } + }; + + return ( + <Button + onClick={handleExport} + disabled={isExporting || menus.length === 0} + variant="outline" + size="sm" + > + <Download className="mr-2 h-4 w-4" /> + {isExporting ? "내보내는 중..." : `엑셀 다운로드 (${menus.length}개)`} + </Button> + ); +}
\ No newline at end of file diff --git a/lib/menu-list/table/menu-list-table.tsx b/lib/menu-list/table/menu-list-table.tsx index dedbc9bf..3998e6b5 100644 --- a/lib/menu-list/table/menu-list-table.tsx +++ b/lib/menu-list/table/menu-list-table.tsx @@ -28,6 +28,7 @@ import { Search, Filter, ExternalLink } from "lucide-react"; import { toast } from "sonner"; import { ManagerSelect } from "./manager-select"; import { InitializeButton } from "./initialize-button"; +import { ExportButton } from "./export-button"; import { toggleMenuActive } from "../servcie"; interface MenuAssignment { @@ -190,13 +191,16 @@ export function MenuListTable({ initialMenus, initialUsers }: MenuListTableProps </div> </div> - {/* 결과 요약 및 초기화 버튼 */} - <div className="flex items-center justify-between text-sm text-muted-foreground"> - <span> + {/* 결과 요약, 엑셀 다운로드 및 초기화 버튼 */} + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground"> 총 {filteredMenus.length}개의 메뉴 {searchQuery && ` (${initialMenus.length}개 중 검색 결과)`} </span> - <InitializeButton /> + <div className="flex gap-2"> + <ExportButton menus={filteredMenus} translate={safeTranslate} /> + <InitializeButton /> + </div> </div> {/* 테이블 */} |
