summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/plant/excel-import-export.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-document-list/plant/excel-import-export.ts')
-rw-r--r--lib/vendor-document-list/plant/excel-import-export.ts788
1 files changed, 788 insertions, 0 deletions
diff --git a/lib/vendor-document-list/plant/excel-import-export.ts b/lib/vendor-document-list/plant/excel-import-export.ts
new file mode 100644
index 00000000..3ddb7195
--- /dev/null
+++ b/lib/vendor-document-list/plant/excel-import-export.ts
@@ -0,0 +1,788 @@
+// excel-import-export.ts
+"use client"
+
+import ExcelJS from 'exceljs'
+import {
+ excelDocumentRowSchema,
+ excelStageRowSchema,
+ type ExcelDocumentRow,
+ type ExcelStageRow,
+ type ExcelImportResult,
+ type CreateDocumentInput
+} from './document-stage-validations'
+import { DocumentStagesOnlyView } from '@/db/schema'
+
+// =============================================================================
+// 1. 엑셀 템플릿 생성 및 다운로드
+// =============================================================================
+
+// 문서 템플릿 생성
+export async function createDocumentTemplate(projectType: "ship" | "plant") {
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet("문서목록", {
+ properties: { defaultColWidth: 15 }
+ })
+
+ const baseHeaders = [
+ "문서번호*",
+ "문서명*",
+ "문서종류*",
+ "PIC",
+ "발행일",
+ "설명"
+ ]
+
+ const plantHeaders = [
+ "벤더문서번호",
+ "벤더명",
+ "벤더코드"
+ ]
+
+ const b4Headers = [
+ "C구분",
+ "D구분",
+ "Degree구분",
+ "부서구분",
+ "S구분",
+ "J구분"
+ ]
+
+ const headers = [
+ ...baseHeaders,
+ ...(projectType === "plant" ? plantHeaders : []),
+ ...b4Headers
+ ]
+
+ // 헤더 행 추가 및 스타일링
+ const headerRow = worksheet.addRow(headers)
+ headerRow.eachCell((cell, colNumber) => {
+ cell.fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FF4472C4' }
+ }
+ cell.font = {
+ color: { argb: 'FFFFFFFF' },
+ bold: true,
+ size: 11
+ }
+ cell.alignment = {
+ horizontal: 'center',
+ vertical: 'middle'
+ }
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ }
+
+ // 필수 필드 표시
+ if (cell.value && String(cell.value).includes('*')) {
+ cell.fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FFE74C3C' }
+ }
+ }
+ })
+
+ // 샘플 데이터 추가
+ const sampleData = projectType === "ship" ? [
+ "SH-2024-001",
+ "기본 설계 도면",
+ "B3",
+ "김철수",
+ new Date("2024-01-15"),
+ "선박 기본 설계 관련 문서",
+ "", "", "", "", "", "" // B4 필드들
+ ] : [
+ "PL-2024-001",
+ "공정 설계 도면",
+ "B4",
+ "이영희",
+ new Date("2024-01-15"),
+ "플랜트 공정 설계 관련 문서",
+ "V-001", // 벤더문서번호
+ "삼성엔지니어링", // 벤더명
+ "SENG", // 벤더코드
+ "C1", "D1", "DEG1", "DEPT1", "S1", "J1" // B4 필드들
+ ]
+
+ const sampleRow = worksheet.addRow(sampleData)
+ sampleRow.eachCell((cell, colNumber) => {
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ }
+
+ // 날짜 형식 설정
+ if (cell.value instanceof Date) {
+ cell.numFmt = 'yyyy-mm-dd'
+ }
+ })
+
+ // 컬럼 너비 자동 조정
+ worksheet.columns.forEach((column, index) => {
+ if (index < 6) {
+ column.width = headers[index].length + 5
+ } else {
+ column.width = 12
+ }
+ })
+
+ // 문서종류 드롭다운 설정
+ const docTypeCol = headers.indexOf("문서종류*") + 1
+ worksheet.dataValidations.add(`${String.fromCharCode(64 + docTypeCol)}2:${String.fromCharCode(64 + docTypeCol)}1000`, {
+ type: 'list',
+ allowBlank: false,
+ formulae: ['"B3,B4,B5"']
+ })
+
+ // Plant 프로젝트의 경우 우선순위 드롭다운 추가
+ if (projectType === "plant") {
+ // 여기에 추가적인 드롭다운들을 설정할 수 있습니다
+ }
+
+ return workbook
+}
+
+// 스테이지 템플릿 생성
+export async function createStageTemplate() {
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet("스테이지목록", {
+ properties: { defaultColWidth: 15 }
+ })
+
+ const headers = [
+ "문서번호*",
+ "스테이지명*",
+ "계획일",
+ "우선순위",
+ "담당자",
+ "설명",
+ "스테이지순서"
+ ]
+
+ // 헤더 행 추가 및 스타일링
+ const headerRow = worksheet.addRow(headers)
+ headerRow.eachCell((cell, colNumber) => {
+ cell.fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FF27AE60' }
+ }
+ cell.font = {
+ color: { argb: 'FFFFFFFF' },
+ bold: true,
+ size: 11
+ }
+ cell.alignment = {
+ horizontal: 'center',
+ vertical: 'middle'
+ }
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ }
+
+ // 필수 필드 표시
+ if (cell.value && String(cell.value).includes('*')) {
+ cell.fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FFE74C3C' }
+ }
+ }
+ })
+
+ // 샘플 데이터 추가
+ const sampleData = [
+ [
+ "SH-2024-001",
+ "초기 설계 검토",
+ new Date("2024-02-15"),
+ "HIGH",
+ "김철수",
+ "초기 설계안 검토 및 승인",
+ 0
+ ],
+ [
+ "SH-2024-001",
+ "상세 설계",
+ new Date("2024-03-15"),
+ "MEDIUM",
+ "이영희",
+ "상세 설계 작업 수행",
+ 1
+ ]
+ ]
+
+ sampleData.forEach(rowData => {
+ const row = worksheet.addRow(rowData)
+ row.eachCell((cell, colNumber) => {
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ }
+
+ // 날짜 형식 설정
+ if (cell.value instanceof Date) {
+ cell.numFmt = 'yyyy-mm-dd'
+ }
+ })
+ })
+
+ // 컬럼 너비 설정
+ worksheet.columns = [
+ { width: 15 }, // 문서번호
+ { width: 20 }, // 스테이지명
+ { width: 12 }, // 계획일
+ { width: 10 }, // 우선순위
+ { width: 15 }, // 담당자
+ { width: 30 }, // 설명
+ { width: 12 }, // 스테이지순서
+ ]
+
+ // 우선순위 드롭다운 설정
+ worksheet.dataValidations.add('D2:D1000', {
+ type: 'list',
+ allowBlank: true,
+ formulae: ['"HIGH,MEDIUM,LOW"']
+ })
+
+ return workbook
+}
+
+// 템플릿 다운로드 함수
+export async function downloadTemplate(type: "documents" | "stages", projectType: "ship" | "plant") {
+ const workbook = await (type === "documents"
+ ? createDocumentTemplate(projectType)
+ : createStageTemplate())
+
+ const filename = type === "documents"
+ ? `문서_임포트_템플릿_${projectType}.xlsx`
+ : `스테이지_임포트_템플릿.xlsx`
+
+ // 브라우저에서 다운로드
+ const buffer = await workbook.xlsx.writeBuffer()
+ const blob = new Blob([buffer], {
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ })
+
+ const url = window.URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = filename
+ link.click()
+
+ // 메모리 정리
+ window.URL.revokeObjectURL(url)
+}
+
+// =============================================================================
+// 2. 엑셀 파일 읽기 및 파싱
+// =============================================================================
+
+// 엑셀 파일을 읽어서 JSON으로 변환
+export async function readExcelFile(file: File): Promise<any[]> {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader()
+
+ reader.onload = async (e) => {
+ try {
+ const buffer = e.target?.result as ArrayBuffer
+ const workbook = new ExcelJS.Workbook()
+ await workbook.xlsx.load(buffer)
+
+ const worksheet = workbook.getWorksheet(1) // 첫 번째 워크시트
+ if (!worksheet) {
+ throw new Error('워크시트를 찾을 수 없습니다')
+ }
+
+ const jsonData: any[] = []
+
+ worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
+ const rowData: any[] = []
+ row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
+ let value = cell.value
+
+ // 날짜 처리
+ if (cell.type === ExcelJS.ValueType.Date) {
+ value = cell.value as Date
+ }
+ // 수식 결과값 처리
+ else if (cell.type === ExcelJS.ValueType.Formula && cell.result) {
+ value = cell.result
+ }
+ // 하이퍼링크 처리
+ else if (cell.type === ExcelJS.ValueType.Hyperlink) {
+ value = cell.value?.text || cell.value
+ }
+
+ rowData[colNumber - 1] = value || ""
+ })
+
+ jsonData.push(rowData)
+ })
+
+ resolve(jsonData)
+ } catch (error) {
+ reject(new Error('엑셀 파일을 읽는 중 오류가 발생했습니다: ' + error))
+ }
+ }
+
+ reader.onerror = () => {
+ reject(new Error('파일을 읽을 수 없습니다'))
+ }
+
+ reader.readAsArrayBuffer(file)
+ })
+}
+
+// 문서 데이터 유효성 검사 및 변환
+export function validateDocumentRows(
+ rawData: any[],
+ contractId: number,
+ projectType: "ship" | "plant"
+): { validData: CreateDocumentInput[], errors: any[] } {
+ if (rawData.length < 2) {
+ throw new Error('데이터가 없습니다. 최소 헤더와 1개 행이 필요합니다.')
+ }
+
+ const headers = rawData[0] as string[]
+ const rows = rawData.slice(1)
+
+ const validData: CreateDocumentInput[] = []
+ const errors: any[] = []
+
+ // 필수 헤더 검사
+ const requiredHeaders = ["문서번호", "문서명", "문서종류"]
+ const missingHeaders = requiredHeaders.filter(h =>
+ !headers.some(header => header.includes(h.replace("*", "")))
+ )
+
+ if (missingHeaders.length > 0) {
+ throw new Error(`필수 헤더가 누락되었습니다: ${missingHeaders.join(", ")}`)
+ }
+
+ // 헤더 인덱스 매핑
+ const headerMap: Record<string, number> = {}
+ headers.forEach((header, index) => {
+ const cleanHeader = header.replace("*", "").trim()
+ headerMap[cleanHeader] = index
+ })
+
+ // 각 행 처리
+ rows.forEach((row: any[], rowIndex) => {
+ try {
+ // 빈 행 스킵
+ if (row.every(cell => !cell || String(cell).trim() === "")) {
+ return
+ }
+
+ const rowData: any = {
+ contractId,
+ docNumber: String(row[headerMap["문서번호"]] || "").trim(),
+ title: String(row[headerMap["문서명"]] || "").trim(),
+ drawingKind: String(row[headerMap["문서종류"]] || "").trim(),
+ pic: String(row[headerMap["PIC"]] || "").trim() || undefined,
+ issuedDate: row[headerMap["발행일"]] ?
+ formatExcelDate(row[headerMap["발행일"]]) : undefined,
+ }
+
+ // Plant 프로젝트 전용 필드
+ if (projectType === "plant") {
+ rowData.vendorDocNumber = String(row[headerMap["벤더문서번호"]] || "").trim() || undefined
+ }
+
+ // B4 전용 필드들
+ const b4Fields = ["C구분", "D구분", "Degree구분", "부서구분", "S구분", "J구분"]
+ const b4FieldMap = {
+ "C구분": "cGbn",
+ "D구분": "dGbn",
+ "Degree구분": "degreeGbn",
+ "부서구분": "deptGbn",
+ "S구분": "sGbn",
+ "J구분": "jGbn"
+ }
+
+ b4Fields.forEach(field => {
+ if (headerMap[field] !== undefined) {
+ const value = String(row[headerMap[field]] || "").trim()
+ if (value) {
+ rowData[b4FieldMap[field as keyof typeof b4FieldMap]] = value
+ }
+ }
+ })
+
+ // 유효성 검사
+ const validatedData = excelDocumentRowSchema.parse({
+ "문서번호": rowData.docNumber,
+ "문서명": rowData.title,
+ "문서종류": rowData.drawingKind,
+ "벤더문서번호": rowData.vendorDocNumber,
+ "PIC": rowData.pic,
+ "발행일": rowData.issuedDate,
+ "C구분": rowData.cGbn,
+ "D구분": rowData.dGbn,
+ "Degree구분": rowData.degreeGbn,
+ "부서구분": rowData.deptGbn,
+ "S구분": rowData.sGbn,
+ "J구분": rowData.jGbn,
+ })
+
+ // CreateDocumentInput 형태로 변환
+ const documentInput: CreateDocumentInput = {
+ contractId,
+ docNumber: validatedData["문서번호"],
+ title: validatedData["문서명"],
+ drawingKind: validatedData["문서종류"],
+ vendorDocNumber: validatedData["벤더문서번호"],
+ pic: validatedData["PIC"],
+ issuedDate: validatedData["발행일"],
+ cGbn: validatedData["C구분"],
+ dGbn: validatedData["D구분"],
+ degreeGbn: validatedData["Degree구분"],
+ deptGbn: validatedData["부서구분"],
+ sGbn: validatedData["S구분"],
+ jGbn: validatedData["J구분"],
+ }
+
+ validData.push(documentInput)
+
+ } catch (error) {
+ errors.push({
+ row: rowIndex + 2, // 엑셀 행 번호 (헤더 포함)
+ message: error instanceof Error ? error.message : "알 수 없는 오류",
+ data: row
+ })
+ }
+ })
+
+ return { validData, errors }
+}
+
+// 엑셀 날짜 형식 변환
+function formatExcelDate(value: any): string | undefined {
+ if (!value) return undefined
+
+ // ExcelJS에서 Date 객체로 처리된 경우
+ if (value instanceof Date) {
+ return value.toISOString().split('T')[0]
+ }
+
+ // 이미 문자열 날짜 형식인 경우
+ if (typeof value === 'string') {
+ const dateMatch = value.match(/^\d{4}-\d{2}-\d{2}$/)
+ if (dateMatch) return value
+
+ // 다른 형식 시도
+ const date = new Date(value)
+ if (!isNaN(date.getTime())) {
+ return date.toISOString().split('T')[0]
+ }
+ }
+
+ // 엑셀 시리얼 날짜인 경우
+ if (typeof value === 'number') {
+ // ExcelJS는 이미 Date 객체로 변환해주므로 이 경우는 드물지만
+ // 1900년 1월 1일부터의 일수로 계산
+ const excelEpoch = new Date(1900, 0, 1)
+ const date = new Date(excelEpoch.getTime() + (value - 2) * 24 * 60 * 60 * 1000)
+ if (!isNaN(date.getTime())) {
+ return date.toISOString().split('T')[0]
+ }
+ }
+
+ return undefined
+}
+
+// =============================================================================
+// 3. 데이터 익스포트
+// =============================================================================
+
+// 문서 데이터를 엑셀로 익스포트
+export function exportDocumentsToExcel(
+ documents: DocumentStagesOnlyView[],
+ projectType: "ship" | "plant"
+) {
+ const headers = [
+ "문서번호",
+ "문서명",
+ "문서종류",
+ "PIC",
+ "발행일",
+ "현재스테이지",
+ "스테이지상태",
+ "계획일",
+ "담당자",
+ "우선순위",
+ "진행률(%)",
+ "완료스테이지",
+ "전체스테이지",
+ "지연여부",
+ "남은일수",
+ "생성일",
+ "수정일"
+ ]
+
+ // Plant 프로젝트 전용 헤더 추가
+ if (projectType === "plant") {
+ headers.splice(3, 0, "벤더문서번호", "벤더명", "벤더코드")
+ }
+
+ const data = documents.map(doc => {
+ const baseData = [
+ doc.docNumber,
+ doc.title,
+ doc.drawingKind || "",
+ doc.pic || "",
+ doc.issuedDate || "",
+ doc.currentStageName || "",
+ getStatusText(doc.currentStageStatus || ""),
+ doc.currentStagePlanDate || "",
+ doc.currentStageAssigneeName || "",
+ getPriorityText(doc.currentStagePriority || ""),
+ doc.progressPercentage || 0,
+ doc.completedStages || 0,
+ doc.totalStages || 0,
+ doc.isOverdue ? "예" : "아니오",
+ doc.daysUntilDue || "",
+ doc.createdAt ? new Date(doc.createdAt).toLocaleDateString() : "",
+ doc.updatedAt ? new Date(doc.updatedAt).toLocaleDateString() : ""
+ ]
+
+ // Plant 프로젝트 데이터 추가
+ if (projectType === "plant") {
+ baseData.splice(3, 0,
+ doc.vendorDocNumber || "",
+ doc.vendorName || "",
+ doc.vendorCode || ""
+ )
+ }
+
+ return baseData
+ })
+
+ const worksheet = XLSX.utils.aoa_to_sheet([headers, ...data])
+
+ // 컬럼 너비 설정
+ const colWidths = [
+ { wch: 15 }, // 문서번호
+ { wch: 30 }, // 문서명
+ { wch: 10 }, // 문서종류
+ ...(projectType === "plant" ? [
+ { wch: 15 }, // 벤더문서번호
+ { wch: 20 }, // 벤더명
+ { wch: 10 }, // 벤더코드
+ ] : []),
+ { wch: 10 }, // PIC
+ { wch: 12 }, // 발행일
+ { wch: 15 }, // 현재스테이지
+ { wch: 10 }, // 스테이지상태
+ { wch: 12 }, // 계획일
+ { wch: 10 }, // 담당자
+ { wch: 8 }, // 우선순위
+ { wch: 8 }, // 진행률
+ { wch: 8 }, // 완료스테이지
+ { wch: 8 }, // 전체스테이지
+ { wch: 8 }, // 지연여부
+ { wch: 8 }, // 남은일수
+ { wch: 12 }, // 생성일
+ { wch: 12 }, // 수정일
+ ]
+
+ worksheet['!cols'] = colWidths
+
+ const workbook = XLSX.utils.book_new()
+ XLSX.utils.book_append_sheet(workbook, worksheet, "문서목록")
+
+ const filename = `문서목록_${new Date().toISOString().split('T')[0]}.xlsx`
+ XLSX.writeFile(workbook, filename)
+}
+
+// 스테이지 상세 데이터를 엑셀로 익스포트
+export function exportStageDetailsToExcel(documents: DocumentStagesOnlyView[]) {
+ const headers = [
+ "문서번호",
+ "문서명",
+ "스테이지명",
+ "스테이지상태",
+ "스테이지순서",
+ "계획일",
+ "담당자",
+ "우선순위",
+ "설명",
+ "노트",
+ "알림일수"
+ ]
+
+ const data: any[] = []
+
+ documents.forEach(doc => {
+ if (doc.allStages && doc.allStages.length > 0) {
+ doc.allStages.forEach(stage => {
+ data.push([
+ doc.docNumber,
+ doc.title,
+ stage.stageName,
+ getStatusText(stage.stageStatus),
+ stage.stageOrder,
+ stage.planDate || "",
+ stage.assigneeName || "",
+ getPriorityText(stage.priority),
+ stage.description || "",
+ stage.notes || "",
+ stage.reminderDays || ""
+ ])
+ })
+ } else {
+ // 스테이지가 없는 문서도 포함
+ data.push([
+ doc.docNumber,
+ doc.title,
+ "", "", "", "", "", "", "", "", ""
+ ])
+ }
+ })
+
+ const worksheet = XLSX.utils.aoa_to_sheet([headers, ...data])
+
+ // 컬럼 너비 설정
+ worksheet['!cols'] = [
+ { wch: 15 }, // 문서번호
+ { wch: 30 }, // 문서명
+ { wch: 20 }, // 스테이지명
+ { wch: 12 }, // 스테이지상태
+ { wch: 8 }, // 스테이지순서
+ { wch: 12 }, // 계획일
+ { wch: 10 }, // 담당자
+ { wch: 8 }, // 우선순위
+ { wch: 25 }, // 설명
+ { wch: 25 }, // 노트
+ { wch: 8 }, // 알림일수
+ ]
+
+ const workbook = XLSX.utils.book_new()
+ XLSX.utils.book_append_sheet(workbook, worksheet, "스테이지상세")
+
+ const filename = `스테이지상세_${new Date().toISOString().split('T')[0]}.xlsx`
+ XLSX.writeFile(workbook, filename)
+}
+
+// =============================================================================
+// 4. 유틸리티 함수들
+// =============================================================================
+
+function getStatusText(status: string): string {
+ switch (status) {
+ case 'PLANNED': return '계획됨'
+ case 'IN_PROGRESS': return '진행중'
+ case 'SUBMITTED': return '제출됨'
+ case 'UNDER_REVIEW': return '검토중'
+ case 'APPROVED': return '승인됨'
+ case 'REJECTED': return '반려됨'
+ case 'COMPLETED': return '완료됨'
+ default: return status
+ }
+}
+
+function getPriorityText(priority: string): string {
+ switch (priority) {
+ case 'HIGH': return '높음'
+ case 'MEDIUM': return '보통'
+ case 'LOW': return '낮음'
+ default: return priority
+ }
+}
+
+// 파일 크기 검증
+export function validateFileSize(file: File, maxSizeMB: number = 10): boolean {
+ const maxSizeBytes = maxSizeMB * 1024 * 1024
+ return file.size <= maxSizeBytes
+}
+
+// 파일 확장자 검증
+export function validateFileExtension(file: File): boolean {
+ const allowedExtensions = ['.xlsx', '.xls']
+ const fileName = file.name.toLowerCase()
+ return allowedExtensions.some(ext => fileName.endsWith(ext))
+}
+
+// ExcelJS 워크북의 유효성 검사
+export async function validateExcelWorkbook(file: File): Promise<{
+ isValid: boolean
+ error?: string
+ worksheetCount?: number
+ firstWorksheetName?: string
+}> {
+ try {
+ const buffer = await file.arrayBuffer()
+ const workbook = new ExcelJS.Workbook()
+ await workbook.xlsx.load(buffer)
+
+ const worksheets = workbook.worksheets
+ if (worksheets.length === 0) {
+ return {
+ isValid: false,
+ error: '워크시트가 없는 파일입니다'
+ }
+ }
+
+ const firstWorksheet = worksheets[0]
+ if (firstWorksheet.rowCount < 2) {
+ return {
+ isValid: false,
+ error: '데이터가 없습니다. 최소 헤더와 1개 행이 필요합니다'
+ }
+ }
+
+ return {
+ isValid: true,
+ worksheetCount: worksheets.length,
+ firstWorksheetName: firstWorksheet.name
+ }
+ } catch (error) {
+ return {
+ isValid: false,
+ error: `파일을 읽을 수 없습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`
+ }
+ }
+}
+
+// 셀 값을 안전하게 문자열로 변환
+export function getCellValueAsString(cell: ExcelJS.Cell): string {
+ if (!cell.value) return ""
+
+ if (cell.value instanceof Date) {
+ return cell.value.toISOString().split('T')[0]
+ }
+
+ if (typeof cell.value === 'object' && 'text' in cell.value) {
+ return cell.value.text || ""
+ }
+
+ if (typeof cell.value === 'object' && 'result' in cell.value) {
+ return String(cell.value.result || "")
+ }
+
+ return String(cell.value)
+}
+
+// 엑셀 컬럼 인덱스를 문자로 변환 (A, B, C, ... Z, AA, AB, ...)
+export function getExcelColumnName(index: number): string {
+ let result = ""
+ while (index > 0) {
+ index--
+ result = String.fromCharCode(65 + (index % 26)) + result
+ index = Math.floor(index / 26)
+ }
+ return result
+} \ No newline at end of file