럭키시트 로드 가능, 옵션이 안불러짐

This commit is contained in:
sheetEasy AI Team
2025-06-21 13:58:49 +09:00
parent bc5b316f3c
commit de6b4debac
16 changed files with 4115 additions and 182 deletions

View File

@@ -0,0 +1,516 @@
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 시트 뷰어 컴포넌트
* - 참고 내용 기반: 완전한 라이브러리 로딩 순서 적용
* - functionlist 오류 방지를 위한 완전한 초기화
* - 필수 플러그인과 CSS 포함
*/
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);
// 스토어에서 시트 데이터 가져오기
const { sheets, activeSheetId, currentFile, setSelectedRange } =
useAppStore();
/**
* CDN 배포판 + functionlist 직접 초기화 방식
*/
const loadLuckysheetLibrary = useCallback((): Promise<void> => {
return new Promise((resolve, reject) => {
// 이미 로드된 경우
if (
window.luckysheet &&
window.LuckyExcel &&
window.$ &&
librariesLoaded
) {
console.log("📦 모든 라이브러리가 이미 로드됨");
resolve();
return;
}
// console.log("📦 CDN 배포판 + functionlist 직접 초기화 방식...");
const loadResource = (
type: "css" | "js",
src: string,
id: string,
): Promise<void> => {
return new Promise((resourceResolve, resourceReject) => {
// 이미 로드된 리소스 체크
if (document.querySelector(`[data-luckysheet-id="${id}"]`)) {
// console.log(`📦 ${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 = () => {
// console.log(`✅ ${id} CSS 로드 완료`);
resourceResolve();
};
link.onerror = (error) => {
// console.error(`❌ ${id} CSS 로드 실패:`, 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 = () => {
// console.log(`✅ ${id} JS 로드 완료`);
resourceResolve();
};
script.onerror = (error) => {
// console.error(`❌ ${id} JS 로드 실패:`, error);
resourceReject(new Error(`${id} JS 로드 실패`));
};
document.head.appendChild(script);
}
});
};
// CDN 배포판 로딩 + functionlist 직접 초기화
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 로드 (CDN 방식 - 공식 문서 순서 준수)
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",
);
// 👉 Plugin.js 로드 완료 후 바로 다음 단계로 진행
console.log("✅ Plugin.js 로드 완료");
// 4. LuckyExcel (Excel 파일 처리용 - 공식 문서 방식)
if (!window.LuckyExcel) {
await loadResource(
"js",
"https://cdn.jsdelivr.net/npm/luckyexcel/dist/luckyexcel.umd.js",
"luckyexcel",
);
}
// 5. Luckysheet 메인 (functionlist 준비 후 - 공식 문서 방식)
if (!window.luckysheet) {
await loadResource(
"js",
"https://cdn.jsdelivr.net/npm/luckysheet/dist/luckysheet.umd.js",
"luckysheet",
);
}
// 라이브러리 로드 후 검증
// console.log("🔍 라이브러리 로드 후 검증 중...");
// NOTE: plugin.js 가 실제 functionlist 를 채웠으므로 별도 지연 대기 불필요
// 필수 객체 검증
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"
),
};
// console.log("🔍 라이브러리 검증 결과:", validationResults);
if (
!validationResults.luckysheet ||
!validationResults.luckysheetCreate
) {
throw new Error(
"Luckysheet 객체가 올바르게 초기화되지 않았습니다.",
);
}
// 🚀 초기화 없이 바로 라이브러리 검증
console.log("🚀 라이브러리 로드 완료, 검증 중...");
setLibrariesLoaded(true);
// console.log("✅ CDN 배포판 + functionlist 초기화 완료");
resolve();
} catch (error) {
// console.error("❌ 라이브러리 로딩 실패:", error);
reject(error);
}
};
loadSequence();
});
}, [librariesLoaded]);
/**
* 참고 내용 기반: 올바른 데이터 구조로 Luckysheet 초기화
*/
const convertXLSXToLuckysheet = useCallback(
async (xlsxBuffer: ArrayBuffer, fileName: string) => {
if (!containerRef.current) {
console.warn("⚠️ 컨테이너가 없습니다.");
return;
}
try {
setIsConverting(true);
setError(null);
// console.log(
// "🔄 참고 내용 기반: XLSX → LuckyExcel → Luckysheet 변환 시작...",
// );
// 라이브러리 로드 확인
await loadLuckysheetLibrary();
// 기존 인스턴스 정리 (참고 내용 권장사항)
// console.log("🧹 기존 Luckysheet 인스턴스 정리...");
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 호출...");
// fileProcessor에서 이미 변환된 데이터를 사용하여 직접 생성
try {
console.log("🍀 이미 변환된 시트 데이터 사용:", currentFile?.name);
// 기존 인스턴스 정리
if (
window.luckysheet &&
typeof window.luckysheet.destroy === "function"
) {
window.luckysheet.destroy();
}
// store에서 sheets 데이터를 가져와서 luckysheet 형식으로 변환
const sheets = useAppStore.getState().sheets;
const luckysheetData = sheets.map((sheet, index) => ({
name: sheet.name,
index: index.toString(),
status: 1,
order: index,
celldata: sheet.config?.data?.[0]?.celldata || [],
row: sheet.config?.data?.[0]?.row || 50,
column: sheet.config?.data?.[0]?.column || 26,
}));
window.luckysheet.create({
container: containerRef.current?.id || "luckysheet-container",
showinfobar: false,
showtoolbar: false,
data: luckysheetData,
});
console.log("🎉 Luckysheet 생성 완료!");
setIsInitialized(true);
setIsConverting(false);
setError(null);
luckysheetRef.current = window.luckysheet;
} catch (createError) {
console.error("❌ Luckysheet 생성 실패:", createError);
setError(
`Luckysheet 생성 실패: ${createError instanceof Error ? createError.message : "알 수 없는 오류"}`,
);
setIsInitialized(false);
setIsConverting(false);
}
} catch (conversionError) {
console.error("❌ 변환 프로세스 실패:", conversionError);
setError(
`변환 프로세스에 실패했습니다: ${
conversionError instanceof Error
? conversionError.message
: String(conversionError)
}`,
);
setIsConverting(false);
setIsInitialized(false);
}
},
[loadLuckysheetLibrary, setSelectedRange],
);
/**
* DOM 컨테이너 준비 상태 체크 - useLayoutEffect로 동기적 체크
*/
useLayoutEffect(() => {
// console.log("🔍 useLayoutEffect: DOM 컨테이너 체크 시작...");
if (containerRef.current) {
// console.log("✅ DOM 컨테이너 준비 완료:", containerRef.current.id);
setIsContainerReady(true);
} else {
// console.warn("⚠️ useLayoutEffect: DOM 컨테이너가 아직 준비되지 않음");
}
}, []);
/**
* DOM 컨테이너 준비 상태 재체크 (fallback)
*/
useEffect(() => {
if (!isContainerReady) {
// console.log("🔄 useEffect: DOM 컨테이너 재체크...");
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 감지, Luckysheet 초기화 시작...", {
fileName: currentFile.name,
bufferSize: currentFile.xlsxBuffer.byteLength,
containerId: containerRef.current.id,
});
// 중복 실행 방지를 위해 즉시 상태 변경
setIsConverting(true);
// 변환된 XLSX ArrayBuffer를 사용하여 직접 변환
convertXLSXToLuckysheet(currentFile.xlsxBuffer, currentFile.name);
} else if (currentFile && !currentFile.xlsxBuffer) {
setError("파일 변환 데이터가 없습니다. 파일을 다시 업로드해주세요.");
}
}, [
currentFile?.xlsxBuffer,
currentFile?.name,
isContainerReady,
isInitialized,
isConverting,
]);
/**
* 컴포넌트 언마운트 시 정리
*/
useEffect(() => {
return () => {
if (luckysheetRef.current && window.luckysheet) {
// console.log("🧹 컴포넌트 언마운트: 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) {
convertXLSXToLuckysheet(
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 ? "XLSX 변환 중..." : "시트 초기화 중..."}
</div>
<div className="text-blue-500 text-sm">
{isConverting
? "변환된 XLSX를 Luckysheet로 처리하고 있습니다."
: "잠시만 기다려주세요."}
</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>
)}
</div>
);
}

View File

@@ -0,0 +1,838 @@
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 시트 뷰어 컴포넌트
* - 참고 내용 기반: 완전한 라이브러리 로딩 순서 적용
* - functionlist 오류 방지를 위한 완전한 초기화
* - 필수 플러그인과 CSS 포함
*/
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);
// 스토어에서 시트 데이터 가져오기
const { sheets, activeSheetId, currentFile, setSelectedRange } =
useAppStore();
/**
* CDN 배포판 + functionlist 직접 초기화 방식
*/
const loadLuckysheetLibrary = useCallback((): Promise<void> => {
return new Promise((resolve, reject) => {
// 이미 로드된 경우
if (
window.luckysheet &&
window.LuckyExcel &&
window.$ &&
librariesLoaded
) {
console.log("📦 모든 라이브러리가 이미 로드됨");
resolve();
return;
}
// console.log("📦 CDN 배포판 + functionlist 직접 초기화 방식...");
const loadResource = (
type: "css" | "js",
src: string,
id: string,
): Promise<void> => {
return new Promise((resourceResolve, resourceReject) => {
// 이미 로드된 리소스 체크
if (document.querySelector(`[data-luckysheet-id="${id}"]`)) {
// console.log(`📦 ${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 = () => {
// console.log(`✅ ${id} CSS 로드 완료`);
resourceResolve();
};
link.onerror = (error) => {
// console.error(`❌ ${id} CSS 로드 실패:`, 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 = () => {
// console.log(`✅ ${id} JS 로드 완료`);
resourceResolve();
};
script.onerror = (error) => {
// console.error(`❌ ${id} JS 로드 실패:`, error);
resourceReject(new Error(`${id} JS 로드 실패`));
};
document.head.appendChild(script);
}
});
};
// CDN 배포판 로딩 + functionlist 직접 초기화
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",
"/luckysheet/dist/plugins/css/pluginsCss.css",
"plugins-css",
);
await loadResource(
"css",
"/luckysheet/dist/plugins/plugins.css",
"plugins-main-css",
);
await loadResource(
"css",
"/luckysheet/dist/css/luckysheet.css",
"luckysheet-css",
);
await loadResource(
"css",
"/luckysheet/dist/assets/iconfont/iconfont.css",
"iconfont-css",
);
// 3. Plugin JS 먼저 로드 (functionlist 초기화 우선)
await loadResource(
"js",
"/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 준비 후)
if (!window.luckysheet) {
await loadResource(
"js",
"/luckysheet/dist/luckysheet.umd.js",
"luckysheet",
);
}
// 라이브러리 로드 후 검증
// console.log("🔍 라이브러리 로드 후 검증 중...");
// NOTE: plugin.js 가 실제 functionlist 를 채웠으므로 별도 지연 대기 불필요
// 필수 객체 검증
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"
),
};
// console.log("🔍 라이브러리 검증 결과:", validationResults);
if (
!validationResults.luckysheet ||
!validationResults.luckysheetCreate
) {
throw new Error(
"Luckysheet 객체가 올바르게 초기화되지 않았습니다.",
);
}
// 🔧 강력한 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 초기화 완료");
resolve();
} catch (error) {
// console.error("❌ 라이브러리 로딩 실패:", error);
reject(error);
}
};
loadSequence();
});
}, [librariesLoaded]);
/**
* 참고 내용 기반: 올바른 데이터 구조로 Luckysheet 초기화
*/
const convertXLSXToLuckysheet = useCallback(
async (xlsxBuffer: ArrayBuffer, fileName: string) => {
if (!containerRef.current) {
console.warn("⚠️ 컨테이너가 없습니다.");
return;
}
try {
setIsConverting(true);
setError(null);
// console.log(
// "🔄 참고 내용 기반: XLSX → LuckyExcel → Luckysheet 변환 시작...",
// );
// 라이브러리 로드 확인
await loadLuckysheetLibrary();
// 기존 인스턴스 정리 (참고 내용 권장사항)
// console.log("🧹 기존 Luckysheet 인스턴스 정리...");
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 호출...");
// 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,
// })) || [],
// });
// 공식 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)
}`,
);
setIsConverting(false);
setIsInitialized(false);
},
);
} catch (conversionError) {
console.error("❌ 변환 프로세스 실패:", conversionError);
setError(
`변환 프로세스에 실패했습니다: ${
conversionError instanceof Error
? conversionError.message
: String(conversionError)
}`,
);
setIsConverting(false);
setIsInitialized(false);
}
},
[loadLuckysheetLibrary, setSelectedRange],
);
/**
* DOM 컨테이너 준비 상태 체크 - useLayoutEffect로 동기적 체크
*/
useLayoutEffect(() => {
// console.log("🔍 useLayoutEffect: DOM 컨테이너 체크 시작...");
if (containerRef.current) {
// console.log("✅ DOM 컨테이너 준비 완료:", containerRef.current.id);
setIsContainerReady(true);
} else {
// console.warn("⚠️ useLayoutEffect: DOM 컨테이너가 아직 준비되지 않음");
}
}, []);
/**
* DOM 컨테이너 준비 상태 재체크 (fallback)
*/
useEffect(() => {
if (!isContainerReady) {
// console.log("🔄 useEffect: DOM 컨테이너 재체크...");
const timer = setTimeout(() => {
if (containerRef.current && !isContainerReady) {
// console.log("✅ useEffect: DOM 컨테이너 지연 준비 완료");
setIsContainerReady(true);
}
}, 100);
return () => clearTimeout(timer);
}
}, [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) {
// console.log("🔄 변환된 XLSX 감지, LuckyExcel → Luckysheet 시작...", {
// fileName: currentFile.name,
// bufferSize: currentFile.xlsxBuffer.byteLength,
// containerId: containerRef.current.id,
// });
// 변환된 XLSX ArrayBuffer를 사용하여 직접 변환 (블로그 포스트 방식)
convertXLSXToLuckysheet(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,
]);
/**
* 컴포넌트 언마운트 시 정리
*/
useEffect(() => {
return () => {
if (luckysheetRef.current && window.luckysheet) {
// console.log("🧹 컴포넌트 언마운트: 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) {
convertXLSXToLuckysheet(
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 ? "XLSX 변환 중..." : "시트 초기화 중..."}
</div>
<div className="text-blue-500 text-sm">
{isConverting
? "변환된 XLSX를 Luckysheet로 처리하고 있습니다."
: "잠시만 기다려주세요."}
</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>
)}
</div>
);
}

View File

@@ -0,0 +1,402 @@
import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom";
import { vi } from "vitest";
import { SheetViewer } from "../SheetViewer";
import { useAppStore } from "../../../stores/useAppStore";
import type { SheetData } from "../../../types/sheet";
// Mock dependencies
vi.mock("../../../stores/useAppStore");
// Luckysheet 모킹
const mockLuckysheet = {
create: vi.fn(),
destroy: vi.fn(),
resize: vi.fn(),
getSheet: vi.fn(),
getAllSheets: vi.fn(),
setActiveSheet: vi.fn(),
};
// Window.luckysheet 모킹
Object.defineProperty(window, "luckysheet", {
value: mockLuckysheet,
writable: true,
});
// useAppStore 모킹 타입
const mockUseAppStore = vi.mocked(useAppStore);
// 기본 스토어 상태
const defaultStoreState = {
sheets: [],
activeSheetId: null,
currentFile: null,
setSelectedRange: vi.fn(),
isLoading: false,
error: null,
setLoading: vi.fn(),
setError: vi.fn(),
uploadFile: vi.fn(),
clearFileUploadErrors: vi.fn(),
resetApp: vi.fn(),
};
// 테스트용 시트 데이터
const mockSheetData: SheetData[] = [
{
id: "sheet_0",
name: "Sheet1",
data: [
["A1", "B1", "C1"],
["A2", "B2", "C2"],
],
config: {
container: "luckysheet_0",
title: "Sheet1",
lang: "ko",
data: [
{
name: "Sheet1",
index: "0",
celldata: [
{
r: 0,
c: 0,
v: { v: "A1", m: "A1", ct: { fa: "General", t: "g" } },
},
{
r: 0,
c: 1,
v: { v: "B1", m: "B1", ct: { fa: "General", t: "g" } },
},
],
status: 1,
order: 0,
row: 2,
column: 3,
},
],
options: {
showtoolbar: true,
showinfobar: false,
showsheetbar: true,
showstatisticBar: false,
allowCopy: true,
allowEdit: true,
enableAddRow: true,
enableAddCol: true,
},
},
},
];
describe("SheetViewer", () => {
beforeEach(() => {
vi.clearAllMocks();
mockUseAppStore.mockReturnValue(defaultStoreState);
});
afterEach(() => {
// DOM 정리
document.head.innerHTML = "";
});
describe("초기 렌더링", () => {
it("시트 데이터가 없을 때 적절한 메시지를 표시한다", () => {
mockUseAppStore.mockReturnValue({
...defaultStoreState,
sheets: [],
});
render(<SheetViewer />);
expect(screen.getByText("표시할 시트가 없습니다")).toBeInTheDocument();
expect(
screen.getByText("Excel 파일을 업로드해주세요."),
).toBeInTheDocument();
});
it("시트 데이터가 있을 때 로딩 상태를 표시한다", () => {
mockUseAppStore.mockReturnValue({
...defaultStoreState,
sheets: mockSheetData,
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
});
render(<SheetViewer />);
expect(screen.getByText("시트 로딩 중...")).toBeInTheDocument();
expect(screen.getByText("잠시만 기다려주세요.")).toBeInTheDocument();
});
it("Luckysheet 컨테이너가 올바르게 렌더링된다", () => {
mockUseAppStore.mockReturnValue({
...defaultStoreState,
sheets: mockSheetData,
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
});
render(<SheetViewer />);
const container = document.getElementById("luckysheet-container");
expect(container).toBeInTheDocument();
expect(container).toHaveClass("w-full", "h-full");
});
});
describe("Luckysheet 초기화", () => {
it("시트 데이터가 변경되면 Luckysheet를 초기화한다", async () => {
mockUseAppStore.mockReturnValue({
...defaultStoreState,
sheets: mockSheetData,
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
});
render(<SheetViewer />);
await waitFor(() => {
expect(mockLuckysheet.create).toHaveBeenCalled();
});
// create 호출 시 전달된 설정 확인
const createCall = mockLuckysheet.create.mock.calls[0];
expect(createCall).toBeDefined();
const config = createCall[0];
expect(config.container).toBe("luckysheet-container");
expect(config.title).toBe("test.xlsx");
expect(config.lang).toBe("ko");
expect(config.data).toHaveLength(1);
});
it("기존 Luckysheet 인스턴스가 있으면 제거한다", async () => {
mockUseAppStore.mockReturnValue({
...defaultStoreState,
sheets: mockSheetData,
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
});
const { rerender } = render(<SheetViewer />);
await waitFor(() => {
expect(mockLuckysheet.create).toHaveBeenCalledTimes(1);
});
// 시트 데이터 변경
const newSheetData: SheetData[] = [
{
...mockSheetData[0],
name: "NewSheet",
},
];
mockUseAppStore.mockReturnValue({
...defaultStoreState,
sheets: newSheetData,
currentFile: { name: "new.xlsx", size: 1000, uploadedAt: new Date() },
});
rerender(<SheetViewer />);
await waitFor(() => {
expect(mockLuckysheet.destroy).toHaveBeenCalled();
expect(mockLuckysheet.create).toHaveBeenCalledTimes(2);
});
});
});
describe("에러 처리", () => {
it("Luckysheet 초기화 실패 시 에러 메시지를 표시한다", async () => {
mockUseAppStore.mockReturnValue({
...defaultStoreState,
sheets: mockSheetData,
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
});
// Luckysheet.create에서 에러 발생 시뮬레이션
mockLuckysheet.create.mockImplementation(() => {
throw new Error("Luckysheet 초기화 실패");
});
render(<SheetViewer />);
await waitFor(() => {
expect(screen.getByText("시트 로드 오류")).toBeInTheDocument();
expect(
screen.getByText(/시트 초기화에 실패했습니다/),
).toBeInTheDocument();
});
// 다시 시도 버튼 확인
const retryButton = screen.getByRole("button", { name: "다시 시도" });
expect(retryButton).toBeInTheDocument();
});
it("다시 시도 버튼을 클릭하면 초기화를 재시도한다", async () => {
mockUseAppStore.mockReturnValue({
...defaultStoreState,
sheets: mockSheetData,
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
});
// 첫 번째 시도에서 실패
mockLuckysheet.create.mockImplementationOnce(() => {
throw new Error("첫 번째 실패");
});
render(<SheetViewer />);
await waitFor(() => {
expect(screen.getByText("시트 로드 오류")).toBeInTheDocument();
});
// 두 번째 시도에서 성공하도록 설정
mockLuckysheet.create.mockImplementationOnce(() => {
// 성공
});
const retryButton = screen.getByRole("button", { name: "다시 시도" });
retryButton.click();
await waitFor(() => {
expect(mockLuckysheet.create).toHaveBeenCalledTimes(2);
});
});
});
describe("이벤트 핸들링", () => {
it("셀 클릭 시 선택된 범위를 스토어에 저장한다", async () => {
const mockSetSelectedRange = vi.fn();
mockUseAppStore.mockReturnValue({
...defaultStoreState,
sheets: mockSheetData,
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
setSelectedRange: mockSetSelectedRange,
});
render(<SheetViewer />);
await waitFor(() => {
expect(mockLuckysheet.create).toHaveBeenCalled();
});
// create 호출 시 전달된 hook 확인
const createCall = mockLuckysheet.create.mock.calls[0];
const config = createCall[0];
// cellClick 핸들러 시뮬레이션
const cellClickHandler = config.hook.cellClick;
expect(cellClickHandler).toBeDefined();
const mockCell = {};
const mockPosition = { r: 1, c: 2 };
const mockSheetFile = { index: "0" };
cellClickHandler(mockCell, mockPosition, mockSheetFile);
expect(mockSetSelectedRange).toHaveBeenCalledWith({
range: {
startRow: 1,
startCol: 2,
endRow: 1,
endCol: 2,
},
sheetId: "0",
});
});
it("시트 활성화 시 활성 시트 ID를 업데이트한다", async () => {
mockUseAppStore.mockReturnValue({
...defaultStoreState,
sheets: mockSheetData,
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
});
// setActiveSheetId를 spy로 설정
const setActiveSheetIdSpy = vi.fn();
useAppStore.getState = vi.fn().mockReturnValue({
setActiveSheetId: setActiveSheetIdSpy,
});
render(<SheetViewer />);
await waitFor(() => {
expect(mockLuckysheet.create).toHaveBeenCalled();
});
// create 호출 시 전달된 hook 확인
const createCall = mockLuckysheet.create.mock.calls[0];
const config = createCall[0];
// sheetActivate 핸들러 시뮬레이션
const sheetActivateHandler = config.hook.sheetActivate;
expect(sheetActivateHandler).toBeDefined();
sheetActivateHandler(0, false, false);
expect(setActiveSheetIdSpy).toHaveBeenCalledWith("sheet_0");
});
});
describe("컴포넌트 생명주기", () => {
it("컴포넌트 언마운트 시 Luckysheet를 정리한다", async () => {
mockUseAppStore.mockReturnValue({
...defaultStoreState,
sheets: mockSheetData,
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
});
const { unmount } = render(<SheetViewer />);
await waitFor(() => {
expect(mockLuckysheet.create).toHaveBeenCalled();
});
unmount();
expect(mockLuckysheet.destroy).toHaveBeenCalled();
});
it("윈도우 리사이즈 시 Luckysheet 리사이즈를 호출한다", async () => {
mockUseAppStore.mockReturnValue({
...defaultStoreState,
sheets: mockSheetData,
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
});
render(<SheetViewer />);
await waitFor(() => {
expect(mockLuckysheet.create).toHaveBeenCalled();
});
// 윈도우 리사이즈 이벤트 시뮬레이션
window.dispatchEvent(new Event("resize"));
expect(mockLuckysheet.resize).toHaveBeenCalled();
});
});
describe("개발 모드 정보", () => {
it("개발 모드에서 시트 정보를 표시한다", () => {
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = "development";
mockUseAppStore.mockReturnValue({
...defaultStoreState,
sheets: mockSheetData,
activeSheetId: "sheet_0",
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
});
render(<SheetViewer />);
expect(screen.getByText("시트 개수: 1")).toBeInTheDocument();
expect(screen.getByText("활성 시트: sheet_0")).toBeInTheDocument();
process.env.NODE_ENV = originalEnv;
});
});
});