Files
sheeteasyAI/src/utils/fileProcessor.ts.bak
2025-06-24 14:15:09 +09:00

1291 lines
40 KiB
TypeScript

import * as XLSX from "xlsx-js-style";
import type { SheetData, FileUploadResult } from "../types/sheet";
import { analyzeSheetStyles } from "./styleTest";
/**
* 파일 처리 관련 유틸리티 - xlsx-js-style 공식 API 활용 버전
* - 모든 파일 형식을 SheetJS를 통해 읽은 후 XLSX로 변환
* - 변환된 XLSX 파일을 LuckyExcel로 전달
* - xlsx-js-style의 공식 스타일 구조를 그대로 활용
*/
// 지원되는 파일 타입
export const SUPPORTED_FILE_TYPES = {
XLSX: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
XLS: "application/vnd.ms-excel",
CSV: "text/csv",
} as const;
export const SUPPORTED_EXTENSIONS = [".xlsx", ".xls", ".csv"] as const;
// 최대 파일 크기 (50MB)
export const MAX_FILE_SIZE = 50 * 1024 * 1024;
/**
* xlsx-js-style 색상 객체를 Luckysheet 색상 문자열로 변환
* 공식 xlsx-js-style COLOR_STYLE 형식을 지원: rgb, theme, indexed
*/
function convertXlsxColorToLuckysheet(colorObj: any): string {
if (!colorObj) return "";
// RGB 형태 - 공식 문서: {rgb: "FFCC00"}
if (colorObj.rgb) {
const rgb = colorObj.rgb.toUpperCase();
// ARGB 형태 (8자리) - 앞의 2자리(Alpha) 제거
if (rgb.length === 8) {
const r = parseInt(rgb.substring(2, 4), 16);
const g = parseInt(rgb.substring(4, 6), 16);
const b = parseInt(rgb.substring(6, 8), 16);
return `rgb(${r},${g},${b})`;
}
// RGB 형태 (6자리)
else if (rgb.length === 6) {
const r = parseInt(rgb.substring(0, 2), 16);
const g = parseInt(rgb.substring(2, 4), 16);
const b = parseInt(rgb.substring(4, 6), 16);
return `rgb(${r},${g},${b})`;
}
}
// Theme 색상 - 공식 문서: {theme: 4} 또는 {theme: 1, tint: 0.4}
if (typeof colorObj.theme === "number") {
// Excel 테마 색상 매핑 (공식 문서 예시 기반)
const themeColors: { [key: number]: string } = {
0: "rgb(255,255,255)", // 배경 1 (흰색)
1: "rgb(0,0,0)", // 텍스트 1 (검정)
2: "rgb(238,236,225)", // 배경 2 (연회색)
3: "rgb(31,73,125)", // 텍스트 2 (어두운 파랑)
4: "rgb(79,129,189)", // 강조 1 (파랑) - 공식 문서 예시
5: "rgb(192,80,77)", // 강조 2 (빨강)
6: "rgb(155,187,89)", // 강조 3 (초록)
7: "rgb(128,100,162)", // 강조 4 (보라)
8: "rgb(75,172,198)", // 강조 5 (하늘색)
9: "rgb(247,150,70)", // 강조 6 (주황)
};
let baseColor = themeColors[colorObj.theme] || "rgb(0,0,0)";
// Tint 적용 - 공식 문서: {theme: 1, tint: 0.4} ("Blue, Accent 1, Lighter 40%")
if (typeof colorObj.tint === "number") {
baseColor = applyTintToRgbColor(baseColor, colorObj.tint);
}
return baseColor;
}
// Indexed 색상 (Excel 기본 색상표)
if (typeof colorObj.indexed === "number") {
const indexedColors: { [key: number]: string } = {
0: "rgb(0,0,0)", // 검정
1: "rgb(255,255,255)", // 흰색
2: "rgb(255,0,0)", // 빨강
3: "rgb(0,255,0)", // 초록
4: "rgb(0,0,255)", // 파랑
5: "rgb(255,255,0)", // 노랑
6: "rgb(255,0,255)", // 마젠타
7: "rgb(0,255,255)", // 시안
8: "rgb(128,0,0)", // 어두운 빨강
9: "rgb(0,128,0)", // 어두운 초록
10: "rgb(0,0,128)", // 어두운 파랑
17: "rgb(128,128,128)", // 회색
};
return indexedColors[colorObj.indexed] || "rgb(0,0,0)";
}
return "";
}
/**
* RGB 색상에 Excel tint 적용
*/
function applyTintToRgbColor(rgbColor: string, tint: number): string {
const match = rgbColor.match(/rgb\((\d+),(\d+),(\d+)\)/);
if (!match) return rgbColor;
const r = parseInt(match[1]);
const g = parseInt(match[2]);
const b = parseInt(match[3]);
// Excel tint 공식 적용
const applyTint = (color: number, tint: number): number => {
if (tint < 0) {
return Math.round(color * (1 + tint));
} else {
return Math.round(color * (1 - tint) + (255 - 255 * (1 - tint)));
}
};
const newR = Math.max(0, Math.min(255, applyTint(r, tint)));
const newG = Math.max(0, Math.min(255, applyTint(g, tint)));
const newB = Math.max(0, Math.min(255, applyTint(b, tint)));
return `rgb(${newR},${newG},${newB})`;
}
/**
* xlsx-js-style 테두리 스타일을 Luckysheet 번호로 변환
* 공식 문서의 BORDER_STYLE 값들을 지원
*/
function convertBorderStyleToLuckysheet(borderStyle: string): number {
const styleMap: { [key: string]: number } = {
thin: 1,
medium: 2,
thick: 3,
dotted: 4,
dashed: 5,
dashDot: 6,
dashDotDot: 7,
double: 8,
hair: 1,
mediumDashed: 5,
mediumDashDot: 6,
mediumDashDotDot: 7,
slantDashDot: 6,
};
return styleMap[borderStyle] || 1;
}
/**
* xlsx-js-style 스타일 객체를 Luckysheet 스타일로 변환
* 공식 xlsx-js-style API 구조를 완전히 활용
*/
function convertXlsxStyleToLuckysheet(xlsxStyle: any): any {
if (!xlsxStyle) return {};
const luckyStyle: any = {};
// 🎨 폰트 스타일 변환 - 공식 문서 font 속성
if (xlsxStyle.font) {
const font = xlsxStyle.font;
// 폰트명 - 공식 문서: {name: "Courier"}
if (font.name) {
luckyStyle.ff = font.name;
}
// 폰트 크기 - 공식 문서: {sz: 24}
if (font.sz) {
luckyStyle.fs = font.sz;
}
// 굵게 - 공식 문서: {bold: true}
if (font.bold) {
luckyStyle.bl = 1;
}
// 기울임 - 공식 문서: {italic: true}
if (font.italic) {
luckyStyle.it = 1;
}
// 밑줄 - 공식 문서: {underline: true}
if (font.underline) {
luckyStyle.un = 1;
}
// 취소선 - 공식 문서: {strike: true}
if (font.strike) {
luckyStyle.st = 1;
}
// 폰트 색상 - 공식 문서: {color: {rgb: "FF0000"}}
if (font.color) {
const fontColor = convertXlsxColorToLuckysheet(font.color);
if (fontColor) {
luckyStyle.fc = fontColor;
}
}
}
// 🎨 배경 스타일 변환 - 공식 문서 fill 속성
if (xlsxStyle.fill) {
const fill = xlsxStyle.fill;
// 배경색 - 공식 문서: {fgColor: {rgb: "E9E9E9"}}
if (fill.fgColor) {
const bgColor = convertXlsxColorToLuckysheet(fill.fgColor);
if (bgColor) {
luckyStyle.bg = bgColor;
}
}
// bgColor도 확인 (패턴 배경의 경우)
else if (fill.bgColor) {
const bgColor = convertXlsxColorToLuckysheet(fill.bgColor);
if (bgColor) {
luckyStyle.bg = bgColor;
}
}
}
// 🎨 정렬 스타일 변환 - 공식 문서 alignment 속성
if (xlsxStyle.alignment) {
const alignment = xlsxStyle.alignment;
// 수평 정렬 - 공식 문서: {horizontal: "center"}
if (alignment.horizontal) {
luckyStyle.ht =
alignment.horizontal === "left"
? 1
: alignment.horizontal === "center"
? 2
: alignment.horizontal === "right"
? 3
: 1;
}
// 수직 정렬 - 공식 문서: {vertical: "center"}
if (alignment.vertical) {
luckyStyle.vt =
alignment.vertical === "top"
? 1
: alignment.vertical === "center"
? 2
: alignment.vertical === "bottom"
? 3
: 2;
}
// 텍스트 줄바꿈 - 공식 문서: {wrapText: true}
if (alignment.wrapText) {
luckyStyle.tb = 1;
}
// 텍스트 회전 - 공식 문서: {textRotation: 90}
if (alignment.textRotation) {
luckyStyle.tr = alignment.textRotation;
}
}
// 🎨 테두리 스타일 변환 - 공식 문서 border 속성
if (xlsxStyle.border) {
const border = xlsxStyle.border;
luckyStyle.bd = {};
// 상단 테두리 - 공식 문서: {top: {style: "thin", color: {rgb: "000000"}}}
if (border.top) {
luckyStyle.bd.t = {
style: convertBorderStyleToLuckysheet(border.top.style || "thin"),
color: convertXlsxColorToLuckysheet(border.top.color) || "rgb(0,0,0)",
};
}
// 하단 테두리
if (border.bottom) {
luckyStyle.bd.b = {
style: convertBorderStyleToLuckysheet(border.bottom.style || "thin"),
color:
convertXlsxColorToLuckysheet(border.bottom.color) || "rgb(0,0,0)",
};
}
// 좌측 테두리
if (border.left) {
luckyStyle.bd.l = {
style: convertBorderStyleToLuckysheet(border.left.style || "thin"),
color: convertXlsxColorToLuckysheet(border.left.color) || "rgb(0,0,0)",
};
}
// 우측 테두리
if (border.right) {
luckyStyle.bd.r = {
style: convertBorderStyleToLuckysheet(border.right.style || "thin"),
color: convertXlsxColorToLuckysheet(border.right.color) || "rgb(0,0,0)",
};
}
}
// 🎨 숫자 포맷 변환 - 공식 문서 numFmt 속성
if (xlsxStyle.numFmt) {
// numFmt는 문자열 또는 숫자일 수 있음
luckyStyle.ct = {
fa: xlsxStyle.numFmt,
t: "n", // 숫자 타입
};
}
return luckyStyle;
}
/**
* 파일 타입 검증
*/
export function validateFileType(file: File): boolean {
const fileName = file.name.toLowerCase();
const extension = fileName.split(".").pop();
const supportedExtensions = SUPPORTED_EXTENSIONS.map((ext) => ext.slice(1));
if (!extension) {
return false;
}
return supportedExtensions.includes(extension);
}
/**
* 파일 크기 검증
*/
export function validateFileSize(file: File): boolean {
return file.size <= MAX_FILE_SIZE;
}
/**
* 파일 이름에서 확장자 제거
*/
export function getFileNameWithoutExtension(fileName: string): string {
return fileName.replace(/\.[^/.]+$/, "");
}
/**
* 에러 메시지 생성
*/
export function getFileErrorMessage(file: File): string {
if (!validateFileType(file)) {
const fileName = file.name.toLowerCase();
const extension = fileName.split(".").pop();
if (!extension || extension === fileName) {
return `파일 확장자가 없습니다. ${SUPPORTED_EXTENSIONS.join(", ")} 파일만 업로드 가능합니다.`;
}
return `지원되지 않는 파일 형식입니다. "${extension}" 대신 ${SUPPORTED_EXTENSIONS.join(", ")} 파일을 업로드해주세요.`;
}
if (!validateFileSize(file)) {
const maxSizeMB = Math.round(MAX_FILE_SIZE / (1024 * 1024));
const currentSizeMB = (file.size / (1024 * 1024)).toFixed(2);
return `파일 크기가 너무 큽니다. 현재 크기: ${currentSizeMB}MB, 최대 허용: ${maxSizeMB}MB`;
}
if (file.name.length > 255) {
return "파일명이 너무 깁니다. 255자 이하의 파일명을 사용해주세요.";
}
const invalidChars = /[<>:"/\\|?*]/;
if (invalidChars.test(file.name)) {
return '파일명에 사용할 수 없는 특수문자가 포함되어 있습니다. (< > : " / \\ | ? *)';
}
return "";
}
/**
* 한글 시트명을 안전하게 처리하는 함수
*/
function sanitizeSheetName(sheetName: string): string {
if (!sheetName || typeof sheetName !== "string") {
return "Sheet1";
}
const maxLength = 31;
let sanitized = sheetName.trim();
if (sanitized.length > maxLength) {
sanitized = sanitized.substring(0, maxLength - 3) + "...";
}
sanitized = sanitized.replace(/[\\\/\*\?\[\]]/g, "_");
return sanitized || "Sheet";
}
/**
* 워크북 구조 검증 함수
*/
function validateWorkbook(workbook: any): { isValid: boolean; error?: string } {
if (!workbook) {
return { isValid: false, error: "워크북이 null 또는 undefined입니다" };
}
if (!workbook.SheetNames) {
return { isValid: false, error: "워크북에 SheetNames 속성이 없습니다" };
}
if (!Array.isArray(workbook.SheetNames)) {
return { isValid: false, error: "SheetNames가 배열이 아닙니다" };
}
if (workbook.SheetNames.length === 0) {
return { isValid: false, error: "워크북에 시트가 없습니다" };
}
return { isValid: true };
}
/**
* SheetJS 데이터를 LuckyExcel 형식으로 변환
*/
function convertSheetJSToLuckyExcel(workbook: any): SheetData[] {
console.log("🔄 SheetJS → LuckyExcel 형식 변환 시작...");
const luckySheets: SheetData[] = [];
// 워크북 구조 검증
const validation = validateWorkbook(workbook);
if (!validation.isValid) {
console.error("❌ 워크북 검증 실패:", validation.error);
throw new Error(`워크북 구조 오류: ${validation.error}`);
}
console.log(`📋 발견된 시트: ${workbook.SheetNames.join(", ")}`);
workbook.SheetNames.forEach((sheetName: string, index: number) => {
console.log(
`📋 시트 ${index + 1}/${workbook.SheetNames.length} "${sheetName}" 변환 중...`,
);
const safeSheetName = sanitizeSheetName(sheetName);
const worksheet = workbook.Sheets[sheetName];
if (!worksheet) {
console.warn(
`⚠️ 시트 "${sheetName}"를 찾을 수 없습니다. 빈 시트로 생성합니다.`,
);
luckySheets.push({
id: `sheet_${index}`,
name: safeSheetName,
data: [[""]],
config: {
container: `luckysheet_${index}`,
title: safeSheetName,
lang: "ko",
data: [
{
name: safeSheetName,
index: index.toString(),
celldata: [],
status: 1,
order: index,
row: 100,
column: 26,
},
],
options: {
showtoolbar: true,
showinfobar: true,
showsheetbar: true,
showstatisticBar: true,
allowCopy: true,
allowEdit: true,
enableAddRow: true,
enableAddCol: true,
},
},
});
return;
}
try {
// 시트 범위 확인
const range = worksheet["!ref"];
if (!range) {
console.warn(
`⚠️ 시트 "${sheetName}"에 데이터 범위가 없습니다. 빈 시트로 처리합니다.`,
);
luckySheets.push({
id: `sheet_${index}`,
name: safeSheetName,
data: [[""]],
config: {
container: `luckysheet_${index}`,
title: safeSheetName,
lang: "ko",
data: [
{
name: safeSheetName,
index: index.toString(),
celldata: [],
status: 1,
order: index,
row: 100,
column: 26,
},
],
options: {
showtoolbar: true,
showinfobar: true,
showsheetbar: true,
showstatisticBar: true,
allowCopy: true,
allowEdit: true,
enableAddRow: true,
enableAddCol: true,
},
},
});
return;
}
// 범위 파싱
const rangeObj = XLSX.utils.decode_range(range);
const maxRow = rangeObj.e.r + 1;
const maxCol = rangeObj.e.c + 1;
console.log(`📐 시트 "${sheetName}" 크기: ${maxRow}행 x ${maxCol}`);
// 2D 배열로 데이터 변환
const data: any[][] = [];
const cellData: any[] = [];
// 데이터 배열 초기화
for (let row = 0; row < maxRow; row++) {
data[row] = new Array(maxCol).fill("");
}
// 셀 데이터 변환
for (let row = rangeObj.s.r; row <= rangeObj.e.r; row++) {
for (let col = rangeObj.s.c; col <= rangeObj.e.c; col++) {
const cellAddress = XLSX.utils.encode_cell({ r: row, c: col });
const cell = worksheet[cellAddress];
if (
cell &&
cell.v !== undefined &&
cell.v !== null &&
cell.v !== ""
) {
let cellValue = cell.v;
if (typeof cellValue === "string") {
cellValue = cellValue.trim();
if (cellValue.length > 1000) {
cellValue = cellValue.substring(0, 997) + "...";
}
}
// 2D 배열에 데이터 저장
data[row][col] = cellValue;
// LuckyExcel celldata 형식으로 변환
const luckyCell: any = {
r: row,
c: col,
v: {
v: cellValue,
m: cell.w || String(cellValue), // 포맷팅된 텍스트 우선 사용
ct: { fa: "General", t: "g" },
},
};
// 🎨 xlsx-js-style 스타일 정보 처리
if (cell.s) {
console.log(
`🎨 셀 ${cellAddress}에 스타일 정보 발견:`,
JSON.stringify(cell.s, null, 2),
);
const convertedStyle = convertXlsxStyleToLuckysheet(cell.s);
console.log(
`🎨 변환된 Luckysheet 스타일:`,
JSON.stringify(convertedStyle, null, 2),
);
luckyCell.v.s = convertedStyle;
}
// 셀 타입에 따른 추가 처리
if (cell.t === "s") {
luckyCell.v.ct.t = "s";
} else if (cell.t === "n") {
luckyCell.v.ct.t = "n";
// 숫자 포맷 처리
if (cell.z) {
luckyCell.v.ct.fa = cell.z;
}
} else if (cell.t === "d") {
luckyCell.v.ct.t = "d";
// 날짜 포맷 처리
if (cell.z) {
luckyCell.v.ct.fa = cell.z;
}
} else if (cell.t === "b") {
luckyCell.v.ct.t = "b";
}
// 수식 처리
if (cell.f) {
luckyCell.v.f = cell.f;
}
cellData.push(luckyCell);
}
}
}
// 🔗 병합 셀 정보 처리
const mergeData: any[] = [];
if (worksheet["!merges"]) {
worksheet["!merges"].forEach((merge: any) => {
mergeData.push({
r: merge.s.r, // 시작 행
c: merge.s.c, // 시작 열
rs: merge.e.r - merge.s.r + 1, // 행 병합 수
cs: merge.e.c - merge.s.c + 1, // 열 병합 수
});
});
console.log(
`🔗 시트 "${sheetName}"에서 ${mergeData.length}개 병합 셀 발견`,
);
}
// 📏 열 너비 정보 처리
const colhidden: { [key: number]: number } = {};
if (worksheet["!cols"]) {
worksheet["!cols"].forEach((col: any, colIndex: number) => {
if (col && col.hidden) {
colhidden[colIndex] = 0; // 숨겨진 열
} else if (col && col.wpx) {
// 픽셀 단위 너비가 있으면 기록 (Luckysheet에서 활용 가능)
console.log(`📏 열 ${colIndex}: ${col.wpx}px`);
}
});
}
// 📐 행 높이 정보 처리
const rowhidden: { [key: number]: number } = {};
if (worksheet["!rows"]) {
worksheet["!rows"].forEach((row: any, rowIndex: number) => {
if (row && row.hidden) {
rowhidden[rowIndex] = 0; // 숨겨진 행
} else if (row && row.hpx) {
// 픽셀 단위 높이가 있으면 기록
console.log(`📐 행 ${rowIndex}: ${row.hpx}px`);
}
});
}
// SheetData 객체 생성
const sheetData: SheetData = {
id: `sheet_${index}`,
name: safeSheetName,
data: data,
config: {
container: `luckysheet_${index}`,
title: safeSheetName,
lang: "ko",
data: [
{
name: safeSheetName,
index: index.toString(),
celldata: cellData,
status: 1,
order: index,
row: maxRow,
column: maxCol,
// 🎨 xlsx-js-style로부터 추가된 스타일 정보들
...(mergeData.length > 0 && { merge: mergeData }), // 병합 셀
...(Object.keys(colhidden).length > 0 && { colhidden }), // 숨겨진 열
...(Object.keys(rowhidden).length > 0 && { rowhidden }), // 숨겨진 행
},
],
options: {
showtoolbar: true,
showinfobar: true,
showsheetbar: true,
showstatisticBar: true,
allowCopy: true,
allowEdit: true,
enableAddRow: true,
enableAddCol: true,
},
},
};
luckySheets.push(sheetData);
console.log(`✅ 시트 "${sheetName}" 변환 완료: ${cellData.length}개 셀`);
} catch (sheetError) {
console.error(`❌ 시트 "${sheetName}" 변환 중 오류:`, sheetError);
// 오류 발생 시 빈 시트로 생성
luckySheets.push({
id: `sheet_${index}`,
name: safeSheetName,
data: [[""]],
config: {
container: `luckysheet_${index}`,
title: safeSheetName,
lang: "ko",
data: [
{
name: safeSheetName,
index: index.toString(),
celldata: [],
status: 1,
order: index,
row: 100,
column: 26,
},
],
options: {
showtoolbar: true,
showinfobar: true,
showsheetbar: true,
showstatisticBar: true,
allowCopy: true,
allowEdit: true,
enableAddRow: true,
enableAddCol: true,
},
},
});
}
});
// 최소 1개 시트는 보장
if (luckySheets.length === 0) {
console.log("📄 시트가 없어서 기본 시트를 생성합니다.");
luckySheets.push({
id: "sheet_0",
name: "Sheet1",
data: [[""]],
config: {
container: "luckysheet_0",
title: "Sheet1",
lang: "ko",
data: [
{
name: "Sheet1",
index: "0",
celldata: [],
status: 1,
order: 0,
row: 100,
column: 26,
},
],
options: {
showtoolbar: true,
showinfobar: false,
showsheetbar: true,
showstatisticBar: false,
allowCopy: true,
allowEdit: true,
enableAddRow: true,
enableAddCol: true,
},
},
});
}
console.log(
`🎉 SheetJS → LuckyExcel 변환 완료: ${luckySheets.length}개 시트`,
);
return luckySheets;
}
/**
* SheetJS로 파일을 읽고 XLSX로 변환한 뒤 LuckyExcel로 처리
*/
async function processFileWithSheetJSToXLSX(
file: File,
): Promise<{ sheets: SheetData[]; xlsxBuffer: ArrayBuffer }> {
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");
// 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
raw: false,
});
} else {
// XLS/XLSX 파일 처리 - 스타일 정보 강제 추출 옵션
console.log(`📊 ${isXLS ? "XLS" : "XLSX"} 파일을 SheetJS로 읽는 중...`);
workbook = XLSX.read(arrayBuffer, {
cellStyles: true, // 스타일 정보 보존
cellNF: true, // 숫자 형식 보존 (스타일의 일부)
bookProps: true, // 문서 속성 보존 (스타일 정보 포함 가능)
WTF: true, // 더 관대한 파싱
});
}
} 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("워크북을 생성할 수 없습니다.");
}
// 기본 검증만 수행
if (!workbook.SheetNames || workbook.SheetNames.length === 0) {
throw new Error("시트 이름 정보가 없습니다.");
}
// Sheets 객체가 없으면 빈 객체로 초기화
if (!workbook.Sheets) {
workbook.Sheets = {};
}
console.log("✅ SheetJS 워크북 읽기 성공:", {
sheetNames: workbook.SheetNames,
sheetCount: workbook.SheetNames.length,
});
// 🎨 스타일 정보 상세 분석 (개발 모드에서만)
if (import.meta.env.DEV) {
analyzeSheetStyles(workbook);
}
// 2단계: 워크북을 XLSX ArrayBuffer로 변환
let xlsxArrayBuffer: ArrayBuffer;
try {
console.log("🔄 XLSX 형식으로 변환 중...");
const xlsxData = XLSX.write(workbook, {
type: "array",
bookType: "xlsx",
cellStyles: true, // 스타일 정보 보존
});
console.log(`✅ XLSX 변환 완료: ${xlsxData.length} bytes`);
// 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`);
} 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");
console.log("🍀 LuckyExcel 처리 시작...");
// Promise를 사용한 LuckyExcel 처리
return new Promise<{ sheets: SheetData[]; xlsxBuffer: ArrayBuffer }>(
(resolve, reject) => {
try {
// LuckyExcel API는 (arrayBuffer, successCallback, errorCallback) 형태로 호출
// 공식 문서: LuckyExcel.transformExcelToLucky(file, successCallback, errorCallback)
(window.LuckyExcel as any).transformExcelToLucky(
xlsxArrayBuffer,
// 성공 콜백 함수 (두 번째 매개변수)
(exportJson: any, _luckysheetfile: any) => {
try {
console.log(
"🍀 LuckyExcel 변환 성공:",
exportJson?.sheets?.length || 0,
"개 시트",
);
// 데이터 유효성 검사
if (
!exportJson ||
!exportJson.sheets ||
!Array.isArray(exportJson.sheets) ||
exportJson.sheets.length === 0
) {
console.warn(
"⚠️ LuckyExcel 결과가 유효하지 않습니다. SheetJS 방식으로 대체 처리합니다.",
);
// LuckyExcel 실패 시 SheetJS 데이터를 직접 변환
const fallbackSheets = convertSheetJSToLuckyExcel(workbook);
resolve({
sheets: fallbackSheets,
xlsxBuffer: xlsxArrayBuffer,
});
return;
}
// LuckyExcel 변환이 성공한 경우 - SheetData 형식으로 변환
const sheets: SheetData[] = exportJson.sheets.map(
(luckySheet: any, index: number) => {
const sheetName = luckySheet.name || `Sheet${index + 1}`;
const maxRow = luckySheet.row || 0;
const maxCol = luckySheet.column || 0;
// 2D 배열 초기화
const data: any[][] = [];
for (let r = 0; r < maxRow; r++) {
data[r] = new Array(maxCol).fill("");
}
// celldata에서 데이터 추출
if (
luckySheet.celldata &&
Array.isArray(luckySheet.celldata)
) {
luckySheet.celldata.forEach((cell: any) => {
if (
cell &&
typeof cell.r === "number" &&
typeof cell.c === "number"
) {
const row = cell.r;
const col = cell.c;
if (row < maxRow && col < maxCol && cell.v) {
const cellValue = cell.v.v || cell.v.m || "";
data[row][col] = String(cellValue).trim();
}
}
});
}
// 빈 데이터 처리
if (data.length === 0) {
data.push([""]);
}
return {
id: `sheet_${index}`,
name: sheetName,
data: data,
xlsxBuffer: xlsxArrayBuffer, // 변환된 XLSX ArrayBuffer 포함
config: {
container: `luckysheet_${index}`,
title: sheetName,
lang: "ko",
data: [
{
name: sheetName,
index: index.toString(),
celldata: luckySheet.celldata || [],
status: 1,
order: index,
row: maxRow,
column: maxCol,
},
],
options: {
showtoolbar: true,
showinfobar: false,
showsheetbar: true,
showstatisticBar: true,
allowCopy: true,
allowEdit: true,
enableAddRow: true,
enableAddCol: true,
},
},
};
},
);
console.log("✅ LuckyExcel 처리 성공:", sheets.length, "개 시트");
resolve({ sheets, xlsxBuffer: xlsxArrayBuffer });
} catch (processError) {
console.error("❌ LuckyExcel 후처리 중 오류:", processError);
// LuckyExcel 후처리 실패 시 SheetJS 방식으로 대체
try {
console.log("🔄 SheetJS 방식으로 대체 처리...");
const fallbackSheets = convertSheetJSToLuckyExcel(workbook);
resolve({
sheets: fallbackSheets,
xlsxBuffer: xlsxArrayBuffer,
});
} 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({ sheets: fallbackSheets, xlsxBuffer: xlsxArrayBuffer });
} catch (fallbackError) {
console.error("❌ SheetJS 대체 처리도 실패:", fallbackError);
reject(fallbackError);
}
},
);
} catch (luckyError) {
console.error("❌ LuckyExcel 호출 중 오류:", luckyError);
// LuckyExcel 호출 실패 시 SheetJS 방식으로 대체
try {
console.log("🔄 SheetJS 방식으로 대체 처리...");
const fallbackSheets = convertSheetJSToLuckyExcel(workbook);
resolve({ sheets: fallbackSheets, xlsxBuffer: xlsxArrayBuffer });
} catch (fallbackError) {
console.error("❌ SheetJS 대체 처리도 실패:", fallbackError);
reject(fallbackError);
}
}
},
);
}
/**
* XLSX 파일을 바로 LuckyExcel로 처리 (공식 예제 순서 준수)
* - 공식 문서 예제를 그대로 따름: LuckyExcel.transformExcelToLucky → luckysheet.create
*/
async function processXLSXWithLuckyExcel(
file: File,
): Promise<{ sheets: SheetData[]; xlsxBuffer: ArrayBuffer }> {
console.log("🍀 XLSX 파일을 LuckyExcel로 직접 처리 시작...");
console.log(`📊 XLSX 파일: ${file.name}, 크기: ${file.size} bytes`);
// Promise를 사용한 LuckyExcel 처리 (공식 예제 순서)
return new Promise<{ sheets: SheetData[]; xlsxBuffer: ArrayBuffer }>(
(resolve, reject) => {
// Make sure to get the xlsx file first, and then use the global method window.LuckyExcel to convert
(window.LuckyExcel as any).transformExcelToLucky(
file,
// After obtaining the converted table data, use luckysheet to initialize or update the existing luckysheet workbook
function (exportJson: any, _luckysheetfile: any) {
console.log(
"🍀 LuckyExcel 변환 성공:",
exportJson?.sheets?.length || 0,
"개 시트",
);
// ArrayBuffer는 성공 시에만 생성
file
.arrayBuffer()
.then((arrayBuffer) => {
// exportJson.sheets를 SheetData 형식으로 단순 변환
const sheets: SheetData[] = exportJson.sheets.map(
(sheet: any, index: number) => ({
id: `sheet_${index}`,
name: sheet.name || `Sheet${index + 1}`,
data: [[""]], // 실제 데이터는 luckysheet에서 처리
xlsxBuffer: arrayBuffer,
config: {
container: `luckysheet_${index}`,
title: exportJson.info?.name || file.name,
lang: "ko",
// Note: Luckysheet needs to introduce a dependency package and initialize the table container before it can be used
data: exportJson.sheets, // exportJson.sheets를 그대로 전달
// 공식 예제에 따른 설정
...(exportJson.info?.name && {
title: exportJson.info.name,
}),
...(exportJson.info?.creator && {
userInfo: exportJson.info.creator,
}),
options: {
showtoolbar: true,
showinfobar: false,
showsheetbar: true,
showstatisticBar: true,
allowCopy: true,
allowEdit: true,
enableAddRow: true,
enableAddCol: true,
},
},
}),
);
console.log(
"✅ XLSX 파일 LuckyExcel 처리 완료:",
sheets.length,
"개 시트",
);
resolve({ sheets, xlsxBuffer: arrayBuffer });
})
.catch((bufferError) => {
reject(new Error(`ArrayBuffer 생성 실패: ${bufferError}`));
});
},
// Import failed. Is your file a valid xlsx?
function (err: any) {
console.error("❌ LuckyExcel 변환 실패:", err);
reject(new Error(`XLSX 파일 변환 실패: ${err}`));
},
);
},
);
}
/**
* 엑셀 파일을 SheetData 배열로 변환 (파일 형식별 최적화 버전)
* - XLSX: LuckyExcel 직접 처리 (스타일 정보 완전 보존)
* - CSV/XLS: SheetJS → XLSX → LuckyExcel 파이프라인
*/
export async function processExcelFile(file: File): Promise<FileUploadResult> {
try {
const errorMessage = getFileErrorMessage(file);
if (errorMessage) {
return {
success: false,
error: errorMessage,
fileName: file.name,
fileSize: file.size,
file: file,
};
}
// 파일 형식 감지
const fileName = file.name.toLowerCase();
const isCSV = fileName.endsWith(".csv");
const isXLS = fileName.endsWith(".xls");
const isXLSX = fileName.endsWith(".xlsx");
if (!isCSV && !isXLS && !isXLSX) {
return {
success: false,
error:
"지원되지 않는 파일 형식입니다. .csv, .xls, .xlsx 파일을 사용해주세요.",
fileName: file.name,
fileSize: file.size,
file: file,
};
}
console.log(
`📁 파일 처리 시작: ${file.name} (${isCSV ? "CSV" : isXLS ? "XLS" : "XLSX"})`,
);
let sheets: SheetData[];
let xlsxBuffer: ArrayBuffer;
if (isXLSX) {
// 🍀 XLSX 파일: LuckyExcel 직접 처리 (스타일 정보 완전 보존)
console.log("🍀 XLSX 파일 - LuckyExcel 직접 처리 방식 사용");
const result = await processXLSXWithLuckyExcel(file);
sheets = result.sheets;
xlsxBuffer = result.xlsxBuffer;
} else {
// 📊 CSV/XLS 파일: SheetJS → XLSX → LuckyExcel 파이프라인
console.log(`📊 ${isCSV ? "CSV" : "XLS"} 파일 - SheetJS 파이프라인 사용`);
const result = await processFileWithSheetJSToXLSX(file);
sheets = result.sheets;
xlsxBuffer = result.xlsxBuffer;
}
if (!sheets || sheets.length === 0) {
return {
success: false,
error: "파일에서 유효한 시트를 찾을 수 없습니다.",
fileName: file.name,
fileSize: file.size,
file: file,
};
}
console.log(`🎉 파일 처리 완료: ${sheets.length}개 시트`);
return {
success: true,
data: sheets,
fileName: file.name,
fileSize: file.size,
file: file,
xlsxBuffer,
};
} catch (error) {
console.error("❌ 파일 처리 중 오류 발생:", error);
let errorMessage = "파일을 읽는 중 오류가 발생했습니다.";
if (error instanceof Error) {
if (
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("파일을 읽을 수 없습니다") ||
error.message.includes("XLSX 파일 처리 실패") ||
error.message.includes("XLSX 파일 변환 실패")
) {
errorMessage = error.message;
} else if (error.message.includes("transformExcelToLucky")) {
errorMessage =
"Excel 파일 변환에 실패했습니다. 파일이 손상되었거나 지원되지 않는 형식일 수 있습니다.";
} else {
errorMessage = `파일 처리 중 오류: ${error.message}`;
}
}
return {
success: false,
error: errorMessage,
fileName: file.name,
fileSize: file.size,
file: file,
};
}
}
/**
* 여러 파일 중 유효한 파일만 필터링
*/
export function filterValidFiles(files: FileList): File[] {
return Array.from(files).filter((file) => {
const errorMessage = getFileErrorMessage(file);
return errorMessage === "";
});
}
/**
* 파일 목록의 에러 정보 수집
*/
export function getFileErrors(
files: FileList,
): { file: File; error: string }[] {
const errors: { file: File; error: string }[] = [];
Array.from(files).forEach((file) => {
const errorMessage = getFileErrorMessage(file);
if (errorMessage !== "") {
errors.push({ file, error: errorMessage });
}
});
return errors;
}