import { ExcelColumnDef } from "@/lib/export" import ExcelJS from "exceljs" /** * Excel 템플릿 다운로드 함수 */ export async function downloadExcelTemplate( columns: ExcelColumnDef[], { filename = "template", includeExampleData = true, useGroupHeader = true, }: { filename?: string includeExampleData?: boolean useGroupHeader?: boolean } = {} ): Promise { let sheetData: any[][] if (useGroupHeader) { // 2줄 헤더 생성 const row1: string[] = [] const row2: string[] = [] columns.forEach((col) => { row1.push(col.group ?? "") row2.push(col.header) }) sheetData = [row1, row2] // 예시 데이터 추가 if (includeExampleData) { // 빈 행 3개 추가 (사용자가 데이터 입력할 공간) for (let i = 0; i < 3; i++) { const exampleRow = columns.map((col) => { // 컬럼 타입에 따른 예시 데이터 if (col.id === "itemNumber") return i === 0 ? `1.${i + 1}` : i === 1 ? "2.1" : "" if (col.id === "subtitle") return i === 0 ? "예시 조항 소제목" : i === 1 ? "하위 조항 예시" : "" if (col.id === "content") return i === 0 ? "조항의 상세 내용을 입력합니다." : i === 1 ? "하위 조항의 내용" : "" if (col.id === "category") return i === 0 ? "일반조항" : i === 1 ? "특별조항" : "" if (col.id === "sortOrder") return i === 0 ? "10" : i === 1 ? "20" : "" if (col.id === "parentId") return i === 1 ? "1" : "" if (col.id === "isActive") return i === 0 ? "활성" : i === 1 ? "활성" : "" if (col.id === "editReason") return i === 0 ? "신규 작성" : "" return "" }) sheetData.push(exampleRow) } } } else { // 1줄 헤더 const headerRow = columns.map((col) => col.header) sheetData = [headerRow] if (includeExampleData) { // 예시 데이터 행 추가 const exampleRow = columns.map(() => "") sheetData.push(exampleRow) } } // ExcelJS로 워크북 생성 const workbook = new ExcelJS.Workbook() const worksheet = workbook.addWorksheet("GTC조항템플릿") // 데이터 추가 sheetData.forEach((arr, idx) => { const row = worksheet.addRow(arr) // 헤더 스타일 적용 if (useGroupHeader) { if (idx < 2) { row.font = { bold: true } row.alignment = { horizontal: "center" } row.eachCell((cell) => { cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFE6F3FF" }, // 연한 파란색 } cell.border = { top: { style: "thin" }, left: { style: "thin" }, bottom: { style: "thin" }, right: { style: "thin" }, } }) } } else { if (idx === 0) { row.font = { bold: true } row.alignment = { horizontal: "center" } row.eachCell((cell) => { cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFE6F3FF" }, } }) } } // 예시 데이터 행 스타일 if (includeExampleData && idx === (useGroupHeader ? 2 : 1)) { row.eachCell((cell) => { cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFFFEAA7" }, // 연한 노란색 } cell.font = { italic: true, color: { argb: "FF666666" } } }) } }) // 그룹 헤더 병합 if (useGroupHeader) { const groupRowIndex = 1 const groupRow = worksheet.getRow(groupRowIndex) let start = 1 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) } } // 컬럼 너비 자동 조정 columns.forEach((col, idx) => { let width = Math.max(col.header.length + 5, 15) // 특정 컬럼은 더 넓게 if (col.id === "content" || col.id === "subtitle") { width = 30 } else if (col.id === "itemNumber") { width = 15 } else if (col.id === "editReason") { width = 20 } worksheet.getColumn(idx + 1).width = width }) // 사용 안내 시트 추가 const instructionSheet = workbook.addWorksheet("사용안내") const instructions = [ ["GTC 조항 Excel 가져오기 사용 안내"], [""], ["1. 기본 규칙"], [" - 첫 번째 시트(GTC조항템플릿)에 데이터를 입력하세요"], [" - 헤더 행은 수정하지 마세요"], [" - 예시 데이터(노란색 행)는 삭제하고 실제 데이터를 입력하세요"], [""], ["2. 필수 입력 항목"], [" - 채번: 필수 입력 (예: 1.1, 2.3.1)"], [" - 소제목: 필수 입력"], [""], ["3. 선택 입력 항목"], [" - 상세항목: 조항의 구체적인 내용"], [" - 분류: 조항의 카테고리 (예: 일반조항, 특별조항)"], [" - 순서: 숫자 (기본값: 10, 20, 30...)"], [" - 상위 조항 ID: 계층 구조를 만들 때 사용"], [" - 활성 상태: '활성' 또는 '비활성' (기본값: 활성)"], [" - 편집 사유: 작성/수정 이유"], [""], ["4. 자동 처리 항목"], [" - ID, 생성일, 수정일: 시스템에서 자동 생성"], [" - 계층 깊이: 상위 조항 ID를 기반으로 자동 계산"], [" - 전체 경로: 시스템에서 자동 생성"], [""], ["5. 채번 규칙"], [" - 같은 부모 하에서 채번은 유일해야 합니다"], [" - 예: 상위 조항이 같으면 1.1, 1.2는 가능하지만 1.1이 중복되면 오류"], [""], ["6. 계층 구조 만들기"], [" - 상위 조항 ID: 기존 조항의 ID를 입력"], [" - 예: ID가 5인 조항 하위에 조항을 만들려면 상위 조항 ID에 5 입력"], [" - 최상위 조항은 상위 조항 ID를 비워두세요"], [""], ["7. 주의사항"], [" - 순서는 숫자로 입력하세요 (소수점 가능: 10, 15.5, 20)"], [" - 상위 조항 ID는 반드시 존재하는 조항의 ID여야 합니다"], [" - 파일 저장 시 .xlsx 형식으로 저장하세요"], ] instructions.forEach((instruction, idx) => { const row = instructionSheet.addRow(instruction) if (idx === 0) { row.font = { bold: true, size: 14 } row.alignment = { horizontal: "center" } } else if (instruction[0]?.match(/^\d+\./)) { row.font = { bold: true } } }) instructionSheet.getColumn(1).width = 80 // 파일 다운로드 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) } /** * Excel 파일에서 데이터 파싱 */ export async function parseExcelFile( file: File, columns: ExcelColumnDef[], { hasGroupHeader = true, sheetName = "GTC조항템플릿", }: { hasGroupHeader?: boolean sheetName?: string } = {} ): Promise<{ data: Partial[] errors: string[] }> { const errors: string[] = [] const data: Partial[] = [] try { const arrayBuffer = await file.arrayBuffer() const workbook = new ExcelJS.Workbook() await workbook.xlsx.load(arrayBuffer) const worksheet = workbook.getWorksheet(sheetName) || workbook.worksheets[0] if (!worksheet) { errors.push("워크시트를 찾을 수 없습니다.") return { data, errors } } // 헤더 행 인덱스 결정 const headerRowIndex = hasGroupHeader ? 2 : 1 const dataStartRowIndex = headerRowIndex + 1 // 헤더 검증 const headerRow = worksheet.getRow(headerRowIndex) const expectedHeaders = columns.map(col => col.header) for (let i = 0; i < expectedHeaders.length; i++) { const cellValue = headerRow.getCell(i + 1).value?.toString() || "" if (cellValue !== expectedHeaders[i]) { errors.push(`헤더가 일치하지 않습니다. 예상: "${expectedHeaders[i]}", 실제: "${cellValue}"`) } } if (errors.length > 0) { return { data, errors } } // 데이터 파싱 let rowIndex = dataStartRowIndex while (rowIndex <= worksheet.actualRowCount) { const row = worksheet.getRow(rowIndex) // 빈 행 체크 (모든 셀이 비어있으면 스킵) const isEmpty = columns.every((col, colIndex) => { const cellValue = row.getCell(colIndex + 1).value return !cellValue || cellValue.toString().trim() === "" }) if (isEmpty) { rowIndex++ continue } const rowData: Partial = {} let hasError = false columns.forEach((col, colIndex) => { const cellValue = row.getCell(colIndex + 1).value let processedValue: any = cellValue // 데이터 타입별 처리 if (cellValue !== null && cellValue !== undefined) { const strValue = cellValue.toString().trim() // 특별한 처리가 필요한 컬럼들 if (col.id === "isActive") { processedValue = strValue === "활성" } else if (col.id === "sortOrder") { const numValue = parseFloat(strValue) processedValue = isNaN(numValue) ? null : numValue } else if (col.id === "parentId") { const numValue = parseInt(strValue) processedValue = isNaN(numValue) ? null : numValue } else { processedValue = strValue } } // 필수 필드 검증 if ((col.id === "itemNumber" || col.id === "subtitle") && (!processedValue || processedValue === "")) { errors.push(`${rowIndex}행: ${col.header}은(는) 필수 입력 항목입니다.`) hasError = true } if (processedValue !== null && processedValue !== undefined && processedValue !== "") { (rowData as any)[col.id] = processedValue } }) if (!hasError) { data.push(rowData) } rowIndex++ } } catch (error) { errors.push(`파일 파싱 중 오류가 발생했습니다: ${error instanceof Error ? error.message : "알 수 없는 오류"}`) } return { data, errors } }