유니버CE 초기화 테스트 완료
This commit is contained in:
5
.cursor/rules/xlsx-js-style.mdc
Normal file
5
.cursor/rules/xlsx-js-style.mdc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
5175
package-lock.json
generated
5175
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -19,6 +19,22 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
|
"@univerjs/core": "^0.8.2",
|
||||||
|
"@univerjs/design": "^0.8.2",
|
||||||
|
"@univerjs/docs": "^0.8.2",
|
||||||
|
"@univerjs/docs-ui": "^0.8.2",
|
||||||
|
"@univerjs/engine-formula": "^0.8.2",
|
||||||
|
"@univerjs/engine-numfmt": "^0.8.2",
|
||||||
|
"@univerjs/engine-render": "^0.8.2",
|
||||||
|
"@univerjs/facade": "^0.5.5",
|
||||||
|
"@univerjs/sheets": "^0.8.2",
|
||||||
|
"@univerjs/sheets-formula": "^0.8.2",
|
||||||
|
"@univerjs/sheets-formula-ui": "^0.8.2",
|
||||||
|
"@univerjs/sheets-numfmt": "^0.8.2",
|
||||||
|
"@univerjs/sheets-numfmt-ui": "^0.8.2",
|
||||||
|
"@univerjs/sheets-ui": "^0.8.2",
|
||||||
|
"@univerjs/ui": "^0.8.2",
|
||||||
|
"@univerjs/uniscript": "^0.8.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
@@ -27,9 +43,7 @@
|
|||||||
"luckysheet": "^2.1.13",
|
"luckysheet": "^2.1.13",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"sheetjs-style": "^0.15.8",
|
|
||||||
"tailwind-merge": "^2.5.4",
|
"tailwind-merge": "^2.5.4",
|
||||||
"xlsx": "^0.18.5",
|
|
||||||
"zustand": "^5.0.2"
|
"zustand": "^5.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -46,6 +60,7 @@
|
|||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.15.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
@@ -54,6 +69,7 @@
|
|||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.1",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
|
"stream-browserify": "^3.0.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^6.0.1",
|
"vite": "^6.0.1",
|
||||||
|
|||||||
56
src/App.tsx
56
src/App.tsx
@@ -1,14 +1,9 @@
|
|||||||
import { useAppStore } from "./stores/useAppStore";
|
import { useState } from "react";
|
||||||
import { Card, CardContent } from "./components/ui/card";
|
|
||||||
import { Button } from "./components/ui/button";
|
import { Button } from "./components/ui/button";
|
||||||
import { FileUpload } from "./components/sheet/FileUpload";
|
import TestSheetViewer from "./components/sheet/TestSheetViewer";
|
||||||
import { SheetViewer } from "./components/sheet/SheetViewer";
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { currentFile, sheets, resetApp } = useAppStore();
|
const [showTestViewer, setShowTestViewer] = useState(false);
|
||||||
|
|
||||||
// 파일이 업로드되어 시트 데이터가 있는 경우와 없는 경우 구분
|
|
||||||
const hasSheetData = currentFile && sheets && sheets.length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
@@ -20,24 +15,19 @@ function App() {
|
|||||||
<h1 className="text-2xl font-bold text-blue-600">sheetEasy AI</h1>
|
<h1 className="text-2xl font-bold text-blue-600">sheetEasy AI</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
{hasSheetData && (
|
{/* 테스트 뷰어 토글 버튼 */}
|
||||||
<>
|
|
||||||
<span className="text-sm text-gray-600">
|
|
||||||
{currentFile.name}
|
|
||||||
</span>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant={showTestViewer ? "default" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={resetApp}
|
onClick={() => setShowTestViewer(!showTestViewer)}
|
||||||
className="text-gray-600 hover:text-gray-800"
|
className="bg-green-500 hover:bg-green-600 text-white border-green-500"
|
||||||
>
|
>
|
||||||
새 파일
|
🧪 테스트 뷰어
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
|
||||||
)}
|
{!showTestViewer && (
|
||||||
{!hasSheetData && (
|
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">
|
||||||
Excel 파일 AI 처리 도구
|
Univer CE 테스트 모드
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -47,15 +37,29 @@ function App() {
|
|||||||
|
|
||||||
{/* 메인 콘텐츠 */}
|
{/* 메인 콘텐츠 */}
|
||||||
<main className="h-[calc(100vh-4rem)]">
|
<main className="h-[calc(100vh-4rem)]">
|
||||||
{hasSheetData ? (
|
{showTestViewer ? (
|
||||||
// 파일이 업로드된 경우: SheetViewer 표시 (전체화면)
|
// 테스트 뷰어 표시
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<SheetViewer className="h-full" />
|
<TestSheetViewer />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// 파일이 업로드되지 않은 경우: FileUpload 표시 (중앙 정렬)
|
// 메인 페이지
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<FileUpload />
|
<div className="text-center py-12">
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 mb-4">
|
||||||
|
🧪 Univer CE 테스트 모드
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 mb-8">
|
||||||
|
현재 Univer CE 전용 테스트 뷰어를 사용해보세요
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowTestViewer(true)}
|
||||||
|
size="lg"
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white px-8 py-3"
|
||||||
|
>
|
||||||
|
테스트 뷰어 시작하기 →
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,504 +0,0 @@
|
|||||||
import React, {
|
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
|
||||||
useRef,
|
|
||||||
useCallback,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { useAppStore } from "../../stores/useAppStore";
|
|
||||||
import type { SheetData } from "../../types/sheet";
|
|
||||||
|
|
||||||
// Window 타입 확장
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
luckysheet: any;
|
|
||||||
LuckyExcel: any;
|
|
||||||
$: any; // jQuery
|
|
||||||
Store: any; // Luckysheet Store
|
|
||||||
luckysheet_function: any; // Luckysheet function list
|
|
||||||
functionlist: any[]; // 글로벌 functionlist
|
|
||||||
luckysheetConfigsetting: any; // Luckysheet 설정 객체
|
|
||||||
luckysheetPostil: any; // Luckysheet 포스틸 객체
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SheetViewerProps {
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Luckysheet 시트 뷰어 컴포넌트
|
|
||||||
* - 메모리 정보 기반: LuckyExcel 변환 결과를 직접 사용
|
|
||||||
* - 커스텀 검증이나 데이터 구조 변경 금지
|
|
||||||
* - luckysheet.create({ data: exportJson.sheets })로 직접 사용
|
|
||||||
*/
|
|
||||||
export function SheetViewer({ className }: SheetViewerProps) {
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const luckysheetRef = useRef<any>(null);
|
|
||||||
const [isInitialized, setIsInitialized] = useState(false);
|
|
||||||
const [isConverting, setIsConverting] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [isContainerReady, setIsContainerReady] = useState(false);
|
|
||||||
const [librariesLoaded, setLibrariesLoaded] = useState(false);
|
|
||||||
|
|
||||||
// 스토어에서 현재 파일 정보만 가져오기 (시트 데이터는 LuckyExcel로 직접 변환)
|
|
||||||
const { currentFile, setSelectedRange } = useAppStore();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CDN 배포판 라이브러리 로딩
|
|
||||||
*/
|
|
||||||
const loadLuckysheetLibrary = useCallback((): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// 이미 로드된 경우
|
|
||||||
if (
|
|
||||||
window.luckysheet &&
|
|
||||||
window.LuckyExcel &&
|
|
||||||
window.$ &&
|
|
||||||
librariesLoaded
|
|
||||||
) {
|
|
||||||
console.log("📦 모든 라이브러리가 이미 로드됨");
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadResource = (
|
|
||||||
type: "css" | "js",
|
|
||||||
src: string,
|
|
||||||
id: string,
|
|
||||||
): Promise<void> => {
|
|
||||||
return new Promise((resourceResolve, resourceReject) => {
|
|
||||||
// 이미 로드된 리소스 체크
|
|
||||||
if (document.querySelector(`[data-luckysheet-id="${id}"]`)) {
|
|
||||||
resourceResolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "css") {
|
|
||||||
const link = document.createElement("link");
|
|
||||||
link.rel = "stylesheet";
|
|
||||||
link.href = src;
|
|
||||||
link.setAttribute("data-luckysheet-id", id);
|
|
||||||
link.onload = () => resourceResolve();
|
|
||||||
link.onerror = (error) =>
|
|
||||||
resourceReject(new Error(`${id} CSS 로드 실패`));
|
|
||||||
document.head.appendChild(link);
|
|
||||||
} else {
|
|
||||||
const script = document.createElement("script");
|
|
||||||
script.src = src;
|
|
||||||
script.setAttribute("data-luckysheet-id", id);
|
|
||||||
script.onload = () => resourceResolve();
|
|
||||||
script.onerror = (error) =>
|
|
||||||
resourceReject(new Error(`${id} JS 로드 실패`));
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadSequence = async () => {
|
|
||||||
try {
|
|
||||||
// 1. jQuery (Luckysheet 의존성)
|
|
||||||
if (!window.$) {
|
|
||||||
await loadResource(
|
|
||||||
"js",
|
|
||||||
"https://code.jquery.com/jquery-3.6.0.min.js",
|
|
||||||
"jquery",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. CSS 로드 (공식 문서 순서 준수)
|
|
||||||
await loadResource(
|
|
||||||
"css",
|
|
||||||
"https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/css/pluginsCss.css",
|
|
||||||
"plugins-css",
|
|
||||||
);
|
|
||||||
await loadResource(
|
|
||||||
"css",
|
|
||||||
"https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/plugins.css",
|
|
||||||
"plugins-main-css",
|
|
||||||
);
|
|
||||||
await loadResource(
|
|
||||||
"css",
|
|
||||||
"https://cdn.jsdelivr.net/npm/luckysheet/dist/css/luckysheet.css",
|
|
||||||
"luckysheet-css",
|
|
||||||
);
|
|
||||||
await loadResource(
|
|
||||||
"css",
|
|
||||||
"https://cdn.jsdelivr.net/npm/luckysheet/dist/assets/iconfont/iconfont.css",
|
|
||||||
"iconfont-css",
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. Plugin JS 먼저 로드 (functionlist 초기화)
|
|
||||||
await loadResource(
|
|
||||||
"js",
|
|
||||||
"https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/js/plugin.js",
|
|
||||||
"plugin-js",
|
|
||||||
);
|
|
||||||
|
|
||||||
// 4. Luckysheet 메인
|
|
||||||
if (!window.luckysheet) {
|
|
||||||
await loadResource(
|
|
||||||
"js",
|
|
||||||
"https://cdn.jsdelivr.net/npm/luckysheet/dist/luckysheet.umd.js",
|
|
||||||
"luckysheet",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. LuckyExcel (Excel 파일 처리용)
|
|
||||||
if (!window.LuckyExcel) {
|
|
||||||
await loadResource(
|
|
||||||
"js",
|
|
||||||
"https://cdn.jsdelivr.net/npm/luckyexcel/dist/luckyexcel.umd.js",
|
|
||||||
"luckyexcel",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 라이브러리 검증
|
|
||||||
const validationResults = {
|
|
||||||
jquery: !!window.$,
|
|
||||||
luckyExcel: !!window.LuckyExcel,
|
|
||||||
luckysheet: !!window.luckysheet,
|
|
||||||
luckysheetCreate: !!(
|
|
||||||
window.luckysheet &&
|
|
||||||
typeof window.luckysheet.create === "function"
|
|
||||||
),
|
|
||||||
luckysheetDestroy: !!(
|
|
||||||
window.luckysheet &&
|
|
||||||
typeof window.luckysheet.destroy === "function"
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
|
||||||
!validationResults.luckysheet ||
|
|
||||||
!validationResults.luckysheetCreate
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
"Luckysheet 객체가 올바르게 초기화되지 않았습니다.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLibrariesLoaded(true);
|
|
||||||
console.log("✅ 라이브러리 로드 완료");
|
|
||||||
resolve();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ 라이브러리 로딩 실패:", error);
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadSequence();
|
|
||||||
});
|
|
||||||
}, [librariesLoaded]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 메모리 정보 기반: LuckyExcel 변환 결과를 직접 사용하는 방식
|
|
||||||
* - LuckyExcel.transformExcelToLucky()에서 반환된 exportJson.sheets를 그대로 사용
|
|
||||||
* - 커스텀 검증이나 데이터 구조 변경 금지
|
|
||||||
*/
|
|
||||||
const convertXLSXWithLuckyExcel = useCallback(
|
|
||||||
async (xlsxBuffer: ArrayBuffer, fileName: string) => {
|
|
||||||
if (!containerRef.current) {
|
|
||||||
console.warn("⚠️ 컨테이너가 없습니다.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsConverting(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
console.log("🍀 메모리 정보 기반: LuckyExcel 직접 변환 시작...");
|
|
||||||
|
|
||||||
// 라이브러리 로드 확인
|
|
||||||
await loadLuckysheetLibrary();
|
|
||||||
|
|
||||||
// 기존 인스턴스 정리
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
window.luckysheet &&
|
|
||||||
typeof window.luckysheet.destroy === "function"
|
|
||||||
) {
|
|
||||||
window.luckysheet.destroy();
|
|
||||||
console.log("✅ 기존 인스턴스 destroy 완료");
|
|
||||||
}
|
|
||||||
} catch (destroyError) {
|
|
||||||
console.warn("⚠️ destroy 중 오류 (무시됨):", destroyError);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 컨테이너 초기화
|
|
||||||
if (containerRef.current) {
|
|
||||||
containerRef.current.innerHTML = "";
|
|
||||||
console.log("✅ 컨테이너 초기화 완료");
|
|
||||||
}
|
|
||||||
|
|
||||||
luckysheetRef.current = null;
|
|
||||||
|
|
||||||
console.log("🍀 LuckyExcel.transformExcelToLucky 호출...");
|
|
||||||
|
|
||||||
// ArrayBuffer를 File 객체로 변환 (LuckyExcel은 File 객체 필요)
|
|
||||||
const file = new File([xlsxBuffer], fileName, {
|
|
||||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
||||||
});
|
|
||||||
|
|
||||||
// LuckyExcel의 직접 변환 사용 (Promise 방식)
|
|
||||||
const luckyExcelResult = await new Promise<any>((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
// 🚨 수정: 첫 번째 매개변수는 File 객체여야 함
|
|
||||||
(window.LuckyExcel as any).transformExcelToLucky(
|
|
||||||
file, // ArrayBuffer 대신 File 객체 사용
|
|
||||||
// 성공 콜백
|
|
||||||
(exportJson: any, luckysheetfile: any) => {
|
|
||||||
console.log("🍀 LuckyExcel 변환 성공!");
|
|
||||||
console.log("🍀 exportJson:", exportJson);
|
|
||||||
console.log("🍀 luckysheetfile:", luckysheetfile);
|
|
||||||
resolve(exportJson);
|
|
||||||
},
|
|
||||||
// 에러 콜백
|
|
||||||
(error: any) => {
|
|
||||||
console.error("❌ LuckyExcel 변환 실패:", error);
|
|
||||||
reject(new Error(`LuckyExcel 변환 실패: ${error}`));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (callError) {
|
|
||||||
console.error("❌ LuckyExcel 호출 중 오류:", callError);
|
|
||||||
reject(callError);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 결과 검증
|
|
||||||
if (
|
|
||||||
!luckyExcelResult ||
|
|
||||||
!luckyExcelResult.sheets ||
|
|
||||||
!Array.isArray(luckyExcelResult.sheets)
|
|
||||||
) {
|
|
||||||
throw new Error("LuckyExcel 변환 결과가 유효하지 않습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎉 LuckyExcel 변환 완료, Luckysheet 생성 중...");
|
|
||||||
|
|
||||||
// 메모리 정보 기반: exportJson.sheets를 그대로 사용
|
|
||||||
// luckysheet.create({ data: exportJson.sheets })
|
|
||||||
window.luckysheet.create({
|
|
||||||
container: containerRef.current?.id || "luckysheet-container",
|
|
||||||
showinfobar: false,
|
|
||||||
showtoolbar: false,
|
|
||||||
showsheetbar: true,
|
|
||||||
showstatisticBar: false,
|
|
||||||
allowCopy: true,
|
|
||||||
allowEdit: true,
|
|
||||||
// 🚨 핵심: LuckyExcel의 원본 변환 결과를 직접 사용
|
|
||||||
data: luckyExcelResult.sheets, // 가공하지 않고 그대로 전달
|
|
||||||
title: luckyExcelResult.info?.name || fileName,
|
|
||||||
// 🚨 수정: userInfo 경로 수정
|
|
||||||
userInfo: luckyExcelResult.info?.creator || false,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("🎉 Luckysheet 생성 완료! (원본 데이터 직접 사용)");
|
|
||||||
setIsInitialized(true);
|
|
||||||
setIsConverting(false);
|
|
||||||
setError(null);
|
|
||||||
luckysheetRef.current = window.luckysheet;
|
|
||||||
} catch (conversionError) {
|
|
||||||
console.error("❌ 변환 프로세스 실패:", conversionError);
|
|
||||||
setError(
|
|
||||||
`변환 프로세스에 실패했습니다: ${
|
|
||||||
conversionError instanceof Error
|
|
||||||
? conversionError.message
|
|
||||||
: String(conversionError)
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
setIsConverting(false);
|
|
||||||
setIsInitialized(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[loadLuckysheetLibrary, setSelectedRange],
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DOM 컨테이너 준비 상태 체크 - useLayoutEffect로 동기적 체크
|
|
||||||
*/
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (containerRef.current) {
|
|
||||||
console.log("✅ DOM 컨테이너 준비 완료:", containerRef.current.id);
|
|
||||||
setIsContainerReady(true);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DOM 컨테이너 준비 상태 재체크 (fallback)
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isContainerReady) {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
if (containerRef.current && !isContainerReady) {
|
|
||||||
console.log("✅ useEffect: DOM 컨테이너 지연 준비 완료");
|
|
||||||
setIsContainerReady(true);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [isContainerReady]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 컴포넌트 마운트 시 초기화
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
currentFile?.xlsxBuffer &&
|
|
||||||
isContainerReady &&
|
|
||||||
containerRef.current &&
|
|
||||||
!isInitialized &&
|
|
||||||
!isConverting
|
|
||||||
) {
|
|
||||||
console.log("🔄 XLSX 버퍼 감지, LuckyExcel 직접 변환 시작...", {
|
|
||||||
fileName: currentFile.name,
|
|
||||||
bufferSize: currentFile.xlsxBuffer.byteLength,
|
|
||||||
containerId: containerRef.current.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 중복 실행 방지
|
|
||||||
setIsConverting(true);
|
|
||||||
|
|
||||||
// LuckyExcel로 직접 변환
|
|
||||||
convertXLSXWithLuckyExcel(currentFile.xlsxBuffer, currentFile.name);
|
|
||||||
} else if (currentFile && !currentFile.xlsxBuffer) {
|
|
||||||
setError("파일 변환 데이터가 없습니다. 파일을 다시 업로드해주세요.");
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
currentFile?.xlsxBuffer,
|
|
||||||
currentFile?.name,
|
|
||||||
isContainerReady,
|
|
||||||
isInitialized,
|
|
||||||
isConverting,
|
|
||||||
convertXLSXWithLuckyExcel,
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 컴포넌트 언마운트 시 정리
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (luckysheetRef.current && window.luckysheet) {
|
|
||||||
try {
|
|
||||||
window.luckysheet.destroy();
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("⚠️ Luckysheet 정리 중 오류:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 윈도우 리사이즈 처리
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
const handleResize = () => {
|
|
||||||
if (luckysheetRef.current && window.luckysheet) {
|
|
||||||
try {
|
|
||||||
window.luckysheet.resize();
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("⚠️ Luckysheet 리사이즈 중 오류:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("resize", handleResize);
|
|
||||||
return () => window.removeEventListener("resize", handleResize);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`w-full h-full min-h-[70vh] ${className || ""}`}
|
|
||||||
style={{ position: "relative" }}
|
|
||||||
>
|
|
||||||
{/* Luckysheet 컨테이너 - 항상 렌더링 */}
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
id="luckysheet-container"
|
|
||||||
className="w-full h-full"
|
|
||||||
style={{
|
|
||||||
minHeight: "70vh",
|
|
||||||
border: "1px solid #e5e7eb",
|
|
||||||
borderRadius: "8px",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 에러 상태 오버레이 */}
|
|
||||||
{error && (
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-red-50 border border-red-200 rounded-lg">
|
|
||||||
<div className="text-center p-6">
|
|
||||||
<div className="text-red-600 text-lg font-semibold mb-2">
|
|
||||||
시트 로드 오류
|
|
||||||
</div>
|
|
||||||
<div className="text-red-500 text-sm mb-4">{error}</div>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setError(null);
|
|
||||||
setIsInitialized(false);
|
|
||||||
setIsConverting(false);
|
|
||||||
if (currentFile?.xlsxBuffer) {
|
|
||||||
convertXLSXWithLuckyExcel(
|
|
||||||
currentFile.xlsxBuffer,
|
|
||||||
currentFile.name,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="mt-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
|
|
||||||
>
|
|
||||||
다시 시도
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 로딩 상태 오버레이 */}
|
|
||||||
{!error &&
|
|
||||||
(isConverting || !isInitialized) &&
|
|
||||||
currentFile?.xlsxBuffer && (
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-blue-50 border border-blue-200 rounded-lg">
|
|
||||||
<div className="text-center p-6">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
||||||
<div className="text-blue-600 text-lg font-semibold mb-2">
|
|
||||||
{isConverting ? "LuckyExcel 변환 중..." : "시트 초기화 중..."}
|
|
||||||
</div>
|
|
||||||
<div className="text-blue-500 text-sm">
|
|
||||||
{isConverting
|
|
||||||
? "원본 Excel 데이터를 완전한 스타일로 변환하고 있습니다."
|
|
||||||
: "잠시만 기다려주세요."}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 데이터 없음 상태 오버레이 */}
|
|
||||||
{!error && !currentFile?.xlsxBuffer && (
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-50 border border-gray-200 rounded-lg">
|
|
||||||
<div className="text-center p-6">
|
|
||||||
<div className="text-gray-500 text-lg font-semibold mb-2">
|
|
||||||
표시할 시트가 없습니다
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-400 text-sm">
|
|
||||||
Excel 파일을 업로드해주세요.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 시트 정보 표시 (개발용) */}
|
|
||||||
{process.env.NODE_ENV === "development" && (
|
|
||||||
<div className="absolute top-2 right-2 bg-black bg-opacity-75 text-white text-xs p-2 rounded z-10">
|
|
||||||
<div>파일: {currentFile?.name}</div>
|
|
||||||
<div>
|
|
||||||
XLSX 버퍼:{" "}
|
|
||||||
{currentFile?.xlsxBuffer
|
|
||||||
? `${currentFile.xlsxBuffer.byteLength} bytes`
|
|
||||||
: "없음"}
|
|
||||||
</div>
|
|
||||||
<div>변환 중: {isConverting ? "예" : "아니오"}</div>
|
|
||||||
<div>초기화: {isInitialized ? "완료" : "대기"}</div>
|
|
||||||
<div>컨테이너 준비: {isContainerReady ? "완료" : "대기"}</div>
|
|
||||||
<div>방식: LuckyExcel 직접 변환</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, {
|
import {
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useRef,
|
useRef,
|
||||||
@@ -6,21 +6,6 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useAppStore } from "../../stores/useAppStore";
|
import { useAppStore } from "../../stores/useAppStore";
|
||||||
import type { SheetData } from "../../types/sheet";
|
|
||||||
|
|
||||||
// Window 타입 확장
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
luckysheet: any;
|
|
||||||
LuckyExcel: any;
|
|
||||||
$: any; // jQuery
|
|
||||||
Store: any; // Luckysheet Store
|
|
||||||
luckysheet_function: any; // Luckysheet function list
|
|
||||||
functionlist: any[]; // 글로벌 functionlist
|
|
||||||
luckysheetConfigsetting: any; // Luckysheet 설정 객체
|
|
||||||
luckysheetPostil: any; // Luckysheet 포스틸 객체
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SheetViewerProps {
|
interface SheetViewerProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -28,9 +13,9 @@ interface SheetViewerProps {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Luckysheet 시트 뷰어 컴포넌트
|
* Luckysheet 시트 뷰어 컴포넌트
|
||||||
* - 참고 내용 기반: 완전한 라이브러리 로딩 순서 적용
|
* - 메모리 정보 기반: LuckyExcel 변환 결과를 직접 사용
|
||||||
* - functionlist 오류 방지를 위한 완전한 초기화
|
* - 커스텀 검증이나 데이터 구조 변경 금지
|
||||||
* - 필수 플러그인과 CSS 포함
|
* - luckysheet.create({ data: exportJson.sheets })로 직접 사용
|
||||||
*/
|
*/
|
||||||
export function SheetViewer({ className }: SheetViewerProps) {
|
export function SheetViewer({ className }: SheetViewerProps) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -41,12 +26,11 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
|||||||
const [isContainerReady, setIsContainerReady] = useState(false);
|
const [isContainerReady, setIsContainerReady] = useState(false);
|
||||||
const [librariesLoaded, setLibrariesLoaded] = useState(false);
|
const [librariesLoaded, setLibrariesLoaded] = useState(false);
|
||||||
|
|
||||||
// 스토어에서 시트 데이터 가져오기
|
// 스토어에서 현재 파일 정보만 가져오기 (시트 데이터는 LuckyExcel로 직접 변환)
|
||||||
const { sheets, activeSheetId, currentFile, setSelectedRange } =
|
const { currentFile, setSelectedRange } = useAppStore();
|
||||||
useAppStore();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CDN 배포판 + functionlist 직접 초기화 방식
|
* CDN 배포판 라이브러리 로딩
|
||||||
*/
|
*/
|
||||||
const loadLuckysheetLibrary = useCallback((): Promise<void> => {
|
const loadLuckysheetLibrary = useCallback((): Promise<void> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -62,8 +46,6 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log("📦 CDN 배포판 + functionlist 직접 초기화 방식...");
|
|
||||||
|
|
||||||
const loadResource = (
|
const loadResource = (
|
||||||
type: "css" | "js",
|
type: "css" | "js",
|
||||||
src: string,
|
src: string,
|
||||||
@@ -72,7 +54,6 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
|||||||
return new Promise((resourceResolve, resourceReject) => {
|
return new Promise((resourceResolve, resourceReject) => {
|
||||||
// 이미 로드된 리소스 체크
|
// 이미 로드된 리소스 체크
|
||||||
if (document.querySelector(`[data-luckysheet-id="${id}"]`)) {
|
if (document.querySelector(`[data-luckysheet-id="${id}"]`)) {
|
||||||
// console.log(`📦 ${id} 이미 로드됨`);
|
|
||||||
resourceResolve();
|
resourceResolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -82,33 +63,22 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
|||||||
link.rel = "stylesheet";
|
link.rel = "stylesheet";
|
||||||
link.href = src;
|
link.href = src;
|
||||||
link.setAttribute("data-luckysheet-id", id);
|
link.setAttribute("data-luckysheet-id", id);
|
||||||
link.onload = () => {
|
link.onload = () => resourceResolve();
|
||||||
// console.log(`✅ ${id} CSS 로드 완료`);
|
link.onerror = () =>
|
||||||
resourceResolve();
|
|
||||||
};
|
|
||||||
link.onerror = (error) => {
|
|
||||||
// console.error(`❌ ${id} CSS 로드 실패:`, error);
|
|
||||||
resourceReject(new Error(`${id} CSS 로드 실패`));
|
resourceReject(new Error(`${id} CSS 로드 실패`));
|
||||||
};
|
|
||||||
document.head.appendChild(link);
|
document.head.appendChild(link);
|
||||||
} else {
|
} else {
|
||||||
const script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
script.src = src;
|
script.src = src;
|
||||||
script.setAttribute("data-luckysheet-id", id);
|
script.setAttribute("data-luckysheet-id", id);
|
||||||
script.onload = () => {
|
script.onload = () => resourceResolve();
|
||||||
// console.log(`✅ ${id} JS 로드 완료`);
|
script.onerror = () =>
|
||||||
resourceResolve();
|
|
||||||
};
|
|
||||||
script.onerror = (error) => {
|
|
||||||
// console.error(`❌ ${id} JS 로드 실패:`, error);
|
|
||||||
resourceReject(new Error(`${id} JS 로드 실패`));
|
resourceReject(new Error(`${id} JS 로드 실패`));
|
||||||
};
|
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// CDN 배포판 로딩 + functionlist 직접 초기화
|
|
||||||
const loadSequence = async () => {
|
const loadSequence = async () => {
|
||||||
try {
|
try {
|
||||||
// 1. jQuery (Luckysheet 의존성)
|
// 1. jQuery (Luckysheet 의존성)
|
||||||
@@ -120,79 +90,54 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. CSS 로드 (빌드된 파일들)
|
// 2. CSS 로드 (공식 문서 순서 준수)
|
||||||
await loadResource(
|
await loadResource(
|
||||||
"css",
|
"css",
|
||||||
"/luckysheet/dist/plugins/css/pluginsCss.css",
|
"https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/css/pluginsCss.css",
|
||||||
"plugins-css",
|
"plugins-css",
|
||||||
);
|
);
|
||||||
await loadResource(
|
await loadResource(
|
||||||
"css",
|
"css",
|
||||||
"/luckysheet/dist/plugins/plugins.css",
|
"https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/plugins.css",
|
||||||
"plugins-main-css",
|
"plugins-main-css",
|
||||||
);
|
);
|
||||||
await loadResource(
|
await loadResource(
|
||||||
"css",
|
"css",
|
||||||
"/luckysheet/dist/css/luckysheet.css",
|
"https://cdn.jsdelivr.net/npm/luckysheet/dist/css/luckysheet.css",
|
||||||
"luckysheet-css",
|
"luckysheet-css",
|
||||||
);
|
);
|
||||||
await loadResource(
|
await loadResource(
|
||||||
"css",
|
"css",
|
||||||
"/luckysheet/dist/assets/iconfont/iconfont.css",
|
"https://cdn.jsdelivr.net/npm/luckysheet/dist/assets/iconfont/iconfont.css",
|
||||||
"iconfont-css",
|
"iconfont-css",
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Plugin JS 먼저 로드 (functionlist 초기화 우선)
|
// 3. Plugin JS 먼저 로드 (functionlist 초기화)
|
||||||
await loadResource(
|
await loadResource(
|
||||||
"js",
|
"js",
|
||||||
"/luckysheet/dist/plugins/js/plugin.js",
|
"https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/js/plugin.js",
|
||||||
"plugin-js",
|
"plugin-js",
|
||||||
);
|
);
|
||||||
|
|
||||||
// 👉 plugin.js 로드 후 실제 functionlist 가 채워졌는지 polling 으로 확인 (최대 3초)
|
// 4. Luckysheet 메인
|
||||||
const waitForFunctionlistReady = (
|
|
||||||
timeout = 3000,
|
|
||||||
interval = 50,
|
|
||||||
): Promise<void> => {
|
|
||||||
return new Promise((res, rej) => {
|
|
||||||
let waited = 0;
|
|
||||||
const timer = setInterval(() => {
|
|
||||||
if (window.Store?.functionlist?.length) {
|
|
||||||
clearInterval(timer);
|
|
||||||
res();
|
|
||||||
} else if ((waited += interval) >= timeout) {
|
|
||||||
clearInterval(timer);
|
|
||||||
rej(new Error("functionlist 초기화 시간 초과"));
|
|
||||||
}
|
|
||||||
}, interval);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
await waitForFunctionlistReady();
|
|
||||||
|
|
||||||
// 4. LuckyExcel (Excel 파일 처리용)
|
|
||||||
if (!window.LuckyExcel) {
|
|
||||||
await loadResource(
|
|
||||||
"js",
|
|
||||||
"/luckysheet/dist/luckyexcel.umd.js",
|
|
||||||
"luckyexcel",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Luckysheet 메인 (functionlist 준비 후)
|
|
||||||
if (!window.luckysheet) {
|
if (!window.luckysheet) {
|
||||||
await loadResource(
|
await loadResource(
|
||||||
"js",
|
"js",
|
||||||
"/luckysheet/dist/luckysheet.umd.js",
|
"https://cdn.jsdelivr.net/npm/luckysheet/dist/luckysheet.umd.js",
|
||||||
"luckysheet",
|
"luckysheet",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 라이브러리 로드 후 검증
|
// 5. LuckyExcel (Excel 파일 처리용)
|
||||||
// console.log("🔍 라이브러리 로드 후 검증 중...");
|
if (!window.LuckyExcel) {
|
||||||
|
await loadResource(
|
||||||
|
"js",
|
||||||
|
"https://cdn.jsdelivr.net/npm/luckyexcel/dist/luckyexcel.umd.js",
|
||||||
|
"luckyexcel",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: plugin.js 가 실제 functionlist 를 채웠으므로 별도 지연 대기 불필요
|
// 라이브러리 검증
|
||||||
// 필수 객체 검증
|
|
||||||
const validationResults = {
|
const validationResults = {
|
||||||
jquery: !!window.$,
|
jquery: !!window.$,
|
||||||
luckyExcel: !!window.LuckyExcel,
|
luckyExcel: !!window.LuckyExcel,
|
||||||
@@ -207,8 +152,6 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log("🔍 라이브러리 검증 결과:", validationResults);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!validationResults.luckysheet ||
|
!validationResults.luckysheet ||
|
||||||
!validationResults.luckysheetCreate
|
!validationResults.luckysheetCreate
|
||||||
@@ -218,75 +161,11 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔧 강력한 functionlist 초기화 (메모리 해결책 적용)
|
|
||||||
// console.log("🔧 강력한 functionlist 및 모든 필수 객체 초기화 중...");
|
|
||||||
try {
|
|
||||||
// 1. Store 객체 강제 생성
|
|
||||||
if (!window.Store) {
|
|
||||||
window.Store = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. functionlist 다중 레벨 초기화
|
|
||||||
if (!window.Store.functionlist) {
|
|
||||||
window.Store.functionlist = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. luckysheet_function 다중 레벨 초기화
|
|
||||||
if (!window.luckysheet_function) {
|
|
||||||
window.luckysheet_function = {};
|
|
||||||
}
|
|
||||||
if (!window.Store.luckysheet_function) {
|
|
||||||
window.Store.luckysheet_function = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Luckysheet 내부에서 사용하는 추가 functionlist 객체들 초기화
|
|
||||||
if (window.luckysheet && !window.luckysheet.functionlist) {
|
|
||||||
window.luckysheet.functionlist = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 글로벌 functionlist 초기화 (다양한 참조 경로 대응)
|
|
||||||
if (!window.functionlist) {
|
|
||||||
window.functionlist = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Store 내부 구조 완전 초기화
|
|
||||||
if (!window.Store.config) {
|
|
||||||
window.Store.config = {};
|
|
||||||
}
|
|
||||||
if (!window.Store.luckysheetfile) {
|
|
||||||
window.Store.luckysheetfile = [];
|
|
||||||
}
|
|
||||||
if (!window.Store.currentSheetIndex) {
|
|
||||||
window.Store.currentSheetIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. Luckysheet 모듈별 초기화 확인
|
|
||||||
if (window.luckysheet) {
|
|
||||||
// 함수 관련 모듈 초기화
|
|
||||||
if (!window.luckysheet.formula) {
|
|
||||||
window.luckysheet.formula = {};
|
|
||||||
}
|
|
||||||
if (!window.luckysheet.formulaCache) {
|
|
||||||
window.luckysheet.formulaCache = {};
|
|
||||||
}
|
|
||||||
if (!window.luckysheet.formulaObjects) {
|
|
||||||
window.luckysheet.formulaObjects = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log("✅ 강력한 functionlist 및 모든 필수 객체 초기화 완료");
|
|
||||||
} catch (functionError) {
|
|
||||||
// console.warn(
|
|
||||||
// "⚠️ 강력한 functionlist 초기화 중 오류 (무시됨):",
|
|
||||||
// functionError,
|
|
||||||
// );
|
|
||||||
}
|
|
||||||
|
|
||||||
setLibrariesLoaded(true);
|
setLibrariesLoaded(true);
|
||||||
// console.log("✅ CDN 배포판 + functionlist 초기화 완료");
|
console.log("✅ 라이브러리 로드 완료");
|
||||||
resolve();
|
resolve();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error("❌ 라이브러리 로딩 실패:", error);
|
console.error("❌ 라이브러리 로딩 실패:", error);
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -296,9 +175,11 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
|||||||
}, [librariesLoaded]);
|
}, [librariesLoaded]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 참고 내용 기반: 올바른 데이터 구조로 Luckysheet 초기화
|
* 메모리 정보 기반: LuckyExcel 변환 결과를 직접 사용하는 방식
|
||||||
|
* - LuckyExcel.transformExcelToLucky()에서 반환된 exportJson.sheets를 그대로 사용
|
||||||
|
* - 커스텀 검증이나 데이터 구조 변경 금지
|
||||||
*/
|
*/
|
||||||
const convertXLSXToLuckysheet = useCallback(
|
const convertXLSXWithLuckyExcel = useCallback(
|
||||||
async (xlsxBuffer: ArrayBuffer, fileName: string) => {
|
async (xlsxBuffer: ArrayBuffer, fileName: string) => {
|
||||||
if (!containerRef.current) {
|
if (!containerRef.current) {
|
||||||
console.warn("⚠️ 컨테이너가 없습니다.");
|
console.warn("⚠️ 컨테이너가 없습니다.");
|
||||||
@@ -309,317 +190,97 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
|||||||
setIsConverting(true);
|
setIsConverting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// console.log(
|
console.log("🍀 메모리 정보 기반: LuckyExcel 직접 변환 시작...");
|
||||||
// "🔄 참고 내용 기반: XLSX → LuckyExcel → Luckysheet 변환 시작...",
|
|
||||||
// );
|
|
||||||
|
|
||||||
// 라이브러리 로드 확인
|
// 라이브러리 로드 확인
|
||||||
await loadLuckysheetLibrary();
|
await loadLuckysheetLibrary();
|
||||||
|
|
||||||
// 기존 인스턴스 정리 (참고 내용 권장사항)
|
// 기존 인스턴스 정리
|
||||||
// console.log("🧹 기존 Luckysheet 인스턴스 정리...");
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
window.luckysheet &&
|
window.luckysheet &&
|
||||||
typeof window.luckysheet.destroy === "function"
|
typeof window.luckysheet.destroy === "function"
|
||||||
) {
|
) {
|
||||||
window.luckysheet.destroy();
|
window.luckysheet.destroy();
|
||||||
// console.log("✅ 기존 인스턴스 destroy 완료");
|
console.log("✅ 기존 인스턴스 destroy 완료");
|
||||||
}
|
}
|
||||||
} catch (destroyError) {
|
} catch (destroyError) {
|
||||||
// console.warn("⚠️ destroy 중 오류 (무시됨):", destroyError);
|
console.warn("⚠️ destroy 중 오류 (무시됨):", destroyError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 컨테이너 초기화
|
// 컨테이너 초기화
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
containerRef.current.innerHTML = "";
|
containerRef.current.innerHTML = "";
|
||||||
// console.log("✅ 컨테이너 초기화 완료");
|
console.log("✅ 컨테이너 초기화 완료");
|
||||||
}
|
}
|
||||||
|
|
||||||
luckysheetRef.current = null;
|
luckysheetRef.current = null;
|
||||||
|
|
||||||
// console.log("🍀 LuckyExcel.transformExcelToLucky 호출...");
|
console.log("🍀 LuckyExcel.transformExcelToLucky 호출...");
|
||||||
|
|
||||||
// LuckyExcel 변환 (참고 내용의 블로그 포스트 방식)
|
// ArrayBuffer를 File 객체로 변환 (LuckyExcel은 File 객체 필요)
|
||||||
window.LuckyExcel.transformExcelToLucky(
|
const file = new File([xlsxBuffer], fileName, {
|
||||||
xlsxBuffer,
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
// 성공 콜백 - 변환 완료 후에만 Luckysheet 초기화
|
});
|
||||||
|
|
||||||
|
// LuckyExcel의 직접 변환 사용 (Promise 방식)
|
||||||
|
const luckyExcelResult = await new Promise<any>((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// 🚨 수정: 첫 번째 매개변수는 File 객체여야 함
|
||||||
|
(window.LuckyExcel as any).transformExcelToLucky(
|
||||||
|
file, // ArrayBuffer 대신 File 객체 사용
|
||||||
|
// 성공 콜백
|
||||||
(exportJson: any, luckysheetfile: any) => {
|
(exportJson: any, luckysheetfile: any) => {
|
||||||
try {
|
console.log("🍀 LuckyExcel 변환 성공!");
|
||||||
// console.log("✅ LuckyExcel 변환 완료:", {
|
console.log("🍀 exportJson:", exportJson);
|
||||||
// hasExportJson: !!exportJson,
|
console.log("🍀 luckysheetfile:", luckysheetfile);
|
||||||
// hasSheets: !!exportJson?.sheets,
|
resolve(exportJson);
|
||||||
// sheetsCount: exportJson?.sheets?.length || 0,
|
|
||||||
// sheetsStructure:
|
|
||||||
// exportJson?.sheets?.map((sheet: any, index: number) => ({
|
|
||||||
// index,
|
|
||||||
// name: sheet?.name,
|
|
||||||
// hasData: !!sheet?.data,
|
|
||||||
// dataLength: Array.isArray(sheet?.data)
|
|
||||||
// ? sheet.data.length
|
|
||||||
// : 0,
|
|
||||||
// })) || [],
|
|
||||||
// });
|
|
||||||
|
|
||||||
// 공식 LuckyExcel 방식: 기본 검증만 수행 (과도한 변환 방지)
|
|
||||||
if (
|
|
||||||
!exportJson ||
|
|
||||||
!exportJson.sheets ||
|
|
||||||
!Array.isArray(exportJson.sheets)
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
"LuckyExcel 변환 결과가 유효하지 않습니다: sheets 배열이 없음",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exportJson.sheets.length === 0) {
|
|
||||||
throw new Error("변환된 시트가 없습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log("✅ LuckyExcel 변환 결과 검증 완료:", {
|
|
||||||
// sheetsCount: exportJson.sheets.length,
|
|
||||||
// hasInfo: !!exportJson.info,
|
|
||||||
// infoName: exportJson.info?.name,
|
|
||||||
// infoCreator: exportJson.info?.creator,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// console.log(
|
|
||||||
// "🎯 functionlist 초기화 완료: Luckysheet 초기화 시작...",
|
|
||||||
// );
|
|
||||||
|
|
||||||
// 메모리 해결책: 최종 완전한 functionlist 및 모든 Luckysheet 내부 객체 초기화
|
|
||||||
try {
|
|
||||||
// Level 1: Store 객체 완전 초기화
|
|
||||||
if (!window.Store) window.Store = {};
|
|
||||||
if (!window.Store.functionlist) window.Store.functionlist = [];
|
|
||||||
if (!window.Store.luckysheet_function)
|
|
||||||
window.Store.luckysheet_function = {};
|
|
||||||
if (!window.Store.config) window.Store.config = {};
|
|
||||||
if (!window.Store.luckysheetfile)
|
|
||||||
window.Store.luckysheetfile = [];
|
|
||||||
|
|
||||||
// Level 2: 글로벌 function 객체들 완전 초기화
|
|
||||||
if (!window.luckysheet_function)
|
|
||||||
window.luckysheet_function = {};
|
|
||||||
if (!window.functionlist) window.functionlist = [];
|
|
||||||
|
|
||||||
// Level 3: Luckysheet 내부 깊은 레벨 초기화
|
|
||||||
if (window.luckysheet) {
|
|
||||||
// 함수 관련 깊은 객체들
|
|
||||||
if (!window.luckysheet.functionlist)
|
|
||||||
window.luckysheet.functionlist = [];
|
|
||||||
if (!window.luckysheet.formula)
|
|
||||||
window.luckysheet.formula = {};
|
|
||||||
if (!window.luckysheet.formulaCache)
|
|
||||||
window.luckysheet.formulaCache = {};
|
|
||||||
if (!window.luckysheet.formulaObjects)
|
|
||||||
window.luckysheet.formulaObjects = {};
|
|
||||||
|
|
||||||
// Store 레퍼런스 초기화
|
|
||||||
if (!window.luckysheet.Store)
|
|
||||||
window.luckysheet.Store = window.Store;
|
|
||||||
if (!window.luckysheet.luckysheetfile)
|
|
||||||
window.luckysheet.luckysheetfile = [];
|
|
||||||
|
|
||||||
// 내부 모듈들 초기화
|
|
||||||
if (!window.luckysheet.menuButton)
|
|
||||||
window.luckysheet.menuButton = {};
|
|
||||||
if (!window.luckysheet.server) window.luckysheet.server = {};
|
|
||||||
if (!window.luckysheet.selection)
|
|
||||||
window.luckysheet.selection = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Level 4: 추가적인 깊은 레벨 객체들 (Luckysheet 내부에서 사용할 수 있는)
|
|
||||||
if (!window.luckysheetConfigsetting)
|
|
||||||
window.luckysheetConfigsetting = {};
|
|
||||||
if (!window.luckysheetPostil) window.luckysheetPostil = {};
|
|
||||||
if (!window.Store.visibledatarow)
|
|
||||||
window.Store.visibledatarow = [];
|
|
||||||
if (!window.Store.visibledatacolumn)
|
|
||||||
window.Store.visibledatacolumn = [];
|
|
||||||
if (!window.Store.defaultcollen)
|
|
||||||
window.Store.defaultcollen = 73;
|
|
||||||
if (!window.Store.defaultrowlen)
|
|
||||||
window.Store.defaultrowlen = 19;
|
|
||||||
|
|
||||||
console.log("✅ 완전한 Luckysheet 내부 객체 초기화 완료");
|
|
||||||
|
|
||||||
// 극한의 방법: Luckysheet 내부 코드 직접 패치 (임시)
|
|
||||||
if (
|
|
||||||
window.luckysheet &&
|
|
||||||
typeof window.luckysheet.create === "function"
|
|
||||||
) {
|
|
||||||
// Luckysheet 내부에서 사용하는 모든 가능한 functionlist 경로 강제 생성
|
|
||||||
const originalCreate = window.luckysheet.create;
|
|
||||||
window.luckysheet.create = function (options: any) {
|
|
||||||
try {
|
|
||||||
// 생성 직전 모든 functionlist 경로 재검증
|
|
||||||
if (!window.Store) window.Store = {};
|
|
||||||
if (!window.Store.functionlist)
|
|
||||||
window.Store.functionlist = [];
|
|
||||||
if (!this.functionlist) this.functionlist = [];
|
|
||||||
if (!this.Store) this.Store = window.Store;
|
|
||||||
if (!this.Store.functionlist)
|
|
||||||
this.Store.functionlist = [];
|
|
||||||
|
|
||||||
// 원본 함수 호출
|
|
||||||
return originalCreate.call(this, options);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
"Luckysheet create 패치된 함수에서 오류:",
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
console.log("🔧 Luckysheet.create 함수 패치 완료");
|
|
||||||
}
|
|
||||||
} catch (finalInitError) {
|
|
||||||
console.warn(
|
|
||||||
"⚠️ 완전한 functionlist 초기화 중 오류:",
|
|
||||||
finalInitError,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 공식 LuckyExcel 방식: exportJson.sheets를 직접 사용
|
|
||||||
const luckysheetOptions = {
|
|
||||||
container: containerRef.current?.id || "luckysheet-container",
|
|
||||||
title: exportJson.info?.name || fileName || "Sheet Easy AI",
|
|
||||||
lang: "ko",
|
|
||||||
data: exportJson.sheets, // 공식 방식: LuckyExcel 결과를 직접 사용
|
|
||||||
|
|
||||||
// userInfo도 공식 예시대로 설정
|
|
||||||
userInfo: exportJson.info?.creator || "Sheet Easy AI User",
|
|
||||||
|
|
||||||
// UI 설정 (모든 기능 활성화)
|
|
||||||
showinfobar: false,
|
|
||||||
showtoolbar: true, // 툴바 활성화
|
|
||||||
showsheetbar: true,
|
|
||||||
showstatisticBar: false,
|
|
||||||
showConfigWindowResize: true,
|
|
||||||
|
|
||||||
// 편집 기능 활성화
|
|
||||||
allowCopy: true,
|
|
||||||
allowEdit: true,
|
|
||||||
enableAddRow: true, // 행/열 추가 활성화
|
|
||||||
enableAddCol: true,
|
|
||||||
|
|
||||||
// 함수 기능 활성화
|
|
||||||
allowUpdate: true,
|
|
||||||
enableAddBackTop: true,
|
|
||||||
showFormulaBar: true, // 수식바 활성화
|
|
||||||
|
|
||||||
// 이벤트 핸들러 (모든 기능)
|
|
||||||
hook: {
|
|
||||||
cellClick: (cell: any, position: any, sheetFile: any) => {
|
|
||||||
// console.log("🖱️ 셀 클릭:", { cell, position, sheetFile });
|
|
||||||
if (
|
|
||||||
position &&
|
|
||||||
typeof position.r === "number" &&
|
|
||||||
typeof position.c === "number"
|
|
||||||
) {
|
|
||||||
setSelectedRange({
|
|
||||||
range: {
|
|
||||||
startRow: position.r,
|
|
||||||
startCol: position.c,
|
|
||||||
endRow: position.r,
|
|
||||||
endCol: position.c,
|
|
||||||
},
|
},
|
||||||
sheetId: sheetFile?.index || "0",
|
// 에러 콜백
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sheetActivate: (
|
|
||||||
index: number,
|
|
||||||
isPivotInitial: boolean,
|
|
||||||
isInitialLoad: boolean,
|
|
||||||
) => {
|
|
||||||
// console.log("📋 시트 활성화:", {
|
|
||||||
// index,
|
|
||||||
// isPivotInitial,
|
|
||||||
// isInitialLoad,
|
|
||||||
// });
|
|
||||||
if (exportJson.sheets[index]) {
|
|
||||||
useAppStore.getState().setActiveSheetId(`sheet_${index}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updated: (operate: any) => {
|
|
||||||
// console.log("🔄 시트 업데이트:", operate);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// === 상세 디버깅 정보 ===
|
|
||||||
console.log("=== LuckyExcel → Luckysheet 디버깅 정보 ===");
|
|
||||||
console.log("📋 exportJson 구조:", {
|
|
||||||
hasExportJson: !!exportJson,
|
|
||||||
hasInfo: !!exportJson?.info,
|
|
||||||
hasSheets: !!exportJson?.sheets,
|
|
||||||
sheetsCount: exportJson?.sheets?.length,
|
|
||||||
infoKeys: Object.keys(exportJson?.info || {}),
|
|
||||||
firstSheetKeys: Object.keys(exportJson?.sheets?.[0] || {}),
|
|
||||||
});
|
|
||||||
console.log("🔧 Functionlist 상태 상세 검사:", {
|
|
||||||
windowStore: !!window.Store,
|
|
||||||
storeFunctionlist: !!window.Store?.functionlist,
|
|
||||||
storeFunctionlistLength: window.Store?.functionlist?.length,
|
|
||||||
luckysheetFunction: !!window.luckysheet_function,
|
|
||||||
globalFunctionlist: !!window.functionlist,
|
|
||||||
globalFunctionlistLength: window.functionlist?.length,
|
|
||||||
luckysheetStoreFunctionlist:
|
|
||||||
!!window.luckysheet?.Store?.functionlist,
|
|
||||||
luckysheetOwnFunctionlist: !!window.luckysheet?.functionlist,
|
|
||||||
allWindowKeys: Object.keys(window).filter((key) =>
|
|
||||||
key.includes("function"),
|
|
||||||
),
|
|
||||||
allStoreKeys: Object.keys(window.Store || {}),
|
|
||||||
allLuckysheetKeys: Object.keys(window.luckysheet || {}).slice(
|
|
||||||
0,
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
console.log("🎯 Luckysheet 객체:", {
|
|
||||||
hasLuckysheet: !!window.luckysheet,
|
|
||||||
hasCreate: typeof window.luckysheet?.create === "function",
|
|
||||||
methodsCount: Object.keys(window.luckysheet || {}).length,
|
|
||||||
});
|
|
||||||
console.log("=== 디버깅 정보 끝 ===");
|
|
||||||
|
|
||||||
// Luckysheet 생성
|
|
||||||
window.luckysheet.create(luckysheetOptions);
|
|
||||||
|
|
||||||
luckysheetRef.current = window.luckysheet;
|
|
||||||
setIsInitialized(true);
|
|
||||||
setIsConverting(false);
|
|
||||||
|
|
||||||
// console.log(
|
|
||||||
// "✅ functionlist 초기화 완료: Luckysheet 초기화 완료!",
|
|
||||||
// );
|
|
||||||
} catch (initError) {
|
|
||||||
console.error("❌ Luckysheet 초기화 실패:", initError);
|
|
||||||
setError(
|
|
||||||
`시트 초기화에 실패했습니다: ${
|
|
||||||
initError instanceof Error
|
|
||||||
? initError.message
|
|
||||||
: String(initError)
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
setIsInitialized(false);
|
|
||||||
setIsConverting(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 오류 콜백
|
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
console.error("❌ LuckyExcel 변환 실패:", error);
|
console.error("❌ LuckyExcel 변환 실패:", error);
|
||||||
setError(
|
reject(new Error(`LuckyExcel 변환 실패: ${error}`));
|
||||||
`XLSX 변환에 실패했습니다: ${
|
|
||||||
error instanceof Error ? error.message : String(error)
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
setIsConverting(false);
|
|
||||||
setIsInitialized(false);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} catch (callError) {
|
||||||
|
console.error("❌ LuckyExcel 호출 중 오류:", callError);
|
||||||
|
reject(callError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 결과 검증
|
||||||
|
if (
|
||||||
|
!luckyExcelResult ||
|
||||||
|
!luckyExcelResult.sheets ||
|
||||||
|
!Array.isArray(luckyExcelResult.sheets)
|
||||||
|
) {
|
||||||
|
throw new Error("LuckyExcel 변환 결과가 유효하지 않습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🎉 LuckyExcel 변환 완료, Luckysheet 생성 중...");
|
||||||
|
|
||||||
|
// 메모리 정보 기반: exportJson.sheets를 그대로 사용
|
||||||
|
// luckysheet.create({ data: exportJson.sheets })
|
||||||
|
window.luckysheet.create({
|
||||||
|
container: containerRef.current?.id || "luckysheet-container",
|
||||||
|
showinfobar: true,
|
||||||
|
showtoolbar: true,
|
||||||
|
showsheetbar: true,
|
||||||
|
showstatisticBar: true,
|
||||||
|
allowCopy: true,
|
||||||
|
allowEdit: true,
|
||||||
|
// 🚨 핵심: LuckyExcel의 원본 변환 결과를 직접 사용
|
||||||
|
data: luckyExcelResult.sheets, // 가공하지 않고 그대로 전달
|
||||||
|
title: luckyExcelResult.info?.name || fileName,
|
||||||
|
// 🚨 수정: userInfo 경로 수정
|
||||||
|
userInfo: luckyExcelResult.info?.creator || false,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("🎉 Luckysheet 생성 완료! (원본 데이터 직접 사용)");
|
||||||
|
setIsInitialized(true);
|
||||||
|
setIsConverting(false);
|
||||||
|
setError(null);
|
||||||
|
luckysheetRef.current = window.luckysheet;
|
||||||
} catch (conversionError) {
|
} catch (conversionError) {
|
||||||
console.error("❌ 변환 프로세스 실패:", conversionError);
|
console.error("❌ 변환 프로세스 실패:", conversionError);
|
||||||
setError(
|
setError(
|
||||||
@@ -640,12 +301,9 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
|||||||
* DOM 컨테이너 준비 상태 체크 - useLayoutEffect로 동기적 체크
|
* DOM 컨테이너 준비 상태 체크 - useLayoutEffect로 동기적 체크
|
||||||
*/
|
*/
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
// console.log("🔍 useLayoutEffect: DOM 컨테이너 체크 시작...");
|
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
// console.log("✅ DOM 컨테이너 준비 완료:", containerRef.current.id);
|
console.log("✅ DOM 컨테이너 준비 완료:", containerRef.current.id);
|
||||||
setIsContainerReady(true);
|
setIsContainerReady(true);
|
||||||
} else {
|
|
||||||
// console.warn("⚠️ useLayoutEffect: DOM 컨테이너가 아직 준비되지 않음");
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -654,10 +312,9 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
|||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isContainerReady) {
|
if (!isContainerReady) {
|
||||||
// console.log("🔄 useEffect: DOM 컨테이너 재체크...");
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
if (containerRef.current && !isContainerReady) {
|
if (containerRef.current && !isContainerReady) {
|
||||||
// console.log("✅ useEffect: DOM 컨테이너 지연 준비 완료");
|
console.log("✅ useEffect: DOM 컨테이너 지연 준비 완료");
|
||||||
setIsContainerReady(true);
|
setIsContainerReady(true);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
@@ -666,43 +323,37 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
|||||||
}, [isContainerReady]);
|
}, [isContainerReady]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 컴포넌트 마운트 시 초기화 - 블로그 포스트 방식 적용
|
* 컴포넌트 마운트 시 초기화
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// console.log("🔍 useEffect 실행 조건 체크:", {
|
if (
|
||||||
// hasCurrentFile: !!currentFile,
|
currentFile?.xlsxBuffer &&
|
||||||
// hasXlsxBuffer: !!currentFile?.xlsxBuffer,
|
isContainerReady &&
|
||||||
// hasContainer: !!containerRef.current,
|
containerRef.current &&
|
||||||
// isContainerReady,
|
!isInitialized &&
|
||||||
// currentFileName: currentFile?.name,
|
!isConverting
|
||||||
// bufferSize: currentFile?.xlsxBuffer?.byteLength,
|
) {
|
||||||
// });
|
console.log("🔄 XLSX 버퍼 감지, LuckyExcel 직접 변환 시작...", {
|
||||||
|
fileName: currentFile.name,
|
||||||
|
bufferSize: currentFile.xlsxBuffer.byteLength,
|
||||||
|
containerId: containerRef.current.id,
|
||||||
|
});
|
||||||
|
|
||||||
if (currentFile?.xlsxBuffer && isContainerReady && containerRef.current) {
|
// 중복 실행 방지
|
||||||
// console.log("🔄 변환된 XLSX 감지, LuckyExcel → Luckysheet 시작...", {
|
setIsConverting(true);
|
||||||
// fileName: currentFile.name,
|
|
||||||
// bufferSize: currentFile.xlsxBuffer.byteLength,
|
|
||||||
// containerId: containerRef.current.id,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// 변환된 XLSX ArrayBuffer를 사용하여 직접 변환 (블로그 포스트 방식)
|
// LuckyExcel로 직접 변환
|
||||||
convertXLSXToLuckysheet(currentFile.xlsxBuffer, currentFile.name);
|
convertXLSXWithLuckyExcel(currentFile.xlsxBuffer, currentFile.name);
|
||||||
} else if (currentFile && !currentFile.xlsxBuffer) {
|
} else if (currentFile && !currentFile.xlsxBuffer) {
|
||||||
// console.warn(
|
|
||||||
// "⚠️ currentFile은 있지만 xlsxBuffer가 없습니다:",
|
|
||||||
// currentFile,
|
|
||||||
// );
|
|
||||||
setError("파일 변환 데이터가 없습니다. 파일을 다시 업로드해주세요.");
|
setError("파일 변환 데이터가 없습니다. 파일을 다시 업로드해주세요.");
|
||||||
} else if (!currentFile) {
|
|
||||||
// console.log("ℹ️ currentFile이 없습니다. 파일을 업로드해주세요.");
|
|
||||||
} else if (currentFile?.xlsxBuffer && !isContainerReady) {
|
|
||||||
// console.log("⏳ DOM 컨테이너 준비 대기 중...");
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
currentFile?.xlsxBuffer,
|
currentFile?.xlsxBuffer,
|
||||||
currentFile?.name,
|
currentFile?.name,
|
||||||
isContainerReady,
|
isContainerReady,
|
||||||
convertXLSXToLuckysheet,
|
isInitialized,
|
||||||
|
isConverting,
|
||||||
|
convertXLSXWithLuckyExcel,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -711,11 +362,10 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (luckysheetRef.current && window.luckysheet) {
|
if (luckysheetRef.current && window.luckysheet) {
|
||||||
// console.log("🧹 컴포넌트 언마운트: Luckysheet 정리 중...");
|
|
||||||
try {
|
try {
|
||||||
window.luckysheet.destroy();
|
window.luckysheet.destroy();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.warn("⚠️ Luckysheet 정리 중 오류:", error);
|
console.warn("⚠️ Luckysheet 정리 중 오류:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -728,9 +378,11 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
|||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
if (luckysheetRef.current && window.luckysheet) {
|
if (luckysheetRef.current && window.luckysheet) {
|
||||||
try {
|
try {
|
||||||
|
if (window.luckysheet.resize) {
|
||||||
window.luckysheet.resize();
|
window.luckysheet.resize();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.warn("⚠️ Luckysheet 리사이즈 중 오류:", error);
|
console.warn("⚠️ Luckysheet 리사이즈 중 오류:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -771,7 +423,7 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
|||||||
setIsInitialized(false);
|
setIsInitialized(false);
|
||||||
setIsConverting(false);
|
setIsConverting(false);
|
||||||
if (currentFile?.xlsxBuffer) {
|
if (currentFile?.xlsxBuffer) {
|
||||||
convertXLSXToLuckysheet(
|
convertXLSXWithLuckyExcel(
|
||||||
currentFile.xlsxBuffer,
|
currentFile.xlsxBuffer,
|
||||||
currentFile.name,
|
currentFile.name,
|
||||||
);
|
);
|
||||||
@@ -793,11 +445,11 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
|||||||
<div className="text-center p-6">
|
<div className="text-center p-6">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||||
<div className="text-blue-600 text-lg font-semibold mb-2">
|
<div className="text-blue-600 text-lg font-semibold mb-2">
|
||||||
{isConverting ? "XLSX 변환 중..." : "시트 초기화 중..."}
|
{isConverting ? "LuckyExcel 변환 중..." : "시트 초기화 중..."}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-blue-500 text-sm">
|
<div className="text-blue-500 text-sm">
|
||||||
{isConverting
|
{isConverting
|
||||||
? "변환된 XLSX를 Luckysheet로 처리하고 있습니다."
|
? "원본 Excel 데이터를 완전한 스타일로 변환하고 있습니다."
|
||||||
: "잠시만 기다려주세요."}
|
: "잠시만 기다려주세요."}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -831,6 +483,7 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
|||||||
<div>변환 중: {isConverting ? "예" : "아니오"}</div>
|
<div>변환 중: {isConverting ? "예" : "아니오"}</div>
|
||||||
<div>초기화: {isInitialized ? "완료" : "대기"}</div>
|
<div>초기화: {isInitialized ? "완료" : "대기"}</div>
|
||||||
<div>컨테이너 준비: {isContainerReady ? "완료" : "대기"}</div>
|
<div>컨테이너 준비: {isContainerReady ? "완료" : "대기"}</div>
|
||||||
|
<div>방식: LuckyExcel 직접 변환</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
174
src/components/sheet/TestSheetViewer.tsx
Normal file
174
src/components/sheet/TestSheetViewer.tsx
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import React, { useRef, useEffect, useState } from "react";
|
||||||
|
import { Univer, UniverInstanceType, LocaleType } from "@univerjs/core";
|
||||||
|
import { defaultTheme } from "@univerjs/design";
|
||||||
|
import { UniverDocsPlugin } from "@univerjs/docs";
|
||||||
|
import { UniverDocsUIPlugin } from "@univerjs/docs-ui";
|
||||||
|
import { UniverFormulaEnginePlugin } from "@univerjs/engine-formula";
|
||||||
|
import { UniverRenderEnginePlugin } from "@univerjs/engine-render";
|
||||||
|
import { UniverSheetsPlugin } from "@univerjs/sheets";
|
||||||
|
import { UniverSheetsFormulaPlugin } from "@univerjs/sheets-formula";
|
||||||
|
import { UniverSheetsFormulaUIPlugin } from "@univerjs/sheets-formula-ui";
|
||||||
|
import { UniverSheetsUIPlugin } from "@univerjs/sheets-ui";
|
||||||
|
import { UniverSheetsNumfmtPlugin } from "@univerjs/sheets-numfmt";
|
||||||
|
import { UniverSheetsNumfmtUIPlugin } from "@univerjs/sheets-numfmt-ui";
|
||||||
|
import { UniverUIPlugin } from "@univerjs/ui";
|
||||||
|
|
||||||
|
// 언어팩 import
|
||||||
|
import DesignEnUS from "@univerjs/design/locale/en-US";
|
||||||
|
import UIEnUS from "@univerjs/ui/locale/en-US";
|
||||||
|
import DocsUIEnUS from "@univerjs/docs-ui/locale/en-US";
|
||||||
|
import SheetsEnUS from "@univerjs/sheets/locale/en-US";
|
||||||
|
import SheetsUIEnUS from "@univerjs/sheets-ui/locale/en-US";
|
||||||
|
import SheetsFormulaUIEnUS from "@univerjs/sheets-formula-ui/locale/en-US";
|
||||||
|
import SheetsNumfmtUIEnUS from "@univerjs/sheets-numfmt-ui/locale/en-US";
|
||||||
|
|
||||||
|
// CSS 스타일 import
|
||||||
|
import "@univerjs/design/lib/index.css";
|
||||||
|
import "@univerjs/ui/lib/index.css";
|
||||||
|
import "@univerjs/docs-ui/lib/index.css";
|
||||||
|
import "@univerjs/sheets-ui/lib/index.css";
|
||||||
|
import "@univerjs/sheets-formula-ui/lib/index.css";
|
||||||
|
import "@univerjs/sheets-numfmt-ui/lib/index.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Univer CE 최소 구현 - 공식 문서 기반
|
||||||
|
* 파일 업로드 없이 기본 스프레드시트만 표시
|
||||||
|
*/
|
||||||
|
const TestSheetViewer: React.FC = () => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const univerRef = useRef<Univer | null>(null);
|
||||||
|
const initializingRef = useRef<boolean>(false);
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
|
||||||
|
// Univer 초기화 - 공식 문서 패턴 따라서
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!containerRef.current ||
|
||||||
|
isInitialized ||
|
||||||
|
univerRef.current ||
|
||||||
|
initializingRef.current
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const initializeUniver = async () => {
|
||||||
|
try {
|
||||||
|
initializingRef.current = true;
|
||||||
|
console.log("🚀 Univer CE 초기화 시작");
|
||||||
|
|
||||||
|
// 1. Univer 인스턴스 생성
|
||||||
|
const univer = new Univer({
|
||||||
|
theme: defaultTheme,
|
||||||
|
locale: LocaleType.EN_US,
|
||||||
|
locales: {
|
||||||
|
[LocaleType.EN_US]: {
|
||||||
|
...DesignEnUS,
|
||||||
|
...UIEnUS,
|
||||||
|
...DocsUIEnUS,
|
||||||
|
...SheetsEnUS,
|
||||||
|
...SheetsUIEnUS,
|
||||||
|
...SheetsFormulaUIEnUS,
|
||||||
|
...SheetsNumfmtUIEnUS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 필수 플러그인 등록 (공식 문서 순서)
|
||||||
|
univer.registerPlugin(UniverRenderEnginePlugin);
|
||||||
|
univer.registerPlugin(UniverFormulaEnginePlugin);
|
||||||
|
|
||||||
|
univer.registerPlugin(UniverUIPlugin, {
|
||||||
|
container: containerRef.current!,
|
||||||
|
});
|
||||||
|
|
||||||
|
univer.registerPlugin(UniverDocsPlugin);
|
||||||
|
univer.registerPlugin(UniverDocsUIPlugin);
|
||||||
|
|
||||||
|
univer.registerPlugin(UniverSheetsPlugin);
|
||||||
|
univer.registerPlugin(UniverSheetsUIPlugin);
|
||||||
|
univer.registerPlugin(UniverSheetsFormulaPlugin);
|
||||||
|
univer.registerPlugin(UniverSheetsFormulaUIPlugin);
|
||||||
|
univer.registerPlugin(UniverSheetsNumfmtPlugin);
|
||||||
|
univer.registerPlugin(UniverSheetsNumfmtUIPlugin);
|
||||||
|
|
||||||
|
// 3. 기본 워크북 생성
|
||||||
|
univer.createUnit(UniverInstanceType.UNIVER_SHEET, {
|
||||||
|
id: "test-workbook",
|
||||||
|
name: "Test Workbook",
|
||||||
|
sheetOrder: ["sheet1"],
|
||||||
|
sheets: {
|
||||||
|
sheet1: {
|
||||||
|
id: "sheet1",
|
||||||
|
name: "Sheet1",
|
||||||
|
cellData: {
|
||||||
|
0: {
|
||||||
|
0: { v: "Hello Univer CE!" },
|
||||||
|
1: { v: "환영합니다!" },
|
||||||
|
},
|
||||||
|
1: {
|
||||||
|
0: { v: "Status" },
|
||||||
|
1: { v: "Ready" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowCount: 100,
|
||||||
|
columnCount: 26,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
univerRef.current = univer;
|
||||||
|
setIsInitialized(true);
|
||||||
|
|
||||||
|
console.log("✅ Univer CE 초기화 완료");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Univer CE 초기화 실패:", error);
|
||||||
|
initializingRef.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializeUniver();
|
||||||
|
}, [isInitialized]);
|
||||||
|
|
||||||
|
// 컴포넌트 언마운트 시 정리
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
try {
|
||||||
|
if (univerRef.current) {
|
||||||
|
univerRef.current.dispose();
|
||||||
|
univerRef.current = null;
|
||||||
|
}
|
||||||
|
initializingRef.current = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Univer dispose 오류:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-screen flex flex-col">
|
||||||
|
{/* 간단한 헤더 */}
|
||||||
|
<div className="bg-white border-b p-4 flex-shrink-0">
|
||||||
|
<h1 className="text-xl font-bold">🧪 Univer CE 최소 테스트</h1>
|
||||||
|
<div className="mt-2">
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
||||||
|
isInitialized
|
||||||
|
? "bg-green-100 text-green-800"
|
||||||
|
: "bg-yellow-100 text-yellow-800"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isInitialized ? "✅ 초기화 완료" : "⏳ 초기화 중..."}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Univer 컨테이너 */}
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="flex-1"
|
||||||
|
style={{ minHeight: "500px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TestSheetViewer;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
// import React from "react";
|
||||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
@@ -29,7 +29,7 @@ class MockDragEvent extends Event {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
global.DragEvent = MockDragEvent;
|
global.DragEvent = MockDragEvent;
|
||||||
|
|
||||||
const mockUseAppStore = useAppStore as vi.MockedFunction<typeof useAppStore>;
|
const mockUseAppStore = useAppStore as any;
|
||||||
|
|
||||||
describe("FileUpload", () => {
|
describe("FileUpload", () => {
|
||||||
const mockSetLoading = vi.fn();
|
const mockSetLoading = vi.fn();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
// import React from "react";
|
||||||
import { render, screen, waitFor } from "@testing-library/react";
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|||||||
165
src/index.css
165
src/index.css
@@ -1,3 +1,12 @@
|
|||||||
|
/* Univer CE 공식 스타일 - @import는 맨 위에 */
|
||||||
|
@import '@univerjs/design/lib/index.css';
|
||||||
|
@import '@univerjs/ui/lib/index.css';
|
||||||
|
@import '@univerjs/docs-ui/lib/index.css';
|
||||||
|
@import '@univerjs/sheets-ui/lib/index.css';
|
||||||
|
@import '@univerjs/sheets-formula-ui/lib/index.css';
|
||||||
|
@import '@univerjs/sheets-numfmt-ui/lib/index.css';
|
||||||
|
|
||||||
|
/* Tailwind CSS */
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
@@ -105,25 +114,29 @@
|
|||||||
.lg\:px-8 { padding-left: 2rem; padding-right: 2rem; }
|
.lg\:px-8 { padding-left: 2rem; padding-right: 2rem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 커스텀 스타일 */
|
/* 전역 스타일 */
|
||||||
body {
|
html, body, #root {
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
height: 100%;
|
||||||
line-height: 1.5;
|
margin: 0;
|
||||||
font-weight: 400;
|
padding: 0;
|
||||||
color-scheme: light;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
color: #1f2937; /* 검은색 계열로 변경 */
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
background-color: #ffffff;
|
sans-serif;
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 스크롤바 스타일링 */
|
/* Univer 컨테이너 스타일 */
|
||||||
|
.univer-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 커스텀 스크롤바 (Univer와 일치) */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 8px;
|
||||||
height: 6px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
@@ -132,20 +145,132 @@ body {
|
|||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #c1c1c1;
|
background: #c1c1c1;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #a1a1a1;
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 파일 업로드 영역 스타일 */
|
||||||
|
.file-upload-area {
|
||||||
|
border: 2px dashed #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload-area:hover {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload-area.dragover {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
background-color: #dbeafe;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 로딩 애니메이션 */
|
/* 로딩 애니메이션 */
|
||||||
|
.loading-spinner {
|
||||||
|
border: 2px solid #f3f3f3;
|
||||||
|
border-top: 2px solid #3b82f6;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to {
|
0% { transform: rotate(0deg); }
|
||||||
transform: rotate(360deg);
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 상태 표시 점 애니메이션 */
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.pulse {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-spin {
|
/* 에러 메시지 스타일 */
|
||||||
animation: spin 1s linear infinite;
|
.error-message {
|
||||||
|
background-color: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
color: #dc2626;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 성공 메시지 스타일 */
|
||||||
|
.success-message {
|
||||||
|
background-color: #f0fdf4;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
color: #16a34a;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 정보 메시지 스타일 */
|
||||||
|
.info-message {
|
||||||
|
background-color: #eff6ff;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
color: #2563eb;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 반응형 디자인 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.file-upload-area {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.univer-container {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 다크 모드 지원 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.file-upload-area {
|
||||||
|
border-color: #374151;
|
||||||
|
background-color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload-area:hover {
|
||||||
|
border-color: #60a5fa;
|
||||||
|
background-color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload-area.dragover {
|
||||||
|
border-color: #60a5fa;
|
||||||
|
background-color: #1e3a8a;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/main.tsx
13
src/main.tsx
@@ -1,10 +1,5 @@
|
|||||||
import { StrictMode } from 'react'
|
import { createRoot } from "react-dom/client";
|
||||||
import { createRoot } from 'react-dom/client'
|
import "./index.css";
|
||||||
import './index.css'
|
import App from "./App.tsx";
|
||||||
import App from './App.tsx'
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById("root")!).render(<App />);
|
||||||
<StrictMode>
|
|
||||||
<App />
|
|
||||||
</StrictMode>,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
import * as XLSX from "xlsx";
|
import * as XLSX from "xlsx-js-style";
|
||||||
import {
|
import {
|
||||||
validateFileType,
|
validateFileType,
|
||||||
validateFileSize,
|
validateFileSize,
|
||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
SUPPORTED_EXTENSIONS,
|
SUPPORTED_EXTENSIONS,
|
||||||
} from "../fileProcessor";
|
} from "../fileProcessor";
|
||||||
|
|
||||||
// SheetJS 모킹 (통합 처리)
|
// xlsx-js-style 모킹 (통합 처리)
|
||||||
vi.mock("xlsx", () => ({
|
vi.mock("xlsx-js-style", () => ({
|
||||||
read: vi.fn(() => ({
|
read: vi.fn(() => ({
|
||||||
SheetNames: ["Sheet1"],
|
SheetNames: ["Sheet1"],
|
||||||
Sheets: {
|
Sheets: {
|
||||||
@@ -30,12 +30,28 @@ vi.mock("xlsx", () => ({
|
|||||||
["테스트", "한글", "데이터"],
|
["테스트", "한글", "데이터"],
|
||||||
["값1", "값2", "값3"],
|
["값1", "값2", "값3"],
|
||||||
]),
|
]),
|
||||||
|
decode_range: vi.fn((_ref) => ({
|
||||||
|
s: { r: 0, c: 0 },
|
||||||
|
e: { r: 1, c: 2 },
|
||||||
|
})),
|
||||||
|
encode_cell: vi.fn(
|
||||||
|
(cell) => `${String.fromCharCode(65 + cell.c)}${cell.r + 1}`,
|
||||||
|
),
|
||||||
|
aoa_to_sheet: vi.fn(() => ({
|
||||||
|
A1: { v: "테스트" },
|
||||||
|
B1: { v: "한글" },
|
||||||
|
C1: { v: "데이터" },
|
||||||
|
"!ref": "A1:C1",
|
||||||
|
})),
|
||||||
|
book_new: vi.fn(() => ({ SheetNames: [], Sheets: {} })),
|
||||||
|
book_append_sheet: vi.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// LuckyExcel 모킹
|
// LuckyExcel 모킹
|
||||||
vi.mock("luckyexcel", () => ({
|
vi.mock("luckyexcel", () => ({
|
||||||
transformExcelToLucky: vi.fn((arrayBuffer, fileName, callback) => {
|
transformExcelToLucky: vi.fn(
|
||||||
|
(_arrayBuffer, successCallback, _errorCallback) => {
|
||||||
// 성공적인 변환 결과 모킹
|
// 성공적인 변환 결과 모킹
|
||||||
const mockResult = {
|
const mockResult = {
|
||||||
sheets: [
|
sheets: [
|
||||||
@@ -82,9 +98,12 @@ vi.mock("luckyexcel", () => ({
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 비동기 콜백 호출
|
// 성공 콜백 비동기 호출 (ArrayBuffer 매개변수 대응)
|
||||||
setTimeout(() => callback(mockResult, null), 0);
|
if (typeof successCallback === "function") {
|
||||||
}),
|
setTimeout(() => successCallback(mockResult, null), 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 파일 생성 도우미 함수
|
// 파일 생성 도우미 함수
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import * as XLSX from "xlsx";
|
import * as XLSX from "xlsx-js-style";
|
||||||
import * as LuckyExcel from "luckyexcel";
|
|
||||||
import type { SheetData, FileUploadResult } from "../types/sheet";
|
import type { SheetData, FileUploadResult } from "../types/sheet";
|
||||||
|
import { analyzeSheetStyles } from "./styleTest";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 파일 처리 관련 유틸리티 - 개선된 버전
|
* 파일 처리 관련 유틸리티 - xlsx-js-style 공식 API 활용 버전
|
||||||
* - 모든 파일 형식을 SheetJS를 통해 읽은 후 XLSX로 변환
|
* - 모든 파일 형식을 SheetJS를 통해 읽은 후 XLSX로 변환
|
||||||
* - 변환된 XLSX 파일을 LuckyExcel로 전달
|
* - 변환된 XLSX 파일을 LuckyExcel로 전달
|
||||||
* - 안정적인 한글 지원 및 에러 처리
|
* - xlsx-js-style의 공식 스타일 구조를 그대로 활용
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 지원되는 파일 타입
|
// 지원되는 파일 타입
|
||||||
@@ -21,6 +21,294 @@ export const SUPPORTED_EXTENSIONS = [".xlsx", ".xls", ".csv"] as const;
|
|||||||
// 최대 파일 크기 (50MB)
|
// 최대 파일 크기 (50MB)
|
||||||
export const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 파일 타입 검증
|
* 파일 타입 검증
|
||||||
*/
|
*/
|
||||||
@@ -99,7 +387,7 @@ function sanitizeSheetName(sheetName: string): string {
|
|||||||
|
|
||||||
sanitized = sanitized.replace(/[\\\/\*\?\[\]]/g, "_");
|
sanitized = sanitized.replace(/[\\\/\*\?\[\]]/g, "_");
|
||||||
|
|
||||||
return sanitized || "Sheet1";
|
return sanitized || "Sheet";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -176,9 +464,9 @@ function convertSheetJSToLuckyExcel(workbook: any): SheetData[] {
|
|||||||
],
|
],
|
||||||
options: {
|
options: {
|
||||||
showtoolbar: true,
|
showtoolbar: true,
|
||||||
showinfobar: false,
|
showinfobar: true,
|
||||||
showsheetbar: true,
|
showsheetbar: true,
|
||||||
showstatisticBar: false,
|
showstatisticBar: true,
|
||||||
allowCopy: true,
|
allowCopy: true,
|
||||||
allowEdit: true,
|
allowEdit: true,
|
||||||
enableAddRow: true,
|
enableAddRow: true,
|
||||||
@@ -218,9 +506,9 @@ function convertSheetJSToLuckyExcel(workbook: any): SheetData[] {
|
|||||||
],
|
],
|
||||||
options: {
|
options: {
|
||||||
showtoolbar: true,
|
showtoolbar: true,
|
||||||
showinfobar: false,
|
showinfobar: true,
|
||||||
showsheetbar: true,
|
showsheetbar: true,
|
||||||
showstatisticBar: false,
|
showstatisticBar: true,
|
||||||
allowCopy: true,
|
allowCopy: true,
|
||||||
allowEdit: true,
|
allowEdit: true,
|
||||||
enableAddRow: true,
|
enableAddRow: true,
|
||||||
@@ -276,22 +564,45 @@ function convertSheetJSToLuckyExcel(workbook: any): SheetData[] {
|
|||||||
c: col,
|
c: col,
|
||||||
v: {
|
v: {
|
||||||
v: cellValue,
|
v: cellValue,
|
||||||
m: String(cellValue),
|
m: cell.w || String(cellValue), // 포맷팅된 텍스트 우선 사용
|
||||||
ct: { fa: "General", t: "g" },
|
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") {
|
if (cell.t === "s") {
|
||||||
luckyCell.v.ct.t = "s";
|
luckyCell.v.ct.t = "s";
|
||||||
} else if (cell.t === "n") {
|
} else if (cell.t === "n") {
|
||||||
luckyCell.v.ct.t = "n";
|
luckyCell.v.ct.t = "n";
|
||||||
|
// 숫자 포맷 처리
|
||||||
|
if (cell.z) {
|
||||||
|
luckyCell.v.ct.fa = cell.z;
|
||||||
|
}
|
||||||
} else if (cell.t === "d") {
|
} else if (cell.t === "d") {
|
||||||
luckyCell.v.ct.t = "d";
|
luckyCell.v.ct.t = "d";
|
||||||
|
// 날짜 포맷 처리
|
||||||
|
if (cell.z) {
|
||||||
|
luckyCell.v.ct.fa = cell.z;
|
||||||
|
}
|
||||||
} else if (cell.t === "b") {
|
} else if (cell.t === "b") {
|
||||||
luckyCell.v.ct.t = "b";
|
luckyCell.v.ct.t = "b";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 수식 처리
|
||||||
if (cell.f) {
|
if (cell.f) {
|
||||||
luckyCell.v.f = cell.f;
|
luckyCell.v.f = cell.f;
|
||||||
}
|
}
|
||||||
@@ -301,6 +612,48 @@ function convertSheetJSToLuckyExcel(workbook: any): SheetData[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔗 병합 셀 정보 처리
|
||||||
|
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 객체 생성
|
// SheetData 객체 생성
|
||||||
const sheetData: SheetData = {
|
const sheetData: SheetData = {
|
||||||
id: `sheet_${index}`,
|
id: `sheet_${index}`,
|
||||||
@@ -319,13 +672,17 @@ function convertSheetJSToLuckyExcel(workbook: any): SheetData[] {
|
|||||||
order: index,
|
order: index,
|
||||||
row: maxRow,
|
row: maxRow,
|
||||||
column: maxCol,
|
column: maxCol,
|
||||||
|
// 🎨 xlsx-js-style로부터 추가된 스타일 정보들
|
||||||
|
...(mergeData.length > 0 && { merge: mergeData }), // 병합 셀
|
||||||
|
...(Object.keys(colhidden).length > 0 && { colhidden }), // 숨겨진 열
|
||||||
|
...(Object.keys(rowhidden).length > 0 && { rowhidden }), // 숨겨진 행
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
options: {
|
options: {
|
||||||
showtoolbar: true,
|
showtoolbar: true,
|
||||||
showinfobar: false,
|
showinfobar: true,
|
||||||
showsheetbar: true,
|
showsheetbar: true,
|
||||||
showstatisticBar: false,
|
showstatisticBar: true,
|
||||||
allowCopy: true,
|
allowCopy: true,
|
||||||
allowEdit: true,
|
allowEdit: true,
|
||||||
enableAddRow: true,
|
enableAddRow: true,
|
||||||
@@ -361,9 +718,9 @@ function convertSheetJSToLuckyExcel(workbook: any): SheetData[] {
|
|||||||
],
|
],
|
||||||
options: {
|
options: {
|
||||||
showtoolbar: true,
|
showtoolbar: true,
|
||||||
showinfobar: false,
|
showinfobar: true,
|
||||||
showsheetbar: true,
|
showsheetbar: true,
|
||||||
showstatisticBar: false,
|
showstatisticBar: true,
|
||||||
allowCopy: true,
|
allowCopy: true,
|
||||||
allowEdit: true,
|
allowEdit: true,
|
||||||
enableAddRow: true,
|
enableAddRow: true,
|
||||||
@@ -428,7 +785,7 @@ async function processFileWithSheetJSToXLSX(
|
|||||||
const fileName = file.name.toLowerCase();
|
const fileName = file.name.toLowerCase();
|
||||||
const isCSV = fileName.endsWith(".csv");
|
const isCSV = fileName.endsWith(".csv");
|
||||||
const isXLS = fileName.endsWith(".xls");
|
const isXLS = fileName.endsWith(".xls");
|
||||||
const isXLSX = fileName.endsWith(".xlsx");
|
// const isXLSX = fileName.endsWith(".xlsx");
|
||||||
|
|
||||||
// 1단계: SheetJS로 파일 읽기
|
// 1단계: SheetJS로 파일 읽기
|
||||||
let workbook: any;
|
let workbook: any;
|
||||||
@@ -444,11 +801,14 @@ async function processFileWithSheetJSToXLSX(
|
|||||||
raw: false,
|
raw: false,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// XLS/XLSX 파일 처리 - 관대한 옵션으로 읽기
|
// XLS/XLSX 파일 처리 - 스타일 정보 강제 추출 옵션
|
||||||
console.log(`📊 ${isXLS ? "XLS" : "XLSX"} 파일을 SheetJS로 읽는 중...`);
|
console.log(`📊 ${isXLS ? "XLS" : "XLSX"} 파일을 SheetJS로 읽는 중...`);
|
||||||
workbook = XLSX.read(arrayBuffer);
|
workbook = XLSX.read(arrayBuffer, {
|
||||||
|
cellStyles: true, // 스타일 정보 보존
|
||||||
// Sheets가 없고 SheetNames만 있는 경우 재시도
|
cellNF: true, // 숫자 형식 보존 (스타일의 일부)
|
||||||
|
bookProps: true, // 문서 속성 보존 (스타일 정보 포함 가능)
|
||||||
|
WTF: true, // 더 관대한 파싱
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (readError) {
|
} catch (readError) {
|
||||||
console.error("❌ SheetJS 파일 읽기 실패:", readError);
|
console.error("❌ SheetJS 파일 읽기 실패:", readError);
|
||||||
@@ -467,14 +827,14 @@ async function processFileWithSheetJSToXLSX(
|
|||||||
throw new Error("워크북을 생성할 수 없습니다.");
|
throw new Error("워크북을 생성할 수 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// workbook.Sheets 존재 및 타입 검증
|
// 기본 검증만 수행
|
||||||
if (!workbook.Sheets || typeof workbook.Sheets !== "object") {
|
if (!workbook.SheetNames || workbook.SheetNames.length === 0) {
|
||||||
throw new Error("유효한 시트가 없습니다.");
|
throw new Error("시트 이름 정보가 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// workbook.SheetNames 배열 검증
|
// Sheets 객체가 없으면 빈 객체로 초기화
|
||||||
if (!Array.isArray(workbook.SheetNames) || workbook.SheetNames.length === 0) {
|
if (!workbook.Sheets) {
|
||||||
throw new Error("시트 이름 정보가 없습니다.");
|
workbook.Sheets = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("✅ SheetJS 워크북 읽기 성공:", {
|
console.log("✅ SheetJS 워크북 읽기 성공:", {
|
||||||
@@ -482,6 +842,11 @@ async function processFileWithSheetJSToXLSX(
|
|||||||
sheetCount: workbook.SheetNames.length,
|
sheetCount: workbook.SheetNames.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🎨 스타일 정보 상세 분석 (개발 모드에서만)
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
analyzeSheetStyles(workbook);
|
||||||
|
}
|
||||||
|
|
||||||
// 2단계: 워크북을 XLSX ArrayBuffer로 변환
|
// 2단계: 워크북을 XLSX ArrayBuffer로 변환
|
||||||
let xlsxArrayBuffer: ArrayBuffer;
|
let xlsxArrayBuffer: ArrayBuffer;
|
||||||
try {
|
try {
|
||||||
@@ -489,83 +854,10 @@ async function processFileWithSheetJSToXLSX(
|
|||||||
const xlsxData = XLSX.write(workbook, {
|
const xlsxData = XLSX.write(workbook, {
|
||||||
type: "array",
|
type: "array",
|
||||||
bookType: "xlsx",
|
bookType: "xlsx",
|
||||||
compression: true,
|
cellStyles: true, // 스타일 정보 보존
|
||||||
|
|
||||||
// 🎨 스타일 정보 완전 보존
|
|
||||||
cellStyles: true, // 셀 스타일, 색상, 폰트, 테두리 등
|
|
||||||
cellDates: true, // 날짜 포맷 정보
|
|
||||||
bookSST: true, // 문자열 테이블 (호환성)
|
|
||||||
|
|
||||||
// 📊 워크북 정보 보존
|
|
||||||
Props: workbook.Props || {},
|
|
||||||
|
|
||||||
// 🎭 테마 정보 보존 (존재하는 경우)
|
|
||||||
...(workbook.themeXLSX && { themeXLSX: workbook.themeXLSX }),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 📋 XLSX.write 완료 후 workbook 상세 정보 로깅
|
console.log(`✅ XLSX 변환 완료: ${xlsxData.length} bytes`);
|
||||||
console.log("📋 =================================");
|
|
||||||
console.log("📋 XLSX.write 완료 후 Workbook 분석:");
|
|
||||||
console.log("📋 =================================");
|
|
||||||
console.log("📋 Workbook 타입:", typeof workbook);
|
|
||||||
console.log("📋 Workbook 생성자:", workbook.constructor.name);
|
|
||||||
console.log("📋 시트 이름들:", workbook.SheetNames);
|
|
||||||
console.log("📋 시트 개수:", workbook.SheetNames?.length || 0);
|
|
||||||
|
|
||||||
// Props 정보
|
|
||||||
console.log("📋 Props 존재:", !!workbook.Props);
|
|
||||||
if (workbook.Props) {
|
|
||||||
console.log("📋 Props 내용:", workbook.Props);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 테마 정보
|
|
||||||
console.log("📋 themeXLSX 존재:", !!workbook.themeXLSX);
|
|
||||||
|
|
||||||
// 각 시트별 상세 정보
|
|
||||||
if (workbook.Sheets && workbook.SheetNames) {
|
|
||||||
workbook.SheetNames.forEach((sheetName: string, index: number) => {
|
|
||||||
const sheet = workbook.Sheets[sheetName];
|
|
||||||
console.log(`📋 시트 ${index + 1} "${sheetName}" 분석:`);
|
|
||||||
console.log(`📋 - 시트 객체 존재:`, !!sheet);
|
|
||||||
|
|
||||||
if (sheet) {
|
|
||||||
console.log(`📋 - 데이터 범위 (!ref):`, sheet["!ref"]);
|
|
||||||
console.log(
|
|
||||||
`📋 - 병합 셀 (!merges):`,
|
|
||||||
sheet["!merges"]?.length || 0,
|
|
||||||
"개",
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`📋 - 열 설정 (!cols):`,
|
|
||||||
sheet["!cols"]?.length || 0,
|
|
||||||
"개",
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`📋 - 행 설정 (!rows):`,
|
|
||||||
sheet["!rows"]?.length || 0,
|
|
||||||
"개",
|
|
||||||
);
|
|
||||||
|
|
||||||
// 셀 샘플 확인 (첫 몇 개 셀)
|
|
||||||
const cellKeys = Object.keys(sheet)
|
|
||||||
.filter((key) => !key.startsWith("!"))
|
|
||||||
.slice(0, 5);
|
|
||||||
console.log(`📋 - 셀 샘플 (첫 5개):`, cellKeys);
|
|
||||||
|
|
||||||
cellKeys.forEach((cellAddr) => {
|
|
||||||
const cell = sheet[cellAddr];
|
|
||||||
console.log(`📋 ${cellAddr}:`, {
|
|
||||||
값: cell.v,
|
|
||||||
타입: cell.t,
|
|
||||||
스타일: !!cell.s,
|
|
||||||
수식: cell.f || "없음",
|
|
||||||
포맷된텍스트: cell.w || "없음",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("📋 =================================");
|
|
||||||
|
|
||||||
// xlsxData는 Uint8Array이므로 ArrayBuffer로 변환
|
// xlsxData는 Uint8Array이므로 ArrayBuffer로 변환
|
||||||
if (xlsxData instanceof Uint8Array) {
|
if (xlsxData instanceof Uint8Array) {
|
||||||
@@ -585,30 +877,6 @@ async function processFileWithSheetJSToXLSX(
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✅ XLSX 변환 완료: ${xlsxArrayBuffer.byteLength} bytes`);
|
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) {
|
} catch (writeError) {
|
||||||
console.error("❌ XLSX 변환 실패:", writeError);
|
console.error("❌ XLSX 변환 실패:", writeError);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -629,53 +897,8 @@ async function processFileWithSheetJSToXLSX(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 원본 파일명에서 확장자를 .xlsx로 변경
|
// 원본 파일명에서 확장자를 .xlsx로 변경
|
||||||
const xlsxFileName = file.name.replace(/\.(csv|xls|xlsx)$/i, ".xlsx");
|
// const xlsxFileName = file.name.replace(/\.(csv|xls|xlsx)$/i, ".xlsx");
|
||||||
|
console.log("🍀 LuckyExcel 처리 시작...");
|
||||||
// 🔍 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 처리
|
// Promise를 사용한 LuckyExcel 처리
|
||||||
return new Promise<{ sheets: SheetData[]; xlsxBuffer: ArrayBuffer }>(
|
return new Promise<{ sheets: SheetData[]; xlsxBuffer: ArrayBuffer }>(
|
||||||
@@ -683,56 +906,16 @@ async function processFileWithSheetJSToXLSX(
|
|||||||
try {
|
try {
|
||||||
// LuckyExcel API는 (arrayBuffer, successCallback, errorCallback) 형태로 호출
|
// LuckyExcel API는 (arrayBuffer, successCallback, errorCallback) 형태로 호출
|
||||||
// 공식 문서: LuckyExcel.transformExcelToLucky(file, successCallback, errorCallback)
|
// 공식 문서: LuckyExcel.transformExcelToLucky(file, successCallback, errorCallback)
|
||||||
(LuckyExcel as any).transformExcelToLucky(
|
(window.LuckyExcel as any).transformExcelToLucky(
|
||||||
xlsxArrayBuffer,
|
xlsxArrayBuffer,
|
||||||
// 성공 콜백 함수 (두 번째 매개변수)
|
// 성공 콜백 함수 (두 번째 매개변수)
|
||||||
(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(
|
console.log(
|
||||||
"🍀 exportJson.sheets 타입:",
|
"🍀 LuckyExcel 변환 성공:",
|
||||||
typeof exportJson.sheets,
|
exportJson?.sheets?.length || 0,
|
||||||
|
"개 시트",
|
||||||
);
|
);
|
||||||
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,
|
|
||||||
sheetsCount: exportJson?.sheets?.length || 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 데이터 유효성 검사
|
// 데이터 유효성 검사
|
||||||
if (
|
if (
|
||||||
@@ -818,7 +1001,7 @@ async function processFileWithSheetJSToXLSX(
|
|||||||
showtoolbar: true,
|
showtoolbar: true,
|
||||||
showinfobar: false,
|
showinfobar: false,
|
||||||
showsheetbar: true,
|
showsheetbar: true,
|
||||||
showstatisticBar: false,
|
showstatisticBar: true,
|
||||||
allowCopy: true,
|
allowCopy: true,
|
||||||
allowEdit: true,
|
allowEdit: true,
|
||||||
enableAddRow: true,
|
enableAddRow: true,
|
||||||
@@ -881,10 +1064,92 @@ async function processFileWithSheetJSToXLSX(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 엑셀 파일을 SheetData 배열로 변환 (개선된 버전)
|
* XLSX 파일을 바로 LuckyExcel로 처리 (공식 예제 순서 준수)
|
||||||
* - 모든 파일을 SheetJS로 읽은 후 XLSX로 변환
|
* - 공식 문서 예제를 그대로 따름: LuckyExcel.transformExcelToLucky → luckysheet.create
|
||||||
* - 변환된 XLSX를 LuckyExcel로 처리
|
*/
|
||||||
* - 실패 시 SheetJS 직접 변환으로 Fallback
|
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> {
|
export async function processExcelFile(file: File): Promise<FileUploadResult> {
|
||||||
try {
|
try {
|
||||||
@@ -920,8 +1185,22 @@ export async function processExcelFile(file: File): Promise<FileUploadResult> {
|
|||||||
`📁 파일 처리 시작: ${file.name} (${isCSV ? "CSV" : isXLS ? "XLS" : "XLSX"})`,
|
`📁 파일 처리 시작: ${file.name} (${isCSV ? "CSV" : isXLS ? "XLS" : "XLSX"})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 통합된 처리 방식: SheetJS → XLSX → LuckyExcel
|
let sheets: SheetData[];
|
||||||
const { sheets, xlsxBuffer } = await processFileWithSheetJSToXLSX(file);
|
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) {
|
if (!sheets || sheets.length === 0) {
|
||||||
return {
|
return {
|
||||||
@@ -959,7 +1238,9 @@ export async function processExcelFile(file: File): Promise<FileUploadResult> {
|
|||||||
error.message.includes("워크북을 생성할 수 없습니다") ||
|
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("XLSX 파일 변환 실패")
|
||||||
) {
|
) {
|
||||||
errorMessage = error.message;
|
errorMessage = error.message;
|
||||||
} else if (error.message.includes("transformExcelToLucky")) {
|
} else if (error.message.includes("transformExcelToLucky")) {
|
||||||
417
src/utils/styleTest.ts.bak
Normal file
417
src/utils/styleTest.ts.bak
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
/**
|
||||||
|
* xlsx-js-style 스타일 보존 테스트 유틸리티
|
||||||
|
* - 다양한 스타일이 적용된 Excel 파일 생성
|
||||||
|
* - 스타일 정보 확인 도구
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as XLSX from "xlsx-js-style";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스타일이 적용된 테스트 Excel 파일 생성
|
||||||
|
*/
|
||||||
|
export function createStyledTestExcel(): ArrayBuffer {
|
||||||
|
// 새 워크북 생성
|
||||||
|
const wb = XLSX.utils.book_new();
|
||||||
|
|
||||||
|
// 테스트 데이터 생성 - xlsx-js-style 공식 API 완전 활용
|
||||||
|
const testData = [
|
||||||
|
// 첫 번째 행 - 폰트 스타일 테스트
|
||||||
|
[
|
||||||
|
{
|
||||||
|
v: "굵은 글씨",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
font: {
|
||||||
|
name: "Courier", // 공식 문서 예시
|
||||||
|
sz: 24, // 공식 문서 예시
|
||||||
|
bold: true,
|
||||||
|
color: { rgb: "000000" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: "빨간 글씨",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
font: {
|
||||||
|
bold: true,
|
||||||
|
color: { rgb: "FF0000" }, // 공식 문서: {color: {rgb: "FF0000"}}
|
||||||
|
sz: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: "테마 색상",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
font: {
|
||||||
|
color: { theme: 4 }, // 공식 문서: {theme: 4} (Blue, Accent 1)
|
||||||
|
sz: 14,
|
||||||
|
italic: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 두 번째 행 - 배경색 테스트
|
||||||
|
[
|
||||||
|
{
|
||||||
|
v: "노란 배경",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
fill: {
|
||||||
|
patternType: "solid",
|
||||||
|
fgColor: { rgb: "FFFF00" }, // 공식 문서: {fgColor: {rgb: "E9E9E9"}}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: "테마 배경",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
fill: {
|
||||||
|
patternType: "solid",
|
||||||
|
fgColor: { theme: 1, tint: 0.4 }, // 공식 문서: {theme: 1, tint: 0.4} ("Blue, Accent 1, Lighter 40%")
|
||||||
|
},
|
||||||
|
font: { color: { rgb: "000000" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: "인덱스 색상",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
fill: {
|
||||||
|
patternType: "solid",
|
||||||
|
fgColor: { indexed: 5 }, // Excel 기본 색상표 - 노랑
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 세 번째 행 - 테두리 테스트
|
||||||
|
[
|
||||||
|
{
|
||||||
|
v: "얇은 테두리",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
border: {
|
||||||
|
top: { style: "thin", color: { rgb: "000000" } },
|
||||||
|
bottom: { style: "thin", color: { rgb: "000000" } },
|
||||||
|
left: { style: "thin", color: { rgb: "000000" } },
|
||||||
|
right: { style: "thin", color: { rgb: "000000" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: "두꺼운 테두리",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
border: {
|
||||||
|
top: { style: "thick", color: { theme: 2 } }, // 테마 색상 사용
|
||||||
|
bottom: { style: "thick", color: { theme: 2 } },
|
||||||
|
left: { style: "thick", color: { theme: 2 } },
|
||||||
|
right: { style: "thick", color: { theme: 2 } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: "다양한 테두리",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
border: {
|
||||||
|
top: { style: "dotted", color: { indexed: 4 } }, // 인덱스 색상 - 파랑
|
||||||
|
bottom: { style: "dashed", color: { indexed: 4 } },
|
||||||
|
left: { style: "dashDot", color: { indexed: 4 } },
|
||||||
|
right: { style: "double", color: { indexed: 4 } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 네 번째 행 - 복합 스타일 테스트
|
||||||
|
[
|
||||||
|
{
|
||||||
|
v: "복합 스타일",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
font: {
|
||||||
|
bold: true,
|
||||||
|
italic: true,
|
||||||
|
underline: true,
|
||||||
|
sz: 16,
|
||||||
|
color: { rgb: "FFFFFF" },
|
||||||
|
name: "Courier", // 공식 문서 예시
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
patternType: "solid",
|
||||||
|
fgColor: { theme: 7, tint: -0.2 }, // 어두운 보라색
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
top: { style: "medium", color: { rgb: "FFD700" } },
|
||||||
|
bottom: { style: "medium", color: { rgb: "FFD700" } },
|
||||||
|
left: { style: "medium", color: { rgb: "FFD700" } },
|
||||||
|
right: { style: "medium", color: { rgb: "FFD700" } },
|
||||||
|
},
|
||||||
|
alignment: {
|
||||||
|
horizontal: "center",
|
||||||
|
vertical: "middle",
|
||||||
|
wrapText: true, // 공식 문서: {wrapText: true}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: 1234.567,
|
||||||
|
t: "n",
|
||||||
|
s: {
|
||||||
|
numFmt: "0.00%", // 공식 문서: numFmt 예시
|
||||||
|
alignment: { horizontal: "right" },
|
||||||
|
font: { bold: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: "줄바꿈\n테스트",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
alignment: {
|
||||||
|
wrapText: true,
|
||||||
|
vertical: "top",
|
||||||
|
textRotation: 0, // 공식 문서: textRotation
|
||||||
|
},
|
||||||
|
font: { sz: 10 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 다섯 번째 행 - 고급 스타일 테스트
|
||||||
|
[
|
||||||
|
{
|
||||||
|
v: "취소선 텍스트",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
font: {
|
||||||
|
strike: true, // 공식 문서: {strike: true}
|
||||||
|
sz: 12,
|
||||||
|
color: { theme: 5, tint: 0.6 }, // 밝은 빨강
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: "회전 텍스트",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
alignment: {
|
||||||
|
textRotation: 45, // 공식 문서: textRotation
|
||||||
|
horizontal: "center",
|
||||||
|
vertical: "middle",
|
||||||
|
},
|
||||||
|
font: { sz: 14, bold: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: new Date(),
|
||||||
|
t: "d",
|
||||||
|
s: {
|
||||||
|
numFmt: "m/dd/yy", // 공식 문서: 날짜 포맷 예시
|
||||||
|
font: { name: "Arial", sz: 10 },
|
||||||
|
alignment: { horizontal: "center" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// 워크시트 생성
|
||||||
|
const ws = XLSX.utils.aoa_to_sheet(testData);
|
||||||
|
|
||||||
|
// 병합 셀 추가
|
||||||
|
if (!ws["!merges"]) ws["!merges"] = [];
|
||||||
|
ws["!merges"].push(
|
||||||
|
{ s: { r: 0, c: 0 }, e: { r: 0, c: 1 } }, // A1:B1 병합
|
||||||
|
{ s: { r: 2, c: 2 }, e: { r: 3, c: 2 } }, // C3:C4 병합
|
||||||
|
);
|
||||||
|
|
||||||
|
// 열 너비 설정
|
||||||
|
ws["!cols"] = [
|
||||||
|
{ wpx: 120 }, // A열 너비
|
||||||
|
{ wpx: 100 }, // B열 너비
|
||||||
|
{ wpx: 80 }, // C열 너비
|
||||||
|
];
|
||||||
|
|
||||||
|
// 행 높이 설정
|
||||||
|
ws["!rows"] = [
|
||||||
|
{ hpx: 30 }, // 1행 높이
|
||||||
|
{ hpx: 25 }, // 2행 높이
|
||||||
|
{ hpx: 40 }, // 3행 높이
|
||||||
|
];
|
||||||
|
|
||||||
|
// 워크시트를 워크북에 추가
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws, "스타일테스트");
|
||||||
|
|
||||||
|
// 추가 시트 생성 (간단한 데이터)
|
||||||
|
const simpleData = [
|
||||||
|
["이름", "나이", "직업"],
|
||||||
|
["홍길동", 30, "개발자"],
|
||||||
|
["김철수", 25, "디자이너"],
|
||||||
|
["이영희", 35, "기획자"],
|
||||||
|
];
|
||||||
|
|
||||||
|
const ws2 = XLSX.utils.aoa_to_sheet(simpleData);
|
||||||
|
|
||||||
|
// 헤더 스타일 적용
|
||||||
|
["A1", "B1", "C1"].forEach((cellAddr) => {
|
||||||
|
if (ws2[cellAddr]) {
|
||||||
|
ws2[cellAddr].s = {
|
||||||
|
font: { bold: true, color: { rgb: "FFFFFF" } },
|
||||||
|
fill: { patternType: "solid", fgColor: { rgb: "5B9BD5" } },
|
||||||
|
alignment: { horizontal: "center" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws2, "간단한데이터");
|
||||||
|
|
||||||
|
// Excel 파일로 변환
|
||||||
|
const excelBuffer = XLSX.write(wb, {
|
||||||
|
type: "array",
|
||||||
|
bookType: "xlsx",
|
||||||
|
cellStyles: true,
|
||||||
|
cellDates: true,
|
||||||
|
bookSST: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ArrayBuffer로 변환
|
||||||
|
if (excelBuffer instanceof Uint8Array) {
|
||||||
|
return excelBuffer.buffer.slice(
|
||||||
|
excelBuffer.byteOffset,
|
||||||
|
excelBuffer.byteOffset + excelBuffer.byteLength,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return excelBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 셀 스타일 정보 분석
|
||||||
|
*/
|
||||||
|
export function analyzeSheetStyles(workbook: any): void {
|
||||||
|
console.log("🎨 =================================");
|
||||||
|
console.log("🎨 Excel 파일 스타일 정보 분석");
|
||||||
|
console.log("🎨 =================================");
|
||||||
|
|
||||||
|
// 🔍 워크북 전체 스타일 정보 확인
|
||||||
|
console.log("🔍 워크북 메타데이터:", {
|
||||||
|
Props: workbook.Props ? "있음" : "없음",
|
||||||
|
Custprops: workbook.Custprops ? "있음" : "없음",
|
||||||
|
Workbook: workbook.Workbook ? "있음" : "없음",
|
||||||
|
SSF: workbook.SSF ? "있음" : "없음",
|
||||||
|
SheetNames: workbook.SheetNames
|
||||||
|
? workbook.SheetNames.length + "개"
|
||||||
|
: "없음",
|
||||||
|
Sheets: workbook.Sheets
|
||||||
|
? Object.keys(workbook.Sheets).length + "개"
|
||||||
|
: "없음",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔍 워크북 스타일 정보 상세 분석
|
||||||
|
if (workbook.SSF) {
|
||||||
|
console.log("🔍 워크북 SSF 스타일 정보:", workbook.SSF);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workbook.Workbook && workbook.Workbook.Styles) {
|
||||||
|
console.log("🔍 워크북 Styles:", workbook.Workbook.Styles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 워크북의 모든 키 확인
|
||||||
|
console.log("🔍 워크북 전체 키들:", Object.keys(workbook));
|
||||||
|
|
||||||
|
if (!workbook.SheetNames || workbook.SheetNames.length === 0) {
|
||||||
|
console.log("🎨 ❌ 시트가 없습니다.");
|
||||||
|
console.log("🎨 ❌ 워크북 전체 구조:", Object.keys(workbook));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
workbook.SheetNames.forEach((sheetName: string, sheetIndex: number) => {
|
||||||
|
const sheet = workbook.Sheets[sheetName];
|
||||||
|
if (!sheet) return;
|
||||||
|
|
||||||
|
console.log(`🎨 시트 ${sheetIndex + 1}: "${sheetName}"`);
|
||||||
|
|
||||||
|
// 시트 메타데이터
|
||||||
|
console.log(`🎨 - 데이터 범위: ${sheet["!ref"] || "없음"}`);
|
||||||
|
console.log(`🎨 - 병합 셀: ${sheet["!merges"]?.length || 0}개`);
|
||||||
|
console.log(`🎨 - 열 설정: ${sheet["!cols"]?.length || 0}개`);
|
||||||
|
console.log(`🎨 - 행 설정: ${sheet["!rows"]?.length || 0}개`);
|
||||||
|
|
||||||
|
// 병합 셀 상세 정보
|
||||||
|
if (sheet["!merges"]) {
|
||||||
|
sheet["!merges"].forEach((merge: any, index: number) => {
|
||||||
|
console.log(
|
||||||
|
`🎨 - 병합 ${index + 1}: ${XLSX.utils.encode_cell(merge.s)}:${XLSX.utils.encode_cell(merge.e)}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 스타일이 적용된 셀 찾기
|
||||||
|
const styledCells: string[] = [];
|
||||||
|
const cellAddresses = Object.keys(sheet).filter(
|
||||||
|
(key) => !key.startsWith("!"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔍 시트 데이터 존재 여부 확인
|
||||||
|
console.log(`🔍 ${sheetName} 기본 정보: ${cellAddresses.length}개 셀 발견`);
|
||||||
|
|
||||||
|
cellAddresses.forEach((cellAddr) => {
|
||||||
|
const cell = sheet[cellAddr];
|
||||||
|
if (cell && cell.s) {
|
||||||
|
styledCells.push(cellAddr);
|
||||||
|
|
||||||
|
// 🔍 첫 3개 셀의 실제 스타일 구조 확인
|
||||||
|
if (styledCells.length <= 3) {
|
||||||
|
console.log(`🔍 셀 ${cellAddr} cell.s 원시값:`, cell.s);
|
||||||
|
console.log(`🔍 cell.s 타입:`, typeof cell.s);
|
||||||
|
console.log(`🔍 cell.s 키들:`, Object.keys(cell.s || {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 스타일 정보 간단 확인
|
||||||
|
const hasStyles = {
|
||||||
|
font: !!cell.s.font,
|
||||||
|
fill: !!cell.s.fill,
|
||||||
|
border: !!cell.s.border,
|
||||||
|
alignment: !!cell.s.alignment,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Object.values(hasStyles).some((v) => v)) {
|
||||||
|
console.log(`🎨 셀 ${cellAddr} 스타일:`, hasStyles);
|
||||||
|
} else if (styledCells.length <= 3) {
|
||||||
|
console.log(`❌ 셀 ${cellAddr} 스타일 없음:`, hasStyles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`🎨 - 스타일 적용된 셀: ${styledCells.length}개 (${styledCells.join(", ")})`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("🎨 =================================");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 브라우저에서 테스트 파일 다운로드
|
||||||
|
*/
|
||||||
|
export function downloadTestFile(): void {
|
||||||
|
try {
|
||||||
|
const buffer = createStyledTestExcel();
|
||||||
|
const blob = new Blob([buffer], {
|
||||||
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = "스타일테스트.xlsx";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
console.log("🎨 스타일 테스트 파일 다운로드 완료!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("🎨 테스트 파일 생성 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/vite-env.d.ts
vendored
29
src/vite-env.d.ts
vendored
@@ -178,6 +178,33 @@ declare module "luckysheet" {
|
|||||||
*/
|
*/
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
luckysheet: any;
|
luckysheet: {
|
||||||
|
create: (options: {
|
||||||
|
container: string;
|
||||||
|
data: any[];
|
||||||
|
title?: string;
|
||||||
|
userInfo?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}) => void;
|
||||||
|
destroy: () => void;
|
||||||
|
resize?: () => void;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
LuckyExcel: {
|
||||||
|
transformExcelToLucky: (
|
||||||
|
file: File | ArrayBuffer,
|
||||||
|
successCallback: (exportJson: any, luckysheetfile: any) => void,
|
||||||
|
errorCallback: (error: any) => void,
|
||||||
|
) => void;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
$: any; // jQuery
|
||||||
|
Store?: any; // Luckysheet Store
|
||||||
|
luckysheet_function?: any; // Luckysheet function list
|
||||||
|
functionlist?: any[]; // 글로벌 functionlist
|
||||||
|
luckysheetConfigsetting?: any; // Luckysheet 설정 객체
|
||||||
|
luckysheetPostil?: any; // Luckysheet 포스틸 객체
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
|
|||||||
@@ -22,5 +22,6 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"],
|
||||||
|
"exclude": ["**/__tests__/**", "**/*.test.ts", "**/*.test.tsx"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "./tsconfig.app.json" },
|
{
|
||||||
{ "path": "./tsconfig.node.json" }
|
"path": "./tsconfig.app.json"
|
||||||
]
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"exclude": ["**/__tests__/**", "**/*.test.ts", "**/*.test.tsx"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,77 @@ import react from "@vitejs/plugin-react";
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
|
||||||
|
// Node.js 호환성 문제 해결
|
||||||
|
define: {
|
||||||
|
global: "globalThis",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Node.js 모듈 호환성 설정
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
stream: "stream-browserify",
|
||||||
|
buffer: "buffer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 의존성 최적화 설정
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: [
|
||||||
|
// 중복 로딩 방지를 위해 redi와 univer 관련 제외
|
||||||
|
"@wendellhu/redi",
|
||||||
|
"@univerjs/core",
|
||||||
|
"@univerjs/design",
|
||||||
|
"@univerjs/ui",
|
||||||
|
"@univerjs/sheets",
|
||||||
|
"@univerjs/sheets-ui",
|
||||||
|
"@univerjs/docs",
|
||||||
|
"@univerjs/docs-ui",
|
||||||
|
"@univerjs/engine-render",
|
||||||
|
"@univerjs/engine-formula",
|
||||||
|
"@univerjs/sheets-formula",
|
||||||
|
"@univerjs/sheets-formula-ui",
|
||||||
|
"@univerjs/sheets-numfmt",
|
||||||
|
"@univerjs/sheets-numfmt-ui",
|
||||||
|
"@univerjs/facade",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// 빌드 설정
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
external: [],
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
// Univer 관련 라이브러리를 별도 청크로 분리
|
||||||
|
"univer-core": [
|
||||||
|
"@univerjs/core",
|
||||||
|
"@univerjs/design",
|
||||||
|
"@univerjs/engine-render",
|
||||||
|
"@univerjs/engine-formula",
|
||||||
|
],
|
||||||
|
"univer-sheets": [
|
||||||
|
"@univerjs/sheets",
|
||||||
|
"@univerjs/sheets-ui",
|
||||||
|
"@univerjs/sheets-formula",
|
||||||
|
"@univerjs/sheets-formula-ui",
|
||||||
|
"@univerjs/sheets-numfmt",
|
||||||
|
"@univerjs/sheets-numfmt-ui",
|
||||||
|
],
|
||||||
|
"univer-docs": ["@univerjs/docs", "@univerjs/docs-ui"],
|
||||||
|
"univer-ui": ["@univerjs/ui", "@univerjs/facade"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 서버 설정
|
||||||
|
server: {
|
||||||
|
fs: {
|
||||||
|
strict: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// @ts-ignore - vitest config
|
// @ts-ignore - vitest config
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user