유니버CE 초기화 테스트 완료

This commit is contained in:
sheetEasy AI Team
2025-06-24 14:15:09 +09:00
parent d9a198a157
commit ba58aaabf5
19 changed files with 6569 additions and 1561 deletions

View File

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

5175
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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"
variant={showTestViewer ? "default" : "outline"}
size="sm"
onClick={resetApp}
className="text-gray-600 hover:text-gray-800"
onClick={() => setShowTestViewer(!showTestViewer)}
className="bg-green-500 hover:bg-green-600 text-white border-green-500"
>
🧪
</Button>
</>
)}
{!hasSheetData && (
{!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>

View File

@@ -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>
);
}

View File

@@ -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 초기화
// 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) => {
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,
// })) || [],
// });
// 공식 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,
console.log("🍀 LuckyExcel 변환 성공!");
console.log("🍀 exportJson:", exportJson);
console.log("🍀 luckysheetfile:", luckysheetfile);
resolve(exportJson);
},
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)
}`,
);
setIsConverting(false);
setIsInitialized(false);
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: 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 {
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>

View 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;

View File

@@ -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();

View File

@@ -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";

View File

@@ -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;
}
}

View File

@@ -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 />);

View File

@@ -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,12 +30,28 @@ 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) => {
transformExcelToLucky: vi.fn(
(_arrayBuffer, successCallback, _errorCallback) => {
// 성공적인 변환 결과 모킹
const mockResult = {
sheets: [
@@ -82,9 +98,12 @@ vi.mock("luckyexcel", () => ({
],
};
// 비동기 콜백 호출
setTimeout(() => callback(mockResult, null), 0);
}),
// 성공 콜백 비동기 호출 (ArrayBuffer 매개변수 대응)
if (typeof successCallback === "function") {
setTimeout(() => successCallback(mockResult, null), 0);
}
},
),
}));
// 파일 생성 도우미 함수

View File

@@ -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,
"🍀 LuckyExcel 변환 성공:",
exportJson?.sheets?.length || 0,
"개 시트",
);
console.log(
"🍀 exportJson.sheets 배열 여부:",
Array.isArray(exportJson.sheets),
);
console.log("🍀 시트 개수:", exportJson?.sheets?.length || 0);
if (exportJson.sheets && Array.isArray(exportJson.sheets)) {
exportJson.sheets.forEach((sheet: any, index: number) => {
console.log(`🍀 시트 ${index + 1}:`, {
name: sheet.name,
row: sheet.row,
column: sheet.column,
celldata길이: sheet.celldata?.length || 0,
키목록: Object.keys(sheet),
});
});
}
}
console.log("🍀 luckysheetfile 존재:", !!luckysheetfile);
console.log("🍀 luckysheetfile 타입:", typeof luckysheetfile);
if (luckysheetfile) {
console.log("🍀 luckysheetfile 구조:", luckysheetfile);
}
console.log("🍀 =================================");
console.log("🔍 LuckyExcel 변환 결과:", {
hasExportJson: !!exportJson,
hasSheets: !!exportJson?.sheets,
sheetsCount: exportJson?.sheets?.length || 0,
});
// 데이터 유효성 검사
if (
@@ -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
View 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
View File

@@ -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 {};

View File

@@ -22,5 +22,6 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
"include": ["src"],
"exclude": ["**/__tests__/**", "**/*.test.ts", "**/*.test.tsx"]
}

View File

@@ -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"]
}

View File

@@ -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,