From bc5b316f3c9db8e53a5146afda115dd15c7a76cd Mon Sep 17 00:00:00 2001 From: sheetEasy AI Team Date: Fri, 20 Jun 2025 15:17:36 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9F=AD=ED=82=A4=EC=97=91=EC=85=80=EC=97=90?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EC=9D=B8=EC=A0=9D=EC=85=98=20=EC=84=B1?= =?UTF-8?q?=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/rules/luckyexcel-api.mdc | 5 + src/utils/fileProcessor.ts | 452 +++++++++++++++++++++---------- 2 files changed, 311 insertions(+), 146 deletions(-) create mode 100644 .cursor/rules/luckyexcel-api.mdc diff --git a/.cursor/rules/luckyexcel-api.mdc b/.cursor/rules/luckyexcel-api.mdc new file mode 100644 index 0000000..b93c988 --- /dev/null +++ b/.cursor/rules/luckyexcel-api.mdc @@ -0,0 +1,5 @@ +--- +description: +globs: +alwaysApply: false +--- diff --git a/src/utils/fileProcessor.ts b/src/utils/fileProcessor.ts index f0c89cd..d77150a 100644 --- a/src/utils/fileProcessor.ts +++ b/src/utils/fileProcessor.ts @@ -4,8 +4,8 @@ import type { SheetData, FileUploadResult } from "../types/sheet"; /** * 파일 처리 관련 유틸리티 - 개선된 버전 - * - 모든 파일 형식 (CSV, XLS, XLSX)을 SheetJS를 통해 처리 - * - LuckyExcel 우선 시도, 실패 시 SheetJS Fallback 사용 + * - 모든 파일 형식을 SheetJS를 통해 읽은 후 XLSX로 변환 + * - 변환된 XLSX 파일을 LuckyExcel로 전달 * - 안정적인 한글 지원 및 에러 처리 */ @@ -122,10 +122,6 @@ function validateWorkbook(workbook: any): { isValid: boolean; error?: string } { return { isValid: false, error: "워크북에 시트가 없습니다" }; } - if (!workbook.Sheets) { - return { isValid: false, error: "워크북에 Sheets 속성이 없습니다" }; - } - return { isValid: true }; } @@ -421,60 +417,270 @@ function convertSheetJSToLuckyExcel(workbook: any): SheetData[] { } /** - * SheetJS를 사용한 Fallback 처리 + * SheetJS로 파일을 읽고 XLSX로 변환한 뒤 LuckyExcel로 처리 */ -async function processWithSheetJSFallback(file: File): Promise { - console.log("🔄 SheetJS Fallback 처리 시작..."); +async function processFileWithSheetJSToXLSX(file: File): Promise { + console.log("📊 SheetJS → XLSX → LuckyExcel 파이프라인 시작..."); const arrayBuffer = await file.arrayBuffer(); + const fileName = file.name.toLowerCase(); + const isCSV = fileName.endsWith(".csv"); + const isXLS = fileName.endsWith(".xls"); + const isXLSX = fileName.endsWith(".xlsx"); - // 단순한 SheetJS 옵션 사용 (이전 코드 방식) - const workbook = XLSX.read(arrayBuffer, { - type: "array", - cellDates: true, - cellNF: false, - cellText: false, - codepage: 65001, // UTF-8 - dense: false, - sheetStubs: true, - raw: false, - }); + // 1단계: SheetJS로 파일 읽기 + let workbook: any; - console.log("📊 SheetJS Fallback 워크북 정보:", { + try { + if (isCSV) { + // CSV 파일 처리 - UTF-8 디코딩 후 읽기 + console.log("📄 CSV 파일을 SheetJS로 읽는 중..."); + const text = new TextDecoder("utf-8").decode(arrayBuffer); + workbook = XLSX.read(text, { + type: "string", + codepage: 65001, // UTF-8 + raw: false, + }); + } else { + // XLS/XLSX 파일 처리 - 관대한 옵션으로 읽기 + console.log(`📊 ${isXLS ? "XLS" : "XLSX"} 파일을 SheetJS로 읽는 중...`); + workbook = XLSX.read(arrayBuffer, { + type: "array", + cellText: true, + sheetStubs: true, + WTF: true, + bookSheets: false, + codepage: 65001, + raw: false, + }); + + // Sheets가 없고 SheetNames만 있는 경우 재시도 + if (workbook.SheetNames?.length > 0 && !workbook.Sheets) { + console.log("⚠️ Sheets 속성이 없어서 재읽기 시도..."); + workbook = XLSX.read(arrayBuffer, { + type: "array", + cellText: true, + sheetStubs: true, + WTF: true, + bookSheets: true, // 강제로 시트 읽기 + codepage: 65001, + raw: false, + }); + + // 여전히 실패하면 수동으로 빈 시트 생성 + if (!workbook.Sheets && workbook.SheetNames?.length > 0) { + console.log("⚠️ 수동으로 빈 시트 생성..."); + workbook.Sheets = {}; + workbook.SheetNames.forEach((sheetName: string) => { + workbook.Sheets[sheetName] = { + "!ref": "A1:A1", + A1: { v: "", t: "s" }, + }; + }); + } + } + } + } catch (readError) { + console.error("❌ SheetJS 파일 읽기 실패:", readError); + throw new Error( + `파일을 읽을 수 없습니다: ${readError instanceof Error ? readError.message : readError}`, + ); + } + + // 파일 버퍼 크기 검증 + if (arrayBuffer.byteLength === 0) { + throw new Error("파일이 비어있습니다."); + } + + // 워크북 null 체크 + if (!workbook) { + throw new Error("워크북을 생성할 수 없습니다."); + } + + // workbook.Sheets 존재 및 타입 검증 + if (!workbook.Sheets || typeof workbook.Sheets !== "object") { + throw new Error("유효한 시트가 없습니다."); + } + + // workbook.SheetNames 배열 검증 + if (!Array.isArray(workbook.SheetNames) || workbook.SheetNames.length === 0) { + throw new Error("시트 이름 정보가 없습니다."); + } + + console.log("✅ SheetJS 워크북 읽기 성공:", { sheetNames: workbook.SheetNames, sheetCount: workbook.SheetNames.length, }); - if (workbook.SheetNames.length === 0) { - throw new Error("파일에 워크시트가 없습니다."); + // 2단계: 워크북을 XLSX ArrayBuffer로 변환 + let xlsxArrayBuffer: ArrayBuffer; + try { + console.log("🔄 XLSX 형식으로 변환 중..."); + const xlsxData = XLSX.write(workbook, { + type: "array", + bookType: "xlsx", + compression: true, + }); + // xlsxData는 Uint8Array이므로 ArrayBuffer로 변환 + if (xlsxData instanceof Uint8Array) { + xlsxArrayBuffer = xlsxData.buffer.slice( + xlsxData.byteOffset, + xlsxData.byteOffset + xlsxData.byteLength, + ); + } else if (xlsxData instanceof ArrayBuffer) { + xlsxArrayBuffer = xlsxData; + } else { + // 다른 타입의 경우 새 ArrayBuffer 생성 + xlsxArrayBuffer = new ArrayBuffer(xlsxData.length); + const view = new Uint8Array(xlsxArrayBuffer); + for (let i = 0; i < xlsxData.length; i++) { + view[i] = xlsxData[i]; + } + } + + console.log(`✅ XLSX 변환 완료: ${xlsxArrayBuffer.byteLength} bytes`); + + // ⏱️ ArrayBuffer 변환 완료 확인 및 검증 + console.log("⏱️ ArrayBuffer 변환 검증 중..."); + + // ArrayBuffer 무결성 검증 + if (!xlsxArrayBuffer || xlsxArrayBuffer.byteLength === 0) { + throw new Error("ArrayBuffer 변환 실패: 빈 버퍼"); + } + + // XLSX 파일 시그니처 사전 검증 + const uint8Check = new Uint8Array(xlsxArrayBuffer); + const signatureCheck = Array.from(uint8Check.slice(0, 4)) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(" "); + + if (signatureCheck !== "50 4b 03 04") { + console.warn( + `⚠️ 잘못된 XLSX 시그니처: ${signatureCheck} (예상: 50 4b 03 04)`, + ); + } + + console.log( + `✅ ArrayBuffer 검증 완료: ${xlsxArrayBuffer.byteLength} bytes, 시그니처: ${signatureCheck}`, + ); + } catch (writeError) { + console.error("❌ XLSX 변환 실패:", writeError); + throw new Error( + `XLSX 변환 실패: ${writeError instanceof Error ? writeError.message : writeError}`, + ); } - return convertSheetJSToLuckyExcel(workbook); -} + // 3단계: ArrayBuffer가 완전히 준비된 후 LuckyExcel로 처리 + console.log("🍀 LuckyExcel로 변환된 XLSX 처리 중..."); -/** - * LuckyExcel 처리 함수 (개선된 버전) - */ -function processWithLuckyExcel(file: File): Promise { - return new Promise(async (resolve, reject) => { - console.log("🍀 LuckyExcel 처리 시작..."); + // ArrayBuffer 최종 검증 + if (!xlsxArrayBuffer) { + throw new Error("ArrayBuffer가 생성되지 않았습니다"); + } - console.log("🔍 LuckyExcel 변환 시작:", { - hasFile: !!file, - fileName: file.name, - fileSize: file.size, - fileType: file.type, - }); + if (xlsxArrayBuffer.byteLength === 0) { + throw new Error("ArrayBuffer 크기가 0입니다"); + } + // 원본 파일명에서 확장자를 .xlsx로 변경 + const xlsxFileName = file.name.replace(/\.(csv|xls|xlsx)$/i, ".xlsx"); + + // 🔍 LuckyExcel로 전달되는 파일 정보 출력 + console.log("📋 ================================="); + console.log("📋 LuckyExcel로 전달되는 파일 정보:"); + console.log("📋 ================================="); + console.log("📋 타이밍:", new Date().toISOString()); + console.log("📋 원본 파일명:", file.name); + console.log("📋 변환된 파일명:", xlsxFileName); + console.log("📋 ArrayBuffer 크기:", xlsxArrayBuffer.byteLength, "bytes"); + console.log("📋 ArrayBuffer 타입:", xlsxArrayBuffer.constructor.name); + + // ArrayBuffer의 처음 100바이트를 16진수로 출력 (헥스 덤프) + const uint8View = new Uint8Array(xlsxArrayBuffer); + const firstBytes = Array.from( + uint8View.slice(0, Math.min(100, uint8View.length)), + ) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(" "); + console.log("📋 ArrayBuffer 처음 100바이트 (hex):", firstBytes); + + // XLSX 파일 시그니처 확인 (PK\x03\x04 또는 50 4B 03 04) + const signature = Array.from(uint8View.slice(0, 4)) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(" "); + console.log( + "📋 파일 시그니처:", + signature, + signature === "50 4b 03 04" ? "(✅ 유효한 XLSX)" : "(❌ 잘못된 시그니처)", + ); + console.log("📋 ================================="); + + // 🚀 LuckyExcel 호출 직전 최종 검증 + console.log("🚀 LuckyExcel 호출 직전 최종 검증:"); + console.log("🚀 ArrayBuffer 타입:", typeof xlsxArrayBuffer); + console.log("🚀 ArrayBuffer 생성자 확인:", xlsxArrayBuffer.constructor.name); + console.log("🚀 ArrayBuffer 크기:", xlsxArrayBuffer.byteLength); + console.log("🚀 ArrayBuffer.isView:", ArrayBuffer.isView(xlsxArrayBuffer)); + console.log("🚀 fileName:", xlsxFileName, "타입:", typeof xlsxFileName); + + console.log("🚀 LuckyExcel 객체:", typeof LuckyExcel); + console.log( + "🚀 transformExcelToLucky 함수:", + typeof (LuckyExcel as any).transformExcelToLucky, + ); + + console.log("🚀 LuckyExcel 호출 시작..."); + + // Promise를 사용한 LuckyExcel 처리 + return new Promise((resolve, reject) => { try { - // File을 ArrayBuffer로 변환 - const arrayBuffer = await file.arrayBuffer(); - - LuckyExcel.transformExcelToLucky( - arrayBuffer, // ArrayBuffer 전달 - file.name, // 파일명 전달 + // LuckyExcel API는 (arrayBuffer, successCallback, errorCallback) 형태로 호출 + // 공식 문서: LuckyExcel.transformExcelToLucky(file, successCallback, errorCallback) + (LuckyExcel as any).transformExcelToLucky( + xlsxArrayBuffer, + // 성공 콜백 함수 (두 번째 매개변수) (exportJson: any, luckysheetfile: any) => { try { + console.log("🍀 ================================="); + console.log("🍀 LuckyExcel 변환 결과 상세 정보:"); + console.log("🍀 ================================="); + console.log("🍀 원본 파일명:", xlsxFileName); + console.log("🍀 exportJson 존재:", !!exportJson); + console.log("🍀 exportJson 타입:", typeof exportJson); + + if (exportJson) { + console.log("🍀 exportJson 전체 구조:", exportJson); + console.log("🍀 exportJson.sheets 존재:", !!exportJson.sheets); + console.log( + "🍀 exportJson.sheets 타입:", + typeof exportJson.sheets, + ); + console.log( + "🍀 exportJson.sheets 배열 여부:", + Array.isArray(exportJson.sheets), + ); + console.log("🍀 시트 개수:", exportJson?.sheets?.length || 0); + + if (exportJson.sheets && Array.isArray(exportJson.sheets)) { + exportJson.sheets.forEach((sheet: any, index: number) => { + console.log(`🍀 시트 ${index + 1}:`, { + name: sheet.name, + row: sheet.row, + column: sheet.column, + celldata길이: sheet.celldata?.length || 0, + 키목록: Object.keys(sheet), + }); + }); + } + } + + console.log("🍀 luckysheetfile 존재:", !!luckysheetfile); + console.log("🍀 luckysheetfile 타입:", typeof luckysheetfile); + if (luckysheetfile) { + console.log("🍀 luckysheetfile 구조:", luckysheetfile); + } + console.log("🍀 ================================="); + console.log("🔍 LuckyExcel 변환 결과:", { hasExportJson: !!exportJson, hasSheets: !!exportJson?.sheets, @@ -485,41 +691,16 @@ function processWithLuckyExcel(file: File): Promise { if ( !exportJson || !exportJson.sheets || - !Array.isArray(exportJson.sheets) + !Array.isArray(exportJson.sheets) || + exportJson.sheets.length === 0 ) { console.warn( - "⚠️ LuckyExcel 결과가 유효하지 않습니다. SheetJS Fallback을 시도합니다.", + "⚠️ LuckyExcel 결과가 유효하지 않습니다. SheetJS 방식으로 대체 처리합니다.", ); - // SheetJS Fallback 처리 - processWithSheetJSFallback(file) - .then(resolve) - .catch((fallbackError) => { - console.error("❌ SheetJS Fallback도 실패:", fallbackError); - reject( - new Error( - `파일 처리 실패: ${fallbackError instanceof Error ? fallbackError.message : fallbackError}`, - ), - ); - }); - return; - } - - if (exportJson.sheets.length === 0) { - console.warn( - "⚠️ LuckyExcel이 빈 시트를 반환했습니다. SheetJS Fallback을 시도합니다.", - ); - - processWithSheetJSFallback(file) - .then(resolve) - .catch((fallbackError) => { - console.error("❌ SheetJS Fallback도 실패:", fallbackError); - reject( - new Error( - `파일 처리 실패: ${fallbackError instanceof Error ? fallbackError.message : fallbackError}`, - ), - ); - }); + // LuckyExcel 실패 시 SheetJS 데이터를 직접 변환 + const fallbackSheets = convertSheetJSToLuckyExcel(workbook); + resolve(fallbackSheets); return; } @@ -596,24 +777,56 @@ function processWithLuckyExcel(file: File): Promise { console.log("✅ LuckyExcel 처리 성공:", sheets.length, "개 시트"); resolve(sheets); - } catch (e) { - console.error("❌ LuckyExcel 후처리 중 오류:", e); - reject(e); + } catch (processError) { + console.error("❌ LuckyExcel 후처리 중 오류:", processError); + + // LuckyExcel 후처리 실패 시 SheetJS 방식으로 대체 + try { + console.log("🔄 SheetJS 방식으로 대체 처리..."); + const fallbackSheets = convertSheetJSToLuckyExcel(workbook); + resolve(fallbackSheets); + } catch (fallbackError) { + console.error("❌ SheetJS 대체 처리도 실패:", fallbackError); + reject(fallbackError); + } + } + }, + // 오류 콜백 함수 (세 번째 매개변수) + (error: any) => { + console.error("❌ LuckyExcel 변환 오류:", error); + + // LuckyExcel 오류 시 SheetJS 방식으로 대체 + try { + console.log("🔄 SheetJS 방식으로 대체 처리..."); + const fallbackSheets = convertSheetJSToLuckyExcel(workbook); + resolve(fallbackSheets); + } catch (fallbackError) { + console.error("❌ SheetJS 대체 처리도 실패:", fallbackError); + reject(fallbackError); } }, ); - } catch (error) { - console.error("❌ LuckyExcel 처리 중 오류:", error); - reject(error); + } catch (luckyError) { + console.error("❌ LuckyExcel 호출 중 오류:", luckyError); + + // LuckyExcel 호출 실패 시 SheetJS 방식으로 대체 + try { + console.log("🔄 SheetJS 방식으로 대체 처리..."); + const fallbackSheets = convertSheetJSToLuckyExcel(workbook); + resolve(fallbackSheets); + } catch (fallbackError) { + console.error("❌ SheetJS 대체 처리도 실패:", fallbackError); + reject(fallbackError); + } } }); } /** * 엑셀 파일을 SheetData 배열로 변환 (개선된 버전) - * - 우선 LuckyExcel로 처리 시도 (XLSX) - * - CSV, XLS는 SheetJS로 XLSX 변환 후 LuckyExcel 처리 - * - 실패 시 SheetJS Fallback 사용 + * - 모든 파일을 SheetJS로 읽은 후 XLSX로 변환 + * - 변환된 XLSX를 LuckyExcel로 처리 + * - 실패 시 SheetJS 직접 변환으로 Fallback */ export async function processExcelFile(file: File): Promise { try { @@ -647,68 +860,8 @@ export async function processExcelFile(file: File): Promise { `📁 파일 처리 시작: ${file.name} (${isCSV ? "CSV" : isXLS ? "XLS" : "XLSX"})`, ); - let sheets: SheetData[]; - - try { - // 1차 시도: LuckyExcel 직접 처리 (XLSX만) - if (isXLSX) { - console.log("🍀 XLSX 파일 - LuckyExcel 직접 처리 시도"); - sheets = await processWithLuckyExcel(file); - } else { - // CSV, XLS는 SheetJS로 XLSX 변환 후 LuckyExcel 처리 - console.log( - `📊 ${isCSV ? "CSV" : "XLS"} 파일 - SheetJS 변환 후 LuckyExcel 처리`, - ); - - const arrayBuffer = await file.arrayBuffer(); - - // SheetJS로 읽기 (단순한 옵션 사용) - let workbook: any; - - if (isCSV) { - // CSV 처리 - const text = new TextDecoder("utf-8").decode(arrayBuffer); - workbook = XLSX.read(text, { - type: "string", - codepage: 65001, - raw: false, - }); - } else { - // XLS 처리 - workbook = XLSX.read(arrayBuffer, { - type: "array", - codepage: 65001, - raw: false, - }); - } - - // XLSX로 변환 - const xlsxBuffer = XLSX.write(workbook, { - type: "array", - bookType: "xlsx", - compression: true, - }); - - // File 객체로 변환하여 LuckyExcel 처리 - const xlsxFile = new File( - [xlsxBuffer], - file.name.replace(/\.(csv|xls)$/i, ".xlsx"), - { - type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - }, - ); - - sheets = await processWithLuckyExcel(xlsxFile); - } - } catch (luckyExcelError) { - console.warn( - "🔄 LuckyExcel 처리 실패, SheetJS Fallback 시도:", - luckyExcelError, - ); - - // 2차 시도: SheetJS Fallback - sheets = await processWithSheetJSFallback(file); - } + // 통합된 처리 방식: SheetJS → XLSX → LuckyExcel + const sheets = await processFileWithSheetJSToXLSX(file); if (!sheets || sheets.length === 0) { return { @@ -736,7 +889,14 @@ export async function processExcelFile(file: File): Promise { if ( error.message.includes("파일에 워크시트가 없습니다") || error.message.includes("워크북 구조 오류") || - error.message.includes("파일 처리 실패") + error.message.includes("파일 처리 실패") || + error.message.includes("파일 읽기 실패") || + error.message.includes("XLSX 변환 실패") || + error.message.includes("파일이 비어있습니다") || + error.message.includes("워크북을 생성할 수 없습니다") || + error.message.includes("유효한 시트가 없습니다") || + error.message.includes("시트 이름 정보가 없습니다") || + error.message.includes("파일을 읽을 수 없습니다") ) { errorMessage = error.message; } else if (error.message.includes("transformExcelToLucky")) {