From 10f90dc68dec42e9a64e081cc0dce6a484447290 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Tue, 29 Jul 2025 11:48:59 +0000
Subject: (대표님, 박서영, 최겸) document-list-only, gtc, vendorDocu,
 docu-list-rule
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gtc-clauses/table/excel-import.tsx             | 340 +++++++++++++++++++++
 1 file changed, 340 insertions(+)
 create mode 100644 lib/gtc-contract/gtc-clauses/table/excel-import.tsx

(limited to 'lib/gtc-contract/gtc-clauses/table/excel-import.tsx')

diff --git a/lib/gtc-contract/gtc-clauses/table/excel-import.tsx b/lib/gtc-contract/gtc-clauses/table/excel-import.tsx
new file mode 100644
index 00000000..d8f435f7
--- /dev/null
+++ b/lib/gtc-contract/gtc-clauses/table/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
-- 
cgit v1.2.3

