diff options
Diffstat (limited to 'lib/basic-contract/gtc-vendor/excel-import.tsx')
| -rw-r--r-- | lib/basic-contract/gtc-vendor/excel-import.tsx | 340 |
1 files changed, 340 insertions, 0 deletions
diff --git a/lib/basic-contract/gtc-vendor/excel-import.tsx b/lib/basic-contract/gtc-vendor/excel-import.tsx new file mode 100644 index 00000000..d8f435f7 --- /dev/null +++ b/lib/basic-contract/gtc-vendor/excel-import.tsx @@ -0,0 +1,340 @@ +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<void> { + 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<TData>( + file: File, + columns: ExcelColumnDef[], + { + hasGroupHeader = true, + sheetName = "GTC조항템플릿", + }: { + hasGroupHeader?: boolean + sheetName?: string + } = {} +): Promise<{ + data: Partial<TData>[] + errors: string[] +}> { + const errors: string[] = [] + const data: Partial<TData>[] = [] + + 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<TData> = {} + 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 } +}
\ No newline at end of file |
