summaryrefslogtreecommitdiff
path: root/lib/basic-contract/gtc-vendor/excel-import.tsx
blob: d8f435f78427b680d2596b335a616930658ccb5c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
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 }
}