유니버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": {
|
||||
"@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",
|
||||
"clsx": "^2.1.1",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -27,9 +43,7 @@
|
||||
"luckysheet": "^2.1.13",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"sheetjs-style": "^0.15.8",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"xlsx": "^0.18.5",
|
||||
"zustand": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -46,6 +60,7 @@
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"buffer": "^6.0.3",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
@@ -54,6 +69,7 @@
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"postcss": "^8.5.1",
|
||||
"prettier": "^3.4.2",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.1",
|
||||
|
||||
64
src/App.tsx
64
src/App.tsx
@@ -1,14 +1,9 @@
|
||||
import { useAppStore } from "./stores/useAppStore";
|
||||
import { Card, CardContent } from "./components/ui/card";
|
||||
import { useState } from "react";
|
||||
import { Button } from "./components/ui/button";
|
||||
import { FileUpload } from "./components/sheet/FileUpload";
|
||||
import { SheetViewer } from "./components/sheet/SheetViewer";
|
||||
import TestSheetViewer from "./components/sheet/TestSheetViewer";
|
||||
|
||||
function App() {
|
||||
const { currentFile, sheets, resetApp } = useAppStore();
|
||||
|
||||
// 파일이 업로드되어 시트 데이터가 있는 경우와 없는 경우 구분
|
||||
const hasSheetData = currentFile && sheets && sheets.length > 0;
|
||||
const [showTestViewer, setShowTestViewer] = useState(false);
|
||||
|
||||
return (
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
{hasSheetData && (
|
||||
<>
|
||||
<span className="text-sm text-gray-600">
|
||||
{currentFile.name}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={resetApp}
|
||||
className="text-gray-600 hover:text-gray-800"
|
||||
>
|
||||
새 파일
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{!hasSheetData && (
|
||||
{/* 테스트 뷰어 토글 버튼 */}
|
||||
<Button
|
||||
variant={showTestViewer ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setShowTestViewer(!showTestViewer)}
|
||||
className="bg-green-500 hover:bg-green-600 text-white border-green-500"
|
||||
>
|
||||
🧪 테스트 뷰어
|
||||
</Button>
|
||||
|
||||
{!showTestViewer && (
|
||||
<span className="text-sm text-gray-600">
|
||||
Excel 파일 AI 처리 도구
|
||||
Univer CE 테스트 모드
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -47,15 +37,29 @@ function App() {
|
||||
|
||||
{/* 메인 콘텐츠 */}
|
||||
<main className="h-[calc(100vh-4rem)]">
|
||||
{hasSheetData ? (
|
||||
// 파일이 업로드된 경우: SheetViewer 표시 (전체화면)
|
||||
{showTestViewer ? (
|
||||
// 테스트 뷰어 표시
|
||||
<div className="h-full">
|
||||
<SheetViewer className="h-full" />
|
||||
<TestSheetViewer />
|
||||
</div>
|
||||
) : (
|
||||
// 파일이 업로드되지 않은 경우: FileUpload 표시 (중앙 정렬)
|
||||
// 메인 페이지
|
||||
<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>
|
||||
)}
|
||||
</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,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
@@ -6,21 +6,6 @@ import React, {
|
||||
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;
|
||||
@@ -28,9 +13,9 @@ interface SheetViewerProps {
|
||||
|
||||
/**
|
||||
* Luckysheet 시트 뷰어 컴포넌트
|
||||
* - 참고 내용 기반: 완전한 라이브러리 로딩 순서 적용
|
||||
* - functionlist 오류 방지를 위한 완전한 초기화
|
||||
* - 필수 플러그인과 CSS 포함
|
||||
* - 메모리 정보 기반: LuckyExcel 변환 결과를 직접 사용
|
||||
* - 커스텀 검증이나 데이터 구조 변경 금지
|
||||
* - luckysheet.create({ data: exportJson.sheets })로 직접 사용
|
||||
*/
|
||||
export function SheetViewer({ className }: SheetViewerProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -41,12 +26,11 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
||||
const [isContainerReady, setIsContainerReady] = useState(false);
|
||||
const [librariesLoaded, setLibrariesLoaded] = useState(false);
|
||||
|
||||
// 스토어에서 시트 데이터 가져오기
|
||||
const { sheets, activeSheetId, currentFile, setSelectedRange } =
|
||||
useAppStore();
|
||||
// 스토어에서 현재 파일 정보만 가져오기 (시트 데이터는 LuckyExcel로 직접 변환)
|
||||
const { currentFile, setSelectedRange } = useAppStore();
|
||||
|
||||
/**
|
||||
* CDN 배포판 + functionlist 직접 초기화 방식
|
||||
* CDN 배포판 라이브러리 로딩
|
||||
*/
|
||||
const loadLuckysheetLibrary = useCallback((): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -62,8 +46,6 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log("📦 CDN 배포판 + functionlist 직접 초기화 방식...");
|
||||
|
||||
const loadResource = (
|
||||
type: "css" | "js",
|
||||
src: string,
|
||||
@@ -72,7 +54,6 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
||||
return new Promise((resourceResolve, resourceReject) => {
|
||||
// 이미 로드된 리소스 체크
|
||||
if (document.querySelector(`[data-luckysheet-id="${id}"]`)) {
|
||||
// console.log(`📦 ${id} 이미 로드됨`);
|
||||
resourceResolve();
|
||||
return;
|
||||
}
|
||||
@@ -82,33 +63,22 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
||||
link.rel = "stylesheet";
|
||||
link.href = src;
|
||||
link.setAttribute("data-luckysheet-id", id);
|
||||
link.onload = () => {
|
||||
// console.log(`✅ ${id} CSS 로드 완료`);
|
||||
resourceResolve();
|
||||
};
|
||||
link.onerror = (error) => {
|
||||
// console.error(`❌ ${id} CSS 로드 실패:`, error);
|
||||
link.onload = () => resourceResolve();
|
||||
link.onerror = () =>
|
||||
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 = () => {
|
||||
// console.log(`✅ ${id} JS 로드 완료`);
|
||||
resourceResolve();
|
||||
};
|
||||
script.onerror = (error) => {
|
||||
// console.error(`❌ ${id} JS 로드 실패:`, error);
|
||||
script.onload = () => resourceResolve();
|
||||
script.onerror = () =>
|
||||
resourceReject(new Error(`${id} JS 로드 실패`));
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// CDN 배포판 로딩 + functionlist 직접 초기화
|
||||
const loadSequence = async () => {
|
||||
try {
|
||||
// 1. jQuery (Luckysheet 의존성)
|
||||
@@ -120,79 +90,54 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// 2. CSS 로드 (빌드된 파일들)
|
||||
// 2. CSS 로드 (공식 문서 순서 준수)
|
||||
await loadResource(
|
||||
"css",
|
||||
"/luckysheet/dist/plugins/css/pluginsCss.css",
|
||||
"https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/css/pluginsCss.css",
|
||||
"plugins-css",
|
||||
);
|
||||
await loadResource(
|
||||
"css",
|
||||
"/luckysheet/dist/plugins/plugins.css",
|
||||
"https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/plugins.css",
|
||||
"plugins-main-css",
|
||||
);
|
||||
await loadResource(
|
||||
"css",
|
||||
"/luckysheet/dist/css/luckysheet.css",
|
||||
"https://cdn.jsdelivr.net/npm/luckysheet/dist/css/luckysheet.css",
|
||||
"luckysheet-css",
|
||||
);
|
||||
await loadResource(
|
||||
"css",
|
||||
"/luckysheet/dist/assets/iconfont/iconfont.css",
|
||||
"https://cdn.jsdelivr.net/npm/luckysheet/dist/assets/iconfont/iconfont.css",
|
||||
"iconfont-css",
|
||||
);
|
||||
|
||||
// 3. Plugin JS 먼저 로드 (functionlist 초기화 우선)
|
||||
// 3. Plugin JS 먼저 로드 (functionlist 초기화)
|
||||
await loadResource(
|
||||
"js",
|
||||
"/luckysheet/dist/plugins/js/plugin.js",
|
||||
"https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/js/plugin.js",
|
||||
"plugin-js",
|
||||
);
|
||||
|
||||
// 👉 plugin.js 로드 후 실제 functionlist 가 채워졌는지 polling 으로 확인 (최대 3초)
|
||||
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 준비 후)
|
||||
// 4. Luckysheet 메인
|
||||
if (!window.luckysheet) {
|
||||
await loadResource(
|
||||
"js",
|
||||
"/luckysheet/dist/luckysheet.umd.js",
|
||||
"https://cdn.jsdelivr.net/npm/luckysheet/dist/luckysheet.umd.js",
|
||||
"luckysheet",
|
||||
);
|
||||
}
|
||||
|
||||
// 라이브러리 로드 후 검증
|
||||
// console.log("🔍 라이브러리 로드 후 검증 중...");
|
||||
// 5. LuckyExcel (Excel 파일 처리용)
|
||||
if (!window.LuckyExcel) {
|
||||
await loadResource(
|
||||
"js",
|
||||
"https://cdn.jsdelivr.net/npm/luckyexcel/dist/luckyexcel.umd.js",
|
||||
"luckyexcel",
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: plugin.js 가 실제 functionlist 를 채웠으므로 별도 지연 대기 불필요
|
||||
// 필수 객체 검증
|
||||
// 라이브러리 검증
|
||||
const validationResults = {
|
||||
jquery: !!window.$,
|
||||
luckyExcel: !!window.LuckyExcel,
|
||||
@@ -207,8 +152,6 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
||||
),
|
||||
};
|
||||
|
||||
// console.log("🔍 라이브러리 검증 결과:", validationResults);
|
||||
|
||||
if (
|
||||
!validationResults.luckysheet ||
|
||||
!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);
|
||||
// console.log("✅ CDN 배포판 + functionlist 초기화 완료");
|
||||
console.log("✅ 라이브러리 로드 완료");
|
||||
resolve();
|
||||
} catch (error) {
|
||||
// console.error("❌ 라이브러리 로딩 실패:", error);
|
||||
console.error("❌ 라이브러리 로딩 실패:", error);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
@@ -296,9 +175,11 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
||||
}, [librariesLoaded]);
|
||||
|
||||
/**
|
||||
* 참고 내용 기반: 올바른 데이터 구조로 Luckysheet 초기화
|
||||
* 메모리 정보 기반: LuckyExcel 변환 결과를 직접 사용하는 방식
|
||||
* - LuckyExcel.transformExcelToLucky()에서 반환된 exportJson.sheets를 그대로 사용
|
||||
* - 커스텀 검증이나 데이터 구조 변경 금지
|
||||
*/
|
||||
const convertXLSXToLuckysheet = useCallback(
|
||||
const convertXLSXWithLuckyExcel = useCallback(
|
||||
async (xlsxBuffer: ArrayBuffer, fileName: string) => {
|
||||
if (!containerRef.current) {
|
||||
console.warn("⚠️ 컨테이너가 없습니다.");
|
||||
@@ -309,317 +190,97 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
||||
setIsConverting(true);
|
||||
setError(null);
|
||||
|
||||
// console.log(
|
||||
// "🔄 참고 내용 기반: XLSX → LuckyExcel → Luckysheet 변환 시작...",
|
||||
// );
|
||||
console.log("🍀 메모리 정보 기반: LuckyExcel 직접 변환 시작...");
|
||||
|
||||
// 라이브러리 로드 확인
|
||||
await loadLuckysheetLibrary();
|
||||
|
||||
// 기존 인스턴스 정리 (참고 내용 권장사항)
|
||||
// console.log("🧹 기존 Luckysheet 인스턴스 정리...");
|
||||
// 기존 인스턴스 정리
|
||||
try {
|
||||
if (
|
||||
window.luckysheet &&
|
||||
typeof window.luckysheet.destroy === "function"
|
||||
) {
|
||||
window.luckysheet.destroy();
|
||||
// console.log("✅ 기존 인스턴스 destroy 완료");
|
||||
console.log("✅ 기존 인스턴스 destroy 완료");
|
||||
}
|
||||
} catch (destroyError) {
|
||||
// console.warn("⚠️ destroy 중 오류 (무시됨):", destroyError);
|
||||
console.warn("⚠️ destroy 중 오류 (무시됨):", destroyError);
|
||||
}
|
||||
|
||||
// 컨테이너 초기화
|
||||
if (containerRef.current) {
|
||||
containerRef.current.innerHTML = "";
|
||||
// console.log("✅ 컨테이너 초기화 완료");
|
||||
console.log("✅ 컨테이너 초기화 완료");
|
||||
}
|
||||
|
||||
luckysheetRef.current = null;
|
||||
|
||||
// console.log("🍀 LuckyExcel.transformExcelToLucky 호출...");
|
||||
console.log("🍀 LuckyExcel.transformExcelToLucky 호출...");
|
||||
|
||||
// LuckyExcel 변환 (참고 내용의 블로그 포스트 방식)
|
||||
window.LuckyExcel.transformExcelToLucky(
|
||||
xlsxBuffer,
|
||||
// 성공 콜백 - 변환 완료 후에만 Luckysheet 초기화
|
||||
(exportJson: any, luckysheetfile: any) => {
|
||||
try {
|
||||
// console.log("✅ LuckyExcel 변환 완료:", {
|
||||
// hasExportJson: !!exportJson,
|
||||
// hasSheets: !!exportJson?.sheets,
|
||||
// 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,
|
||||
// })) || [],
|
||||
// });
|
||||
// ArrayBuffer를 File 객체로 변환 (LuckyExcel은 File 객체 필요)
|
||||
const file = new File([xlsxBuffer], fileName, {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
});
|
||||
|
||||
// 공식 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) => {
|
||||
console.error("❌ LuckyExcel 변환 실패:", error);
|
||||
setError(
|
||||
`XLSX 변환에 실패했습니다: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
// 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}`));
|
||||
},
|
||||
);
|
||||
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) {
|
||||
console.error("❌ 변환 프로세스 실패:", conversionError);
|
||||
setError(
|
||||
@@ -640,12 +301,9 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
||||
* DOM 컨테이너 준비 상태 체크 - useLayoutEffect로 동기적 체크
|
||||
*/
|
||||
useLayoutEffect(() => {
|
||||
// console.log("🔍 useLayoutEffect: DOM 컨테이너 체크 시작...");
|
||||
if (containerRef.current) {
|
||||
// console.log("✅ DOM 컨테이너 준비 완료:", containerRef.current.id);
|
||||
console.log("✅ DOM 컨테이너 준비 완료:", containerRef.current.id);
|
||||
setIsContainerReady(true);
|
||||
} else {
|
||||
// console.warn("⚠️ useLayoutEffect: DOM 컨테이너가 아직 준비되지 않음");
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -654,10 +312,9 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isContainerReady) {
|
||||
// console.log("🔄 useEffect: DOM 컨테이너 재체크...");
|
||||
const timer = setTimeout(() => {
|
||||
if (containerRef.current && !isContainerReady) {
|
||||
// console.log("✅ useEffect: DOM 컨테이너 지연 준비 완료");
|
||||
console.log("✅ useEffect: DOM 컨테이너 지연 준비 완료");
|
||||
setIsContainerReady(true);
|
||||
}
|
||||
}, 100);
|
||||
@@ -666,43 +323,37 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
||||
}, [isContainerReady]);
|
||||
|
||||
/**
|
||||
* 컴포넌트 마운트 시 초기화 - 블로그 포스트 방식 적용
|
||||
* 컴포넌트 마운트 시 초기화
|
||||
*/
|
||||
useEffect(() => {
|
||||
// console.log("🔍 useEffect 실행 조건 체크:", {
|
||||
// hasCurrentFile: !!currentFile,
|
||||
// hasXlsxBuffer: !!currentFile?.xlsxBuffer,
|
||||
// hasContainer: !!containerRef.current,
|
||||
// isContainerReady,
|
||||
// currentFileName: currentFile?.name,
|
||||
// bufferSize: currentFile?.xlsxBuffer?.byteLength,
|
||||
// });
|
||||
if (
|
||||
currentFile?.xlsxBuffer &&
|
||||
isContainerReady &&
|
||||
containerRef.current &&
|
||||
!isInitialized &&
|
||||
!isConverting
|
||||
) {
|
||||
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 시작...", {
|
||||
// fileName: currentFile.name,
|
||||
// bufferSize: currentFile.xlsxBuffer.byteLength,
|
||||
// containerId: containerRef.current.id,
|
||||
// });
|
||||
// 중복 실행 방지
|
||||
setIsConverting(true);
|
||||
|
||||
// 변환된 XLSX ArrayBuffer를 사용하여 직접 변환 (블로그 포스트 방식)
|
||||
convertXLSXToLuckysheet(currentFile.xlsxBuffer, currentFile.name);
|
||||
// LuckyExcel로 직접 변환
|
||||
convertXLSXWithLuckyExcel(currentFile.xlsxBuffer, currentFile.name);
|
||||
} else if (currentFile && !currentFile.xlsxBuffer) {
|
||||
// console.warn(
|
||||
// "⚠️ currentFile은 있지만 xlsxBuffer가 없습니다:",
|
||||
// currentFile,
|
||||
// );
|
||||
setError("파일 변환 데이터가 없습니다. 파일을 다시 업로드해주세요.");
|
||||
} else if (!currentFile) {
|
||||
// console.log("ℹ️ currentFile이 없습니다. 파일을 업로드해주세요.");
|
||||
} else if (currentFile?.xlsxBuffer && !isContainerReady) {
|
||||
// console.log("⏳ DOM 컨테이너 준비 대기 중...");
|
||||
}
|
||||
}, [
|
||||
currentFile?.xlsxBuffer,
|
||||
currentFile?.name,
|
||||
isContainerReady,
|
||||
convertXLSXToLuckysheet,
|
||||
isInitialized,
|
||||
isConverting,
|
||||
convertXLSXWithLuckyExcel,
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -711,11 +362,10 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (luckysheetRef.current && window.luckysheet) {
|
||||
// console.log("🧹 컴포넌트 언마운트: Luckysheet 정리 중...");
|
||||
try {
|
||||
window.luckysheet.destroy();
|
||||
} catch (error) {
|
||||
// console.warn("⚠️ Luckysheet 정리 중 오류:", error);
|
||||
console.warn("⚠️ Luckysheet 정리 중 오류:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -728,9 +378,11 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
||||
const handleResize = () => {
|
||||
if (luckysheetRef.current && window.luckysheet) {
|
||||
try {
|
||||
window.luckysheet.resize();
|
||||
if (window.luckysheet.resize) {
|
||||
window.luckysheet.resize();
|
||||
}
|
||||
} catch (error) {
|
||||
// console.warn("⚠️ Luckysheet 리사이즈 중 오류:", error);
|
||||
console.warn("⚠️ Luckysheet 리사이즈 중 오류:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -771,7 +423,7 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
||||
setIsInitialized(false);
|
||||
setIsConverting(false);
|
||||
if (currentFile?.xlsxBuffer) {
|
||||
convertXLSXToLuckysheet(
|
||||
convertXLSXWithLuckyExcel(
|
||||
currentFile.xlsxBuffer,
|
||||
currentFile.name,
|
||||
);
|
||||
@@ -793,11 +445,11 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
||||
<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 ? "XLSX 변환 중..." : "시트 초기화 중..."}
|
||||
{isConverting ? "LuckyExcel 변환 중..." : "시트 초기화 중..."}
|
||||
</div>
|
||||
<div className="text-blue-500 text-sm">
|
||||
{isConverting
|
||||
? "변환된 XLSX를 Luckysheet로 처리하고 있습니다."
|
||||
? "원본 Excel 데이터를 완전한 스타일로 변환하고 있습니다."
|
||||
: "잠시만 기다려주세요."}
|
||||
</div>
|
||||
</div>
|
||||
@@ -831,6 +483,7 @@ export function SheetViewer({ className }: SheetViewerProps) {
|
||||
<div>변환 중: {isConverting ? "예" : "아니오"}</div>
|
||||
<div>초기화: {isInitialized ? "완료" : "대기"}</div>
|
||||
<div>컨테이너 준비: {isContainerReady ? "완료" : "대기"}</div>
|
||||
<div>방식: LuckyExcel 직접 변환</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 userEvent from "@testing-library/user-event";
|
||||
import "@testing-library/jest-dom";
|
||||
@@ -29,7 +29,7 @@ class MockDragEvent extends Event {
|
||||
// @ts-ignore
|
||||
global.DragEvent = MockDragEvent;
|
||||
|
||||
const mockUseAppStore = useAppStore as vi.MockedFunction<typeof useAppStore>;
|
||||
const mockUseAppStore = useAppStore as any;
|
||||
|
||||
describe("FileUpload", () => {
|
||||
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 "@testing-library/jest-dom";
|
||||
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 components;
|
||||
@tailwind utilities;
|
||||
@@ -105,25 +114,29 @@
|
||||
.lg\:px-8 { padding-left: 2rem; padding-right: 2rem; }
|
||||
}
|
||||
|
||||
/* 커스텀 스타일 */
|
||||
body {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
color-scheme: light;
|
||||
color: #1f2937; /* 검은색 계열로 변경 */
|
||||
background-color: #ffffff;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
/* 전역 스타일 */
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* 스크롤바 스타일링 */
|
||||
/* Univer 컨테이너 스타일 */
|
||||
.univer-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 커스텀 스크롤바 (Univer와 일치) */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@@ -132,20 +145,132 @@ body {
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-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 {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
0% { transform: rotate(0deg); }
|
||||
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 './index.css'
|
||||
import App from './App.tsx'
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App.tsx";
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
createRoot(document.getElementById("root")!).render(<App />);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import * as XLSX from "xlsx";
|
||||
import * as XLSX from "xlsx-js-style";
|
||||
import {
|
||||
validateFileType,
|
||||
validateFileSize,
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
SUPPORTED_EXTENSIONS,
|
||||
} from "../fileProcessor";
|
||||
|
||||
// SheetJS 모킹 (통합 처리)
|
||||
vi.mock("xlsx", () => ({
|
||||
// xlsx-js-style 모킹 (통합 처리)
|
||||
vi.mock("xlsx-js-style", () => ({
|
||||
read: vi.fn(() => ({
|
||||
SheetNames: ["Sheet1"],
|
||||
Sheets: {
|
||||
@@ -30,61 +30,80 @@ vi.mock("xlsx", () => ({
|
||||
["테스트", "한글", "데이터"],
|
||||
["값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 모킹
|
||||
vi.mock("luckyexcel", () => ({
|
||||
transformExcelToLucky: vi.fn((arrayBuffer, fileName, callback) => {
|
||||
// 성공적인 변환 결과 모킹
|
||||
const mockResult = {
|
||||
sheets: [
|
||||
{
|
||||
name: "Sheet1",
|
||||
index: "0",
|
||||
status: 1,
|
||||
order: 0,
|
||||
row: 2,
|
||||
column: 3,
|
||||
celldata: [
|
||||
{
|
||||
r: 0,
|
||||
c: 0,
|
||||
v: { v: "테스트", m: "테스트", ct: { fa: "General", t: "g" } },
|
||||
},
|
||||
{
|
||||
r: 0,
|
||||
c: 1,
|
||||
v: { v: "한글", m: "한글", ct: { fa: "General", t: "g" } },
|
||||
},
|
||||
{
|
||||
r: 0,
|
||||
c: 2,
|
||||
v: { v: "데이터", m: "데이터", ct: { fa: "General", t: "g" } },
|
||||
},
|
||||
{
|
||||
r: 1,
|
||||
c: 0,
|
||||
v: { v: "값1", m: "값1", ct: { fa: "General", t: "g" } },
|
||||
},
|
||||
{
|
||||
r: 1,
|
||||
c: 1,
|
||||
v: { v: "값2", m: "값2", ct: { fa: "General", t: "g" } },
|
||||
},
|
||||
{
|
||||
r: 1,
|
||||
c: 2,
|
||||
v: { v: "값3", m: "값3", ct: { fa: "General", t: "g" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
transformExcelToLucky: vi.fn(
|
||||
(_arrayBuffer, successCallback, _errorCallback) => {
|
||||
// 성공적인 변환 결과 모킹
|
||||
const mockResult = {
|
||||
sheets: [
|
||||
{
|
||||
name: "Sheet1",
|
||||
index: "0",
|
||||
status: 1,
|
||||
order: 0,
|
||||
row: 2,
|
||||
column: 3,
|
||||
celldata: [
|
||||
{
|
||||
r: 0,
|
||||
c: 0,
|
||||
v: { v: "테스트", m: "테스트", ct: { fa: "General", t: "g" } },
|
||||
},
|
||||
{
|
||||
r: 0,
|
||||
c: 1,
|
||||
v: { v: "한글", m: "한글", ct: { fa: "General", t: "g" } },
|
||||
},
|
||||
{
|
||||
r: 0,
|
||||
c: 2,
|
||||
v: { v: "데이터", m: "데이터", ct: { fa: "General", t: "g" } },
|
||||
},
|
||||
{
|
||||
r: 1,
|
||||
c: 0,
|
||||
v: { v: "값1", m: "값1", ct: { fa: "General", t: "g" } },
|
||||
},
|
||||
{
|
||||
r: 1,
|
||||
c: 1,
|
||||
v: { v: "값2", m: "값2", ct: { fa: "General", t: "g" } },
|
||||
},
|
||||
{
|
||||
r: 1,
|
||||
c: 2,
|
||||
v: { v: "값3", m: "값3", ct: { fa: "General", t: "g" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 비동기 콜백 호출
|
||||
setTimeout(() => callback(mockResult, null), 0);
|
||||
}),
|
||||
// 성공 콜백 비동기 호출 (ArrayBuffer 매개변수 대응)
|
||||
if (typeof successCallback === "function") {
|
||||
setTimeout(() => successCallback(mockResult, null), 0);
|
||||
}
|
||||
},
|
||||
),
|
||||
}));
|
||||
|
||||
// 파일 생성 도우미 함수
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as XLSX from "xlsx";
|
||||
import * as LuckyExcel from "luckyexcel";
|
||||
import * as XLSX from "xlsx-js-style";
|
||||
import type { SheetData, FileUploadResult } from "../types/sheet";
|
||||
import { analyzeSheetStyles } from "./styleTest";
|
||||
|
||||
/**
|
||||
* 파일 처리 관련 유틸리티 - 개선된 버전
|
||||
* 파일 처리 관련 유틸리티 - xlsx-js-style 공식 API 활용 버전
|
||||
* - 모든 파일 형식을 SheetJS를 통해 읽은 후 XLSX로 변환
|
||||
* - 변환된 XLSX 파일을 LuckyExcel로 전달
|
||||
* - 안정적인 한글 지원 및 에러 처리
|
||||
* - xlsx-js-style의 공식 스타일 구조를 그대로 활용
|
||||
*/
|
||||
|
||||
// 지원되는 파일 타입
|
||||
@@ -21,6 +21,294 @@ export const SUPPORTED_EXTENSIONS = [".xlsx", ".xls", ".csv"] as const;
|
||||
// 최대 파일 크기 (50MB)
|
||||
export const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* xlsx-js-style 색상 객체를 Luckysheet 색상 문자열로 변환
|
||||
* 공식 xlsx-js-style COLOR_STYLE 형식을 지원: rgb, theme, indexed
|
||||
*/
|
||||
function convertXlsxColorToLuckysheet(colorObj: any): string {
|
||||
if (!colorObj) return "";
|
||||
|
||||
// RGB 형태 - 공식 문서: {rgb: "FFCC00"}
|
||||
if (colorObj.rgb) {
|
||||
const rgb = colorObj.rgb.toUpperCase();
|
||||
// ARGB 형태 (8자리) - 앞의 2자리(Alpha) 제거
|
||||
if (rgb.length === 8) {
|
||||
const r = parseInt(rgb.substring(2, 4), 16);
|
||||
const g = parseInt(rgb.substring(4, 6), 16);
|
||||
const b = parseInt(rgb.substring(6, 8), 16);
|
||||
return `rgb(${r},${g},${b})`;
|
||||
}
|
||||
// RGB 형태 (6자리)
|
||||
else if (rgb.length === 6) {
|
||||
const r = parseInt(rgb.substring(0, 2), 16);
|
||||
const g = parseInt(rgb.substring(2, 4), 16);
|
||||
const b = parseInt(rgb.substring(4, 6), 16);
|
||||
return `rgb(${r},${g},${b})`;
|
||||
}
|
||||
}
|
||||
|
||||
// Theme 색상 - 공식 문서: {theme: 4} 또는 {theme: 1, tint: 0.4}
|
||||
if (typeof colorObj.theme === "number") {
|
||||
// Excel 테마 색상 매핑 (공식 문서 예시 기반)
|
||||
const themeColors: { [key: number]: string } = {
|
||||
0: "rgb(255,255,255)", // 배경 1 (흰색)
|
||||
1: "rgb(0,0,0)", // 텍스트 1 (검정)
|
||||
2: "rgb(238,236,225)", // 배경 2 (연회색)
|
||||
3: "rgb(31,73,125)", // 텍스트 2 (어두운 파랑)
|
||||
4: "rgb(79,129,189)", // 강조 1 (파랑) - 공식 문서 예시
|
||||
5: "rgb(192,80,77)", // 강조 2 (빨강)
|
||||
6: "rgb(155,187,89)", // 강조 3 (초록)
|
||||
7: "rgb(128,100,162)", // 강조 4 (보라)
|
||||
8: "rgb(75,172,198)", // 강조 5 (하늘색)
|
||||
9: "rgb(247,150,70)", // 강조 6 (주황)
|
||||
};
|
||||
|
||||
let baseColor = themeColors[colorObj.theme] || "rgb(0,0,0)";
|
||||
|
||||
// Tint 적용 - 공식 문서: {theme: 1, tint: 0.4} ("Blue, Accent 1, Lighter 40%")
|
||||
if (typeof colorObj.tint === "number") {
|
||||
baseColor = applyTintToRgbColor(baseColor, colorObj.tint);
|
||||
}
|
||||
|
||||
return baseColor;
|
||||
}
|
||||
|
||||
// Indexed 색상 (Excel 기본 색상표)
|
||||
if (typeof colorObj.indexed === "number") {
|
||||
const indexedColors: { [key: number]: string } = {
|
||||
0: "rgb(0,0,0)", // 검정
|
||||
1: "rgb(255,255,255)", // 흰색
|
||||
2: "rgb(255,0,0)", // 빨강
|
||||
3: "rgb(0,255,0)", // 초록
|
||||
4: "rgb(0,0,255)", // 파랑
|
||||
5: "rgb(255,255,0)", // 노랑
|
||||
6: "rgb(255,0,255)", // 마젠타
|
||||
7: "rgb(0,255,255)", // 시안
|
||||
8: "rgb(128,0,0)", // 어두운 빨강
|
||||
9: "rgb(0,128,0)", // 어두운 초록
|
||||
10: "rgb(0,0,128)", // 어두운 파랑
|
||||
17: "rgb(128,128,128)", // 회색
|
||||
};
|
||||
|
||||
return indexedColors[colorObj.indexed] || "rgb(0,0,0)";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* RGB 색상에 Excel tint 적용
|
||||
*/
|
||||
function applyTintToRgbColor(rgbColor: string, tint: number): string {
|
||||
const match = rgbColor.match(/rgb\((\d+),(\d+),(\d+)\)/);
|
||||
if (!match) return rgbColor;
|
||||
|
||||
const r = parseInt(match[1]);
|
||||
const g = parseInt(match[2]);
|
||||
const b = parseInt(match[3]);
|
||||
|
||||
// Excel tint 공식 적용
|
||||
const applyTint = (color: number, tint: number): number => {
|
||||
if (tint < 0) {
|
||||
return Math.round(color * (1 + tint));
|
||||
} else {
|
||||
return Math.round(color * (1 - tint) + (255 - 255 * (1 - tint)));
|
||||
}
|
||||
};
|
||||
|
||||
const newR = Math.max(0, Math.min(255, applyTint(r, tint)));
|
||||
const newG = Math.max(0, Math.min(255, applyTint(g, tint)));
|
||||
const newB = Math.max(0, Math.min(255, applyTint(b, tint)));
|
||||
|
||||
return `rgb(${newR},${newG},${newB})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* xlsx-js-style 테두리 스타일을 Luckysheet 번호로 변환
|
||||
* 공식 문서의 BORDER_STYLE 값들을 지원
|
||||
*/
|
||||
function convertBorderStyleToLuckysheet(borderStyle: string): number {
|
||||
const styleMap: { [key: string]: number } = {
|
||||
thin: 1,
|
||||
medium: 2,
|
||||
thick: 3,
|
||||
dotted: 4,
|
||||
dashed: 5,
|
||||
dashDot: 6,
|
||||
dashDotDot: 7,
|
||||
double: 8,
|
||||
hair: 1,
|
||||
mediumDashed: 5,
|
||||
mediumDashDot: 6,
|
||||
mediumDashDotDot: 7,
|
||||
slantDashDot: 6,
|
||||
};
|
||||
|
||||
return styleMap[borderStyle] || 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* xlsx-js-style 스타일 객체를 Luckysheet 스타일로 변환
|
||||
* 공식 xlsx-js-style API 구조를 완전히 활용
|
||||
*/
|
||||
function convertXlsxStyleToLuckysheet(xlsxStyle: any): any {
|
||||
if (!xlsxStyle) return {};
|
||||
|
||||
const luckyStyle: any = {};
|
||||
|
||||
// 🎨 폰트 스타일 변환 - 공식 문서 font 속성
|
||||
if (xlsxStyle.font) {
|
||||
const font = xlsxStyle.font;
|
||||
|
||||
// 폰트명 - 공식 문서: {name: "Courier"}
|
||||
if (font.name) {
|
||||
luckyStyle.ff = font.name;
|
||||
}
|
||||
|
||||
// 폰트 크기 - 공식 문서: {sz: 24}
|
||||
if (font.sz) {
|
||||
luckyStyle.fs = font.sz;
|
||||
}
|
||||
|
||||
// 굵게 - 공식 문서: {bold: true}
|
||||
if (font.bold) {
|
||||
luckyStyle.bl = 1;
|
||||
}
|
||||
|
||||
// 기울임 - 공식 문서: {italic: true}
|
||||
if (font.italic) {
|
||||
luckyStyle.it = 1;
|
||||
}
|
||||
|
||||
// 밑줄 - 공식 문서: {underline: true}
|
||||
if (font.underline) {
|
||||
luckyStyle.un = 1;
|
||||
}
|
||||
|
||||
// 취소선 - 공식 문서: {strike: true}
|
||||
if (font.strike) {
|
||||
luckyStyle.st = 1;
|
||||
}
|
||||
|
||||
// 폰트 색상 - 공식 문서: {color: {rgb: "FF0000"}}
|
||||
if (font.color) {
|
||||
const fontColor = convertXlsxColorToLuckysheet(font.color);
|
||||
if (fontColor) {
|
||||
luckyStyle.fc = fontColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🎨 배경 스타일 변환 - 공식 문서 fill 속성
|
||||
if (xlsxStyle.fill) {
|
||||
const fill = xlsxStyle.fill;
|
||||
|
||||
// 배경색 - 공식 문서: {fgColor: {rgb: "E9E9E9"}}
|
||||
if (fill.fgColor) {
|
||||
const bgColor = convertXlsxColorToLuckysheet(fill.fgColor);
|
||||
if (bgColor) {
|
||||
luckyStyle.bg = bgColor;
|
||||
}
|
||||
}
|
||||
// bgColor도 확인 (패턴 배경의 경우)
|
||||
else if (fill.bgColor) {
|
||||
const bgColor = convertXlsxColorToLuckysheet(fill.bgColor);
|
||||
if (bgColor) {
|
||||
luckyStyle.bg = bgColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🎨 정렬 스타일 변환 - 공식 문서 alignment 속성
|
||||
if (xlsxStyle.alignment) {
|
||||
const alignment = xlsxStyle.alignment;
|
||||
|
||||
// 수평 정렬 - 공식 문서: {horizontal: "center"}
|
||||
if (alignment.horizontal) {
|
||||
luckyStyle.ht =
|
||||
alignment.horizontal === "left"
|
||||
? 1
|
||||
: alignment.horizontal === "center"
|
||||
? 2
|
||||
: alignment.horizontal === "right"
|
||||
? 3
|
||||
: 1;
|
||||
}
|
||||
|
||||
// 수직 정렬 - 공식 문서: {vertical: "center"}
|
||||
if (alignment.vertical) {
|
||||
luckyStyle.vt =
|
||||
alignment.vertical === "top"
|
||||
? 1
|
||||
: alignment.vertical === "center"
|
||||
? 2
|
||||
: alignment.vertical === "bottom"
|
||||
? 3
|
||||
: 2;
|
||||
}
|
||||
|
||||
// 텍스트 줄바꿈 - 공식 문서: {wrapText: true}
|
||||
if (alignment.wrapText) {
|
||||
luckyStyle.tb = 1;
|
||||
}
|
||||
|
||||
// 텍스트 회전 - 공식 문서: {textRotation: 90}
|
||||
if (alignment.textRotation) {
|
||||
luckyStyle.tr = alignment.textRotation;
|
||||
}
|
||||
}
|
||||
|
||||
// 🎨 테두리 스타일 변환 - 공식 문서 border 속성
|
||||
if (xlsxStyle.border) {
|
||||
const border = xlsxStyle.border;
|
||||
luckyStyle.bd = {};
|
||||
|
||||
// 상단 테두리 - 공식 문서: {top: {style: "thin", color: {rgb: "000000"}}}
|
||||
if (border.top) {
|
||||
luckyStyle.bd.t = {
|
||||
style: convertBorderStyleToLuckysheet(border.top.style || "thin"),
|
||||
color: convertXlsxColorToLuckysheet(border.top.color) || "rgb(0,0,0)",
|
||||
};
|
||||
}
|
||||
|
||||
// 하단 테두리
|
||||
if (border.bottom) {
|
||||
luckyStyle.bd.b = {
|
||||
style: convertBorderStyleToLuckysheet(border.bottom.style || "thin"),
|
||||
color:
|
||||
convertXlsxColorToLuckysheet(border.bottom.color) || "rgb(0,0,0)",
|
||||
};
|
||||
}
|
||||
|
||||
// 좌측 테두리
|
||||
if (border.left) {
|
||||
luckyStyle.bd.l = {
|
||||
style: convertBorderStyleToLuckysheet(border.left.style || "thin"),
|
||||
color: convertXlsxColorToLuckysheet(border.left.color) || "rgb(0,0,0)",
|
||||
};
|
||||
}
|
||||
|
||||
// 우측 테두리
|
||||
if (border.right) {
|
||||
luckyStyle.bd.r = {
|
||||
style: convertBorderStyleToLuckysheet(border.right.style || "thin"),
|
||||
color: convertXlsxColorToLuckysheet(border.right.color) || "rgb(0,0,0)",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 🎨 숫자 포맷 변환 - 공식 문서 numFmt 속성
|
||||
if (xlsxStyle.numFmt) {
|
||||
// numFmt는 문자열 또는 숫자일 수 있음
|
||||
luckyStyle.ct = {
|
||||
fa: xlsxStyle.numFmt,
|
||||
t: "n", // 숫자 타입
|
||||
};
|
||||
}
|
||||
|
||||
return luckyStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 타입 검증
|
||||
*/
|
||||
@@ -99,7 +387,7 @@ function sanitizeSheetName(sheetName: string): string {
|
||||
|
||||
sanitized = sanitized.replace(/[\\\/\*\?\[\]]/g, "_");
|
||||
|
||||
return sanitized || "Sheet1";
|
||||
return sanitized || "Sheet";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,9 +464,9 @@ function convertSheetJSToLuckyExcel(workbook: any): SheetData[] {
|
||||
],
|
||||
options: {
|
||||
showtoolbar: true,
|
||||
showinfobar: false,
|
||||
showinfobar: true,
|
||||
showsheetbar: true,
|
||||
showstatisticBar: false,
|
||||
showstatisticBar: true,
|
||||
allowCopy: true,
|
||||
allowEdit: true,
|
||||
enableAddRow: true,
|
||||
@@ -218,9 +506,9 @@ function convertSheetJSToLuckyExcel(workbook: any): SheetData[] {
|
||||
],
|
||||
options: {
|
||||
showtoolbar: true,
|
||||
showinfobar: false,
|
||||
showinfobar: true,
|
||||
showsheetbar: true,
|
||||
showstatisticBar: false,
|
||||
showstatisticBar: true,
|
||||
allowCopy: true,
|
||||
allowEdit: true,
|
||||
enableAddRow: true,
|
||||
@@ -276,22 +564,45 @@ function convertSheetJSToLuckyExcel(workbook: any): SheetData[] {
|
||||
c: col,
|
||||
v: {
|
||||
v: cellValue,
|
||||
m: String(cellValue),
|
||||
m: cell.w || String(cellValue), // 포맷팅된 텍스트 우선 사용
|
||||
ct: { fa: "General", t: "g" },
|
||||
},
|
||||
};
|
||||
|
||||
// 🎨 xlsx-js-style 스타일 정보 처리
|
||||
if (cell.s) {
|
||||
console.log(
|
||||
`🎨 셀 ${cellAddress}에 스타일 정보 발견:`,
|
||||
JSON.stringify(cell.s, null, 2),
|
||||
);
|
||||
const convertedStyle = convertXlsxStyleToLuckysheet(cell.s);
|
||||
console.log(
|
||||
`🎨 변환된 Luckysheet 스타일:`,
|
||||
JSON.stringify(convertedStyle, null, 2),
|
||||
);
|
||||
luckyCell.v.s = convertedStyle;
|
||||
}
|
||||
|
||||
// 셀 타입에 따른 추가 처리
|
||||
if (cell.t === "s") {
|
||||
luckyCell.v.ct.t = "s";
|
||||
} else if (cell.t === "n") {
|
||||
luckyCell.v.ct.t = "n";
|
||||
// 숫자 포맷 처리
|
||||
if (cell.z) {
|
||||
luckyCell.v.ct.fa = cell.z;
|
||||
}
|
||||
} else if (cell.t === "d") {
|
||||
luckyCell.v.ct.t = "d";
|
||||
// 날짜 포맷 처리
|
||||
if (cell.z) {
|
||||
luckyCell.v.ct.fa = cell.z;
|
||||
}
|
||||
} else if (cell.t === "b") {
|
||||
luckyCell.v.ct.t = "b";
|
||||
}
|
||||
|
||||
// 수식 처리
|
||||
if (cell.f) {
|
||||
luckyCell.v.f = cell.f;
|
||||
}
|
||||
@@ -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 객체 생성
|
||||
const sheetData: SheetData = {
|
||||
id: `sheet_${index}`,
|
||||
@@ -319,13 +672,17 @@ function convertSheetJSToLuckyExcel(workbook: any): SheetData[] {
|
||||
order: index,
|
||||
row: maxRow,
|
||||
column: maxCol,
|
||||
// 🎨 xlsx-js-style로부터 추가된 스타일 정보들
|
||||
...(mergeData.length > 0 && { merge: mergeData }), // 병합 셀
|
||||
...(Object.keys(colhidden).length > 0 && { colhidden }), // 숨겨진 열
|
||||
...(Object.keys(rowhidden).length > 0 && { rowhidden }), // 숨겨진 행
|
||||
},
|
||||
],
|
||||
options: {
|
||||
showtoolbar: true,
|
||||
showinfobar: false,
|
||||
showinfobar: true,
|
||||
showsheetbar: true,
|
||||
showstatisticBar: false,
|
||||
showstatisticBar: true,
|
||||
allowCopy: true,
|
||||
allowEdit: true,
|
||||
enableAddRow: true,
|
||||
@@ -361,9 +718,9 @@ function convertSheetJSToLuckyExcel(workbook: any): SheetData[] {
|
||||
],
|
||||
options: {
|
||||
showtoolbar: true,
|
||||
showinfobar: false,
|
||||
showinfobar: true,
|
||||
showsheetbar: true,
|
||||
showstatisticBar: false,
|
||||
showstatisticBar: true,
|
||||
allowCopy: true,
|
||||
allowEdit: true,
|
||||
enableAddRow: true,
|
||||
@@ -428,7 +785,7 @@ async function processFileWithSheetJSToXLSX(
|
||||
const fileName = file.name.toLowerCase();
|
||||
const isCSV = fileName.endsWith(".csv");
|
||||
const isXLS = fileName.endsWith(".xls");
|
||||
const isXLSX = fileName.endsWith(".xlsx");
|
||||
// const isXLSX = fileName.endsWith(".xlsx");
|
||||
|
||||
// 1단계: SheetJS로 파일 읽기
|
||||
let workbook: any;
|
||||
@@ -444,11 +801,14 @@ async function processFileWithSheetJSToXLSX(
|
||||
raw: false,
|
||||
});
|
||||
} else {
|
||||
// XLS/XLSX 파일 처리 - 관대한 옵션으로 읽기
|
||||
// XLS/XLSX 파일 처리 - 스타일 정보 강제 추출 옵션
|
||||
console.log(`📊 ${isXLS ? "XLS" : "XLSX"} 파일을 SheetJS로 읽는 중...`);
|
||||
workbook = XLSX.read(arrayBuffer);
|
||||
|
||||
// Sheets가 없고 SheetNames만 있는 경우 재시도
|
||||
workbook = XLSX.read(arrayBuffer, {
|
||||
cellStyles: true, // 스타일 정보 보존
|
||||
cellNF: true, // 숫자 형식 보존 (스타일의 일부)
|
||||
bookProps: true, // 문서 속성 보존 (스타일 정보 포함 가능)
|
||||
WTF: true, // 더 관대한 파싱
|
||||
});
|
||||
}
|
||||
} catch (readError) {
|
||||
console.error("❌ SheetJS 파일 읽기 실패:", readError);
|
||||
@@ -467,14 +827,14 @@ async function processFileWithSheetJSToXLSX(
|
||||
throw new Error("워크북을 생성할 수 없습니다.");
|
||||
}
|
||||
|
||||
// workbook.Sheets 존재 및 타입 검증
|
||||
if (!workbook.Sheets || typeof workbook.Sheets !== "object") {
|
||||
throw new Error("유효한 시트가 없습니다.");
|
||||
// 기본 검증만 수행
|
||||
if (!workbook.SheetNames || workbook.SheetNames.length === 0) {
|
||||
throw new Error("시트 이름 정보가 없습니다.");
|
||||
}
|
||||
|
||||
// workbook.SheetNames 배열 검증
|
||||
if (!Array.isArray(workbook.SheetNames) || workbook.SheetNames.length === 0) {
|
||||
throw new Error("시트 이름 정보가 없습니다.");
|
||||
// Sheets 객체가 없으면 빈 객체로 초기화
|
||||
if (!workbook.Sheets) {
|
||||
workbook.Sheets = {};
|
||||
}
|
||||
|
||||
console.log("✅ SheetJS 워크북 읽기 성공:", {
|
||||
@@ -482,6 +842,11 @@ async function processFileWithSheetJSToXLSX(
|
||||
sheetCount: workbook.SheetNames.length,
|
||||
});
|
||||
|
||||
// 🎨 스타일 정보 상세 분석 (개발 모드에서만)
|
||||
if (import.meta.env.DEV) {
|
||||
analyzeSheetStyles(workbook);
|
||||
}
|
||||
|
||||
// 2단계: 워크북을 XLSX ArrayBuffer로 변환
|
||||
let xlsxArrayBuffer: ArrayBuffer;
|
||||
try {
|
||||
@@ -489,83 +854,10 @@ async function processFileWithSheetJSToXLSX(
|
||||
const xlsxData = XLSX.write(workbook, {
|
||||
type: "array",
|
||||
bookType: "xlsx",
|
||||
compression: true,
|
||||
|
||||
// 🎨 스타일 정보 완전 보존
|
||||
cellStyles: true, // 셀 스타일, 색상, 폰트, 테두리 등
|
||||
cellDates: true, // 날짜 포맷 정보
|
||||
bookSST: true, // 문자열 테이블 (호환성)
|
||||
|
||||
// 📊 워크북 정보 보존
|
||||
Props: workbook.Props || {},
|
||||
|
||||
// 🎭 테마 정보 보존 (존재하는 경우)
|
||||
...(workbook.themeXLSX && { themeXLSX: workbook.themeXLSX }),
|
||||
cellStyles: true, // 스타일 정보 보존
|
||||
});
|
||||
|
||||
// 📋 XLSX.write 완료 후 workbook 상세 정보 로깅
|
||||
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("📋 =================================");
|
||||
console.log(`✅ XLSX 변환 완료: ${xlsxData.length} bytes`);
|
||||
|
||||
// xlsxData는 Uint8Array이므로 ArrayBuffer로 변환
|
||||
if (xlsxData instanceof Uint8Array) {
|
||||
@@ -585,30 +877,6 @@ async function processFileWithSheetJSToXLSX(
|
||||
}
|
||||
|
||||
console.log(`✅ XLSX 변환 완료: ${xlsxArrayBuffer.byteLength} bytes`);
|
||||
|
||||
// ⏱️ ArrayBuffer 변환 완료 확인 및 검증
|
||||
console.log("⏱️ ArrayBuffer 변환 검증 중...");
|
||||
|
||||
// ArrayBuffer 무결성 검증
|
||||
if (!xlsxArrayBuffer || xlsxArrayBuffer.byteLength === 0) {
|
||||
throw new Error("ArrayBuffer 변환 실패: 빈 버퍼");
|
||||
}
|
||||
|
||||
// XLSX 파일 시그니처 사전 검증
|
||||
const uint8Check = new Uint8Array(xlsxArrayBuffer);
|
||||
const signatureCheck = Array.from(uint8Check.slice(0, 4))
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join(" ");
|
||||
|
||||
if (signatureCheck !== "50 4b 03 04") {
|
||||
console.warn(
|
||||
`⚠️ 잘못된 XLSX 시그니처: ${signatureCheck} (예상: 50 4b 03 04)`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ ArrayBuffer 검증 완료: ${xlsxArrayBuffer.byteLength} bytes, 시그니처: ${signatureCheck}`,
|
||||
);
|
||||
} catch (writeError) {
|
||||
console.error("❌ XLSX 변환 실패:", writeError);
|
||||
throw new Error(
|
||||
@@ -629,53 +897,8 @@ async function processFileWithSheetJSToXLSX(
|
||||
}
|
||||
|
||||
// 원본 파일명에서 확장자를 .xlsx로 변경
|
||||
const xlsxFileName = file.name.replace(/\.(csv|xls|xlsx)$/i, ".xlsx");
|
||||
|
||||
// 🔍 LuckyExcel로 전달되는 파일 정보 출력
|
||||
console.log("📋 =================================");
|
||||
console.log("📋 LuckyExcel로 전달되는 파일 정보:");
|
||||
console.log("📋 =================================");
|
||||
console.log("📋 타이밍:", new Date().toISOString());
|
||||
console.log("📋 원본 파일명:", file.name);
|
||||
console.log("📋 변환된 파일명:", xlsxFileName);
|
||||
console.log("📋 ArrayBuffer 크기:", xlsxArrayBuffer.byteLength, "bytes");
|
||||
console.log("📋 ArrayBuffer 타입:", xlsxArrayBuffer.constructor.name);
|
||||
|
||||
// ArrayBuffer의 처음 100바이트를 16진수로 출력 (헥스 덤프)
|
||||
const uint8View = new Uint8Array(xlsxArrayBuffer);
|
||||
const firstBytes = Array.from(
|
||||
uint8View.slice(0, Math.min(100, uint8View.length)),
|
||||
)
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join(" ");
|
||||
console.log("📋 ArrayBuffer 처음 100바이트 (hex):", firstBytes);
|
||||
|
||||
// XLSX 파일 시그니처 확인 (PK\x03\x04 또는 50 4B 03 04)
|
||||
const signature = Array.from(uint8View.slice(0, 4))
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join(" ");
|
||||
console.log(
|
||||
"📋 파일 시그니처:",
|
||||
signature,
|
||||
signature === "50 4b 03 04" ? "(✅ 유효한 XLSX)" : "(❌ 잘못된 시그니처)",
|
||||
);
|
||||
console.log("📋 =================================");
|
||||
|
||||
// 🚀 LuckyExcel 호출 직전 최종 검증
|
||||
console.log("🚀 LuckyExcel 호출 직전 최종 검증:");
|
||||
console.log("🚀 ArrayBuffer 타입:", typeof xlsxArrayBuffer);
|
||||
console.log("🚀 ArrayBuffer 생성자 확인:", xlsxArrayBuffer.constructor.name);
|
||||
console.log("🚀 ArrayBuffer 크기:", xlsxArrayBuffer.byteLength);
|
||||
console.log("🚀 ArrayBuffer.isView:", ArrayBuffer.isView(xlsxArrayBuffer));
|
||||
console.log("🚀 fileName:", xlsxFileName, "타입:", typeof xlsxFileName);
|
||||
|
||||
console.log("🚀 LuckyExcel 객체:", typeof LuckyExcel);
|
||||
console.log(
|
||||
"🚀 transformExcelToLucky 함수:",
|
||||
typeof (LuckyExcel as any).transformExcelToLucky,
|
||||
);
|
||||
|
||||
console.log("🚀 LuckyExcel 호출 시작...");
|
||||
// const xlsxFileName = file.name.replace(/\.(csv|xls|xlsx)$/i, ".xlsx");
|
||||
console.log("🍀 LuckyExcel 처리 시작...");
|
||||
|
||||
// Promise를 사용한 LuckyExcel 처리
|
||||
return new Promise<{ sheets: SheetData[]; xlsxBuffer: ArrayBuffer }>(
|
||||
@@ -683,56 +906,16 @@ async function processFileWithSheetJSToXLSX(
|
||||
try {
|
||||
// LuckyExcel API는 (arrayBuffer, successCallback, errorCallback) 형태로 호출
|
||||
// 공식 문서: LuckyExcel.transformExcelToLucky(file, successCallback, errorCallback)
|
||||
(LuckyExcel as any).transformExcelToLucky(
|
||||
(window.LuckyExcel as any).transformExcelToLucky(
|
||||
xlsxArrayBuffer,
|
||||
// 성공 콜백 함수 (두 번째 매개변수)
|
||||
(exportJson: any, luckysheetfile: any) => {
|
||||
(exportJson: any, _luckysheetfile: any) => {
|
||||
try {
|
||||
console.log("🍀 =================================");
|
||||
console.log("🍀 LuckyExcel 변환 결과 상세 정보:");
|
||||
console.log("🍀 =================================");
|
||||
console.log("🍀 원본 파일명:", xlsxFileName);
|
||||
console.log("🍀 exportJson 존재:", !!exportJson);
|
||||
console.log("🍀 exportJson 타입:", typeof exportJson);
|
||||
|
||||
if (exportJson) {
|
||||
console.log("🍀 exportJson 전체 구조:", exportJson);
|
||||
console.log("🍀 exportJson.sheets 존재:", !!exportJson.sheets);
|
||||
console.log(
|
||||
"🍀 exportJson.sheets 타입:",
|
||||
typeof exportJson.sheets,
|
||||
);
|
||||
console.log(
|
||||
"🍀 exportJson.sheets 배열 여부:",
|
||||
Array.isArray(exportJson.sheets),
|
||||
);
|
||||
console.log("🍀 시트 개수:", exportJson?.sheets?.length || 0);
|
||||
|
||||
if (exportJson.sheets && Array.isArray(exportJson.sheets)) {
|
||||
exportJson.sheets.forEach((sheet: any, index: number) => {
|
||||
console.log(`🍀 시트 ${index + 1}:`, {
|
||||
name: sheet.name,
|
||||
row: sheet.row,
|
||||
column: sheet.column,
|
||||
celldata길이: sheet.celldata?.length || 0,
|
||||
키목록: Object.keys(sheet),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log("🍀 luckysheetfile 존재:", !!luckysheetfile);
|
||||
console.log("🍀 luckysheetfile 타입:", typeof luckysheetfile);
|
||||
if (luckysheetfile) {
|
||||
console.log("🍀 luckysheetfile 구조:", luckysheetfile);
|
||||
}
|
||||
console.log("🍀 =================================");
|
||||
|
||||
console.log("🔍 LuckyExcel 변환 결과:", {
|
||||
hasExportJson: !!exportJson,
|
||||
hasSheets: !!exportJson?.sheets,
|
||||
sheetsCount: exportJson?.sheets?.length || 0,
|
||||
});
|
||||
console.log(
|
||||
"🍀 LuckyExcel 변환 성공:",
|
||||
exportJson?.sheets?.length || 0,
|
||||
"개 시트",
|
||||
);
|
||||
|
||||
// 데이터 유효성 검사
|
||||
if (
|
||||
@@ -818,7 +1001,7 @@ async function processFileWithSheetJSToXLSX(
|
||||
showtoolbar: true,
|
||||
showinfobar: false,
|
||||
showsheetbar: true,
|
||||
showstatisticBar: false,
|
||||
showstatisticBar: true,
|
||||
allowCopy: true,
|
||||
allowEdit: true,
|
||||
enableAddRow: true,
|
||||
@@ -881,10 +1064,92 @@ async function processFileWithSheetJSToXLSX(
|
||||
}
|
||||
|
||||
/**
|
||||
* 엑셀 파일을 SheetData 배열로 변환 (개선된 버전)
|
||||
* - 모든 파일을 SheetJS로 읽은 후 XLSX로 변환
|
||||
* - 변환된 XLSX를 LuckyExcel로 처리
|
||||
* - 실패 시 SheetJS 직접 변환으로 Fallback
|
||||
* XLSX 파일을 바로 LuckyExcel로 처리 (공식 예제 순서 준수)
|
||||
* - 공식 문서 예제를 그대로 따름: LuckyExcel.transformExcelToLucky → luckysheet.create
|
||||
*/
|
||||
async function processXLSXWithLuckyExcel(
|
||||
file: File,
|
||||
): Promise<{ sheets: SheetData[]; xlsxBuffer: ArrayBuffer }> {
|
||||
console.log("🍀 XLSX 파일을 LuckyExcel로 직접 처리 시작...");
|
||||
console.log(`📊 XLSX 파일: ${file.name}, 크기: ${file.size} bytes`);
|
||||
|
||||
// Promise를 사용한 LuckyExcel 처리 (공식 예제 순서)
|
||||
return new Promise<{ sheets: SheetData[]; xlsxBuffer: ArrayBuffer }>(
|
||||
(resolve, reject) => {
|
||||
// Make sure to get the xlsx file first, and then use the global method window.LuckyExcel to convert
|
||||
(window.LuckyExcel as any).transformExcelToLucky(
|
||||
file,
|
||||
// After obtaining the converted table data, use luckysheet to initialize or update the existing luckysheet workbook
|
||||
function (exportJson: any, _luckysheetfile: any) {
|
||||
console.log(
|
||||
"🍀 LuckyExcel 변환 성공:",
|
||||
exportJson?.sheets?.length || 0,
|
||||
"개 시트",
|
||||
);
|
||||
|
||||
// ArrayBuffer는 성공 시에만 생성
|
||||
file
|
||||
.arrayBuffer()
|
||||
.then((arrayBuffer) => {
|
||||
// exportJson.sheets를 SheetData 형식으로 단순 변환
|
||||
const sheets: SheetData[] = exportJson.sheets.map(
|
||||
(sheet: any, index: number) => ({
|
||||
id: `sheet_${index}`,
|
||||
name: sheet.name || `Sheet${index + 1}`,
|
||||
data: [[""]], // 실제 데이터는 luckysheet에서 처리
|
||||
xlsxBuffer: arrayBuffer,
|
||||
config: {
|
||||
container: `luckysheet_${index}`,
|
||||
title: exportJson.info?.name || file.name,
|
||||
lang: "ko",
|
||||
// Note: Luckysheet needs to introduce a dependency package and initialize the table container before it can be used
|
||||
data: exportJson.sheets, // exportJson.sheets를 그대로 전달
|
||||
// 공식 예제에 따른 설정
|
||||
...(exportJson.info?.name && {
|
||||
title: exportJson.info.name,
|
||||
}),
|
||||
...(exportJson.info?.creator && {
|
||||
userInfo: exportJson.info.creator,
|
||||
}),
|
||||
options: {
|
||||
showtoolbar: true,
|
||||
showinfobar: false,
|
||||
showsheetbar: true,
|
||||
showstatisticBar: true,
|
||||
allowCopy: true,
|
||||
allowEdit: true,
|
||||
enableAddRow: true,
|
||||
enableAddCol: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
console.log(
|
||||
"✅ XLSX 파일 LuckyExcel 처리 완료:",
|
||||
sheets.length,
|
||||
"개 시트",
|
||||
);
|
||||
resolve({ sheets, xlsxBuffer: arrayBuffer });
|
||||
})
|
||||
.catch((bufferError) => {
|
||||
reject(new Error(`ArrayBuffer 생성 실패: ${bufferError}`));
|
||||
});
|
||||
},
|
||||
// Import failed. Is your file a valid xlsx?
|
||||
function (err: any) {
|
||||
console.error("❌ LuckyExcel 변환 실패:", err);
|
||||
reject(new Error(`XLSX 파일 변환 실패: ${err}`));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 엑셀 파일을 SheetData 배열로 변환 (파일 형식별 최적화 버전)
|
||||
* - XLSX: LuckyExcel 직접 처리 (스타일 정보 완전 보존)
|
||||
* - CSV/XLS: SheetJS → XLSX → LuckyExcel 파이프라인
|
||||
*/
|
||||
export async function processExcelFile(file: File): Promise<FileUploadResult> {
|
||||
try {
|
||||
@@ -920,8 +1185,22 @@ export async function processExcelFile(file: File): Promise<FileUploadResult> {
|
||||
`📁 파일 처리 시작: ${file.name} (${isCSV ? "CSV" : isXLS ? "XLS" : "XLSX"})`,
|
||||
);
|
||||
|
||||
// 통합된 처리 방식: SheetJS → XLSX → LuckyExcel
|
||||
const { sheets, xlsxBuffer } = await processFileWithSheetJSToXLSX(file);
|
||||
let sheets: SheetData[];
|
||||
let xlsxBuffer: ArrayBuffer;
|
||||
|
||||
if (isXLSX) {
|
||||
// 🍀 XLSX 파일: LuckyExcel 직접 처리 (스타일 정보 완전 보존)
|
||||
console.log("🍀 XLSX 파일 - LuckyExcel 직접 처리 방식 사용");
|
||||
const result = await processXLSXWithLuckyExcel(file);
|
||||
sheets = result.sheets;
|
||||
xlsxBuffer = result.xlsxBuffer;
|
||||
} else {
|
||||
// 📊 CSV/XLS 파일: SheetJS → XLSX → LuckyExcel 파이프라인
|
||||
console.log(`📊 ${isCSV ? "CSV" : "XLS"} 파일 - SheetJS 파이프라인 사용`);
|
||||
const result = await processFileWithSheetJSToXLSX(file);
|
||||
sheets = result.sheets;
|
||||
xlsxBuffer = result.xlsxBuffer;
|
||||
}
|
||||
|
||||
if (!sheets || sheets.length === 0) {
|
||||
return {
|
||||
@@ -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("XLSX 파일 처리 실패") ||
|
||||
error.message.includes("XLSX 파일 변환 실패")
|
||||
) {
|
||||
errorMessage = error.message;
|
||||
} 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 {
|
||||
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,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src"],
|
||||
"exclude": ["**/__tests__/**", "**/*.test.ts", "**/*.test.tsx"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
{
|
||||
"files": [],
|
||||
"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/
|
||||
export default defineConfig({
|
||||
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
|
||||
test: {
|
||||
globals: true,
|
||||
|
||||
Reference in New Issue
Block a user