럭키엑셀에 파일 인젝션 성공

This commit is contained in:
sheetEasy AI Team
2025-06-20 15:17:36 +09:00
parent 3a8c6af7ea
commit bc5b316f3c
2 changed files with 311 additions and 146 deletions

View File

@@ -0,0 +1,5 @@
---
description:
globs:
alwaysApply: false
---

View File

@@ -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<SheetData[]> {
console.log("🔄 SheetJS Fallback 처리 시작...");
async function processFileWithSheetJSToXLSX(file: File): Promise<SheetData[]> {
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,
// 1단계: SheetJS로 파일 읽기
let workbook: any;
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
dense: false,
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,
});
console.log("📊 SheetJS Fallback 워크북 정보:", {
// 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];
}
}
return convertSheetJSToLuckyExcel(workbook);
}
console.log(`✅ XLSX 변환 완료: ${xlsxArrayBuffer.byteLength} bytes`);
/**
* LuckyExcel 처리 함수 (개선된 버전)
*/
function processWithLuckyExcel(file: File): Promise<SheetData[]> {
return new Promise(async (resolve, reject) => {
console.log("🍀 LuckyExcel 처리 시작...");
// ⏱️ ArrayBuffer 변환 완료 확인 및 검증
console.log("⏱️ ArrayBuffer 변환 검증 중...");
console.log("🔍 LuckyExcel 변환 시작:", {
hasFile: !!file,
fileName: file.name,
fileSize: file.size,
fileType: file.type,
});
// 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}`,
);
}
// 3단계: ArrayBuffer가 완전히 준비된 후 LuckyExcel로 처리
console.log("🍀 LuckyExcel로 변환된 XLSX 처리 중...");
// ArrayBuffer 최종 검증
if (!xlsxArrayBuffer) {
throw new Error("ArrayBuffer가 생성되지 않았습니다");
}
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<SheetData[]>((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<SheetData[]> {
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<SheetData[]> {
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<FileUploadResult> {
try {
@@ -647,68 +860,8 @@ export async function processExcelFile(file: File): Promise<FileUploadResult> {
`📁 파일 처리 시작: ${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<FileUploadResult> {
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")) {