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

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를 통해 처리 * - 모든 파일 형식을 SheetJS를 통해 읽은 후 XLSX로 변환
* - LuckyExcel 우선 시도, 실패 시 SheetJS Fallback 사용 * - 변환된 XLSX 파일을 LuckyExcel로 전달
* - 안정적인 한글 지원 및 에러 처리 * - 안정적인 한글 지원 및 에러 처리
*/ */
@@ -122,10 +122,6 @@ function validateWorkbook(workbook: any): { isValid: boolean; error?: string } {
return { isValid: false, error: "워크북에 시트가 없습니다" }; return { isValid: false, error: "워크북에 시트가 없습니다" };
} }
if (!workbook.Sheets) {
return { isValid: false, error: "워크북에 Sheets 속성이 없습니다" };
}
return { isValid: true }; return { isValid: true };
} }
@@ -421,60 +417,270 @@ function convertSheetJSToLuckyExcel(workbook: any): SheetData[] {
} }
/** /**
* SheetJS를 사용한 Fallback 처리 * SheetJS로 파일을 읽고 XLSX로 변환한 뒤 LuckyExcel로 처리
*/ */
async function processWithSheetJSFallback(file: File): Promise<SheetData[]> { async function processFileWithSheetJSToXLSX(file: File): Promise<SheetData[]> {
console.log("🔄 SheetJS Fallback 처리 시작..."); console.log("📊 SheetJS → XLSX → LuckyExcel 파이프라인 시작...");
const arrayBuffer = await file.arrayBuffer(); 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 옵션 사용 (이전 코드 방식) // 1단계: SheetJS로 파일 읽기
const workbook = XLSX.read(arrayBuffer, { let workbook: any;
type: "array",
cellDates: true, try {
cellNF: false, if (isCSV) {
cellText: false, // 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 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, sheetStubs: true,
WTF: true,
bookSheets: false,
codepage: 65001,
raw: false, 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, sheetNames: workbook.SheetNames,
sheetCount: workbook.SheetNames.length, sheetCount: workbook.SheetNames.length,
}); });
if (workbook.SheetNames.length === 0) { // 2단계: 워크북을 XLSX ArrayBuffer로 변환
throw new Error("파일에 워크시트가 없습니다."); 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`);
}
/** // ⏱️ ArrayBuffer 변환 완료 확인 및 검증
* LuckyExcel 처리 함수 (개선된 버전) console.log("⏱️ ArrayBuffer 변환 검증 중...");
*/
function processWithLuckyExcel(file: File): Promise<SheetData[]> {
return new Promise(async (resolve, reject) => {
console.log("🍀 LuckyExcel 처리 시작...");
console.log("🔍 LuckyExcel 변환 시작:", { // ArrayBuffer 무결성 검증
hasFile: !!file, if (!xlsxArrayBuffer || xlsxArrayBuffer.byteLength === 0) {
fileName: file.name, throw new Error("ArrayBuffer 변환 실패: 빈 버퍼");
fileSize: file.size, }
fileType: file.type,
});
// 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 { try {
// File을 ArrayBuffer로 변환 // LuckyExcel API는 (arrayBuffer, successCallback, errorCallback) 형태로 호출
const arrayBuffer = await file.arrayBuffer(); // 공식 문서: LuckyExcel.transformExcelToLucky(file, successCallback, errorCallback)
(LuckyExcel as any).transformExcelToLucky(
LuckyExcel.transformExcelToLucky( xlsxArrayBuffer,
arrayBuffer, // ArrayBuffer 전달 // 성공 콜백 함수 (두 번째 매개변수)
file.name, // 파일명 전달
(exportJson: any, luckysheetfile: any) => { (exportJson: any, luckysheetfile: any) => {
try { 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 변환 결과:", { console.log("🔍 LuckyExcel 변환 결과:", {
hasExportJson: !!exportJson, hasExportJson: !!exportJson,
hasSheets: !!exportJson?.sheets, hasSheets: !!exportJson?.sheets,
@@ -485,41 +691,16 @@ function processWithLuckyExcel(file: File): Promise<SheetData[]> {
if ( if (
!exportJson || !exportJson ||
!exportJson.sheets || !exportJson.sheets ||
!Array.isArray(exportJson.sheets) !Array.isArray(exportJson.sheets) ||
exportJson.sheets.length === 0
) { ) {
console.warn( console.warn(
"⚠️ LuckyExcel 결과가 유효하지 않습니다. SheetJS Fallback을 시도합니다.", "⚠️ LuckyExcel 결과가 유효하지 않습니다. SheetJS 방식으로 대체 처리합니다.",
); );
// SheetJS Fallback 처리 // LuckyExcel 실패 시 SheetJS 데이터를 직접 변환
processWithSheetJSFallback(file) const fallbackSheets = convertSheetJSToLuckyExcel(workbook);
.then(resolve) resolve(fallbackSheets);
.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}`,
),
);
});
return; return;
} }
@@ -596,24 +777,56 @@ function processWithLuckyExcel(file: File): Promise<SheetData[]> {
console.log("✅ LuckyExcel 처리 성공:", sheets.length, "개 시트"); console.log("✅ LuckyExcel 처리 성공:", sheets.length, "개 시트");
resolve(sheets); resolve(sheets);
} catch (e) { } catch (processError) {
console.error("❌ LuckyExcel 후처리 중 오류:", e); console.error("❌ LuckyExcel 후처리 중 오류:", processError);
reject(e);
// 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) { } catch (luckyError) {
console.error("❌ LuckyExcel 처리 중 오류:", error); console.error("❌ LuckyExcel 호출 중 오류:", luckyError);
reject(error);
// LuckyExcel 호출 실패 시 SheetJS 방식으로 대체
try {
console.log("🔄 SheetJS 방식으로 대체 처리...");
const fallbackSheets = convertSheetJSToLuckyExcel(workbook);
resolve(fallbackSheets);
} catch (fallbackError) {
console.error("❌ SheetJS 대체 처리도 실패:", fallbackError);
reject(fallbackError);
}
} }
}); });
} }
/** /**
* 엑셀 파일을 SheetData 배열로 변환 (개선된 버전) * 엑셀 파일을 SheetData 배열로 변환 (개선된 버전)
* - 우선 LuckyExcel로 처리 시도 (XLSX) * - 모든 파일을 SheetJS로 읽은 후 XLSX로 변환
* - CSV, XLS는 SheetJS로 XLSX 변환 후 LuckyExcel 처리 * - 변환된 XLSX를 LuckyExcel 처리
* - 실패 시 SheetJS Fallback 사용 * - 실패 시 SheetJS 직접 변환으로 Fallback
*/ */
export async function processExcelFile(file: File): Promise<FileUploadResult> { export async function processExcelFile(file: File): Promise<FileUploadResult> {
try { try {
@@ -647,68 +860,8 @@ export async function processExcelFile(file: File): Promise<FileUploadResult> {
`📁 파일 처리 시작: ${file.name} (${isCSV ? "CSV" : isXLS ? "XLS" : "XLSX"})`, `📁 파일 처리 시작: ${file.name} (${isCSV ? "CSV" : isXLS ? "XLS" : "XLSX"})`,
); );
let sheets: SheetData[]; // 통합된 처리 방식: SheetJS → XLSX → LuckyExcel
const sheets = await processFileWithSheetJSToXLSX(file);
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);
}
if (!sheets || sheets.length === 0) { if (!sheets || sheets.length === 0) {
return { return {
@@ -736,7 +889,14 @@ export async function processExcelFile(file: File): Promise<FileUploadResult> {
if ( if (
error.message.includes("파일에 워크시트가 없습니다") || error.message.includes("파일에 워크시트가 없습니다") ||
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; errorMessage = error.message;
} else if (error.message.includes("transformExcelToLucky")) { } else if (error.message.includes("transformExcelToLucky")) {