import React, { useRef, useEffect, useState, useCallback } from "react"; // 공식 문서 권장: presets 패키지 사용으로 간소화 import { createUniver, defaultTheme, LocaleType, merge, } from "@univerjs/presets"; import { UniverSheetsCorePreset } from "@univerjs/presets/preset-sheets-core"; import UniverPresetSheetsCoreEnUS from "@univerjs/presets/preset-sheets-core/locales/en-US"; import { UniverInstanceType } from "@univerjs/core"; // Presets CSS import import "@univerjs/presets/lib/styles/preset-sheets-core.css"; import { cn } from "../../lib/utils"; import LuckyExcel from "@zwight/luckyexcel"; import PromptInput from "./PromptInput"; import HistoryPanel from "../ui/historyPanel"; import { useAppStore } from "../../stores/useAppStore"; import { rangeToAddress } from "../../utils/cellUtils"; import { CellSelectionHandler } from "../../utils/cellSelectionHandler"; import { aiProcessor } from "../../utils/aiProcessor"; import type { HistoryEntry } from "../../types/ai"; // 전역 고유 키 생성 const GLOBAL_UNIVER_KEY = "__GLOBAL_UNIVER_INSTANCE__"; const GLOBAL_STATE_KEY = "__GLOBAL_UNIVER_STATE__"; // 전역 상태 인터페이스 interface GlobalUniverState { instance: any | null; // createUniver 반환 타입 univerAPI: any | null; // univerAPI 별도 저장 isInitializing: boolean; isDisposing: boolean; initializationPromise: Promise | null; lastContainerId: string | null; } // Window 객체에 전역 상태 확장 declare global { interface Window { [GLOBAL_UNIVER_KEY]: any | null; [GLOBAL_STATE_KEY]: GlobalUniverState; __UNIVER_DEBUG__: { getGlobalUniver: () => any | null; getGlobalState: () => GlobalUniverState; clearGlobalState: () => void; forceReset: () => void; completeCleanup: () => void; }; } } // 전역 상태 초기화 함수 const initializeGlobalState = (): GlobalUniverState => { if (!window[GLOBAL_STATE_KEY]) { window[GLOBAL_STATE_KEY] = { instance: null, univerAPI: null, isInitializing: false, isDisposing: false, initializationPromise: null, lastContainerId: null, }; } return window[GLOBAL_STATE_KEY]; }; // 전역 상태 가져오기 const getGlobalState = (): GlobalUniverState => { return window[GLOBAL_STATE_KEY] || initializeGlobalState(); }; /** * Presets 기반 강화된 전역 Univer 관리자 * 공식 문서 권장사항에 따라 createUniver 사용 */ const UniverseManager = { // 전역 인스턴스 생성 (완전 단일 인스턴스 보장) async createInstance(container: HTMLElement): Promise { const state = getGlobalState(); const containerId = container.id || `container-${Date.now()}`; console.log(`🚀 Univer 인스턴스 생성 요청 - Container: ${containerId}`); // 이미 존재하는 인스턴스가 있고 같은 컨테이너면 재사용 if ( state.instance && state.univerAPI && state.lastContainerId === containerId ) { console.log("✅ 기존 전역 Univer 인스턴스와 univerAPI 재사용"); return { univer: state.instance, univerAPI: state.univerAPI }; } // 초기화가 진행 중이면 대기 if (state.isInitializing && state.initializationPromise) { console.log("⏳ 기존 초기화 프로세스 대기 중..."); return state.initializationPromise; } // 새로운 초기화 시작 state.isInitializing = true; console.log("🔄 새로운 Univer 인스턴스 생성 시작"); // 기존 인스턴스 정리 if (state.instance) { try { console.log("🗑️ 기존 인스턴스 정리 중..."); await state.instance.dispose(); state.instance = null; window[GLOBAL_UNIVER_KEY] = null; } catch (error) { console.warn("⚠️ 기존 인스턴스 정리 중 오류:", error); } } // 초기화 Promise 생성 state.initializationPromise = (async () => { try { // 컨테이너 ID 설정 if (!container.id) { container.id = containerId; } console.log("🛠️ Presets 기반 Univer 인스턴스 생성 중..."); // 공식 문서 권장: createUniver 사용으로 대폭 간소화 const { univer, univerAPI } = createUniver({ locale: LocaleType.EN_US, locales: { [LocaleType.EN_US]: merge({}, UniverPresetSheetsCoreEnUS), }, theme: defaultTheme, presets: [ UniverSheetsCorePreset({ container: container, }), ], }); // 전역 상태 업데이트 (univerAPI도 함께 저장) state.instance = univer; state.univerAPI = univerAPI; state.lastContainerId = containerId; window[GLOBAL_UNIVER_KEY] = univer; console.log("✅ Presets 기반 Univer 인스턴스와 univerAPI 생성 완료"); return { univer, univerAPI }; } catch (error) { console.error("❌ Univer 인스턴스 생성 실패:", error); throw error; } finally { state.isInitializing = false; state.initializationPromise = null; } })(); return state.initializationPromise; }, // 전역 인스턴스 정리 async disposeInstance(): Promise { const state = getGlobalState(); if (state.isDisposing) { console.log("🔄 이미 dispose 진행 중..."); return; } if (!state.instance) { console.log("ℹ️ 정리할 인스턴스가 없음"); return; } state.isDisposing = true; try { console.log("🗑️ 전역 Univer 인스턴스 dispose 시작"); await state.instance.dispose(); state.instance = null; state.univerAPI = null; state.lastContainerId = null; window[GLOBAL_UNIVER_KEY] = null; console.log("✅ 전역 Univer 인스턴스 dispose 완료"); } catch (error) { console.error("❌ dispose 실패:", error); } finally { state.isDisposing = false; } }, // 현재 인스턴스 반환 getInstance(): any | null { const state = getGlobalState(); return state.instance || window[GLOBAL_UNIVER_KEY] || null; }, // 상태 확인 메서드들 isInitializing(): boolean { return getGlobalState().isInitializing || false; }, isDisposing(): boolean { return getGlobalState().isDisposing || false; }, // 강제 리셋 (디버깅용) forceReset(): void { const state = getGlobalState(); if (state.instance) { try { state.instance.dispose(); } catch (error) { console.warn("강제 리셋 중 dispose 오류:", error); } } // REDI 전역 상태 완전 정리 시도 try { // 브라우저의 전역 REDI 상태 정리 (가능한 경우) if (typeof window !== "undefined") { // REDI 관련 전역 객체들 정리 const globalKeys = Object.keys(window).filter( (key) => key.includes("redi") || key.includes("REDI") || key.includes("univerjs") || key.includes("univer"), ); globalKeys.forEach((key) => { try { delete (window as any)[key]; } catch (e) { console.warn(`전역 키 ${key} 정리 실패:`, e); } }); console.log("🧹 REDI 전역 상태 정리 시도 완료"); } } catch (error) { console.warn("⚠️ REDI 전역 상태 정리 중 오류:", error); } state.instance = null; state.univerAPI = null; state.isInitializing = false; state.isDisposing = false; state.initializationPromise = null; state.lastContainerId = null; window[GLOBAL_UNIVER_KEY] = null; console.log("🔄 전역 Univer 상태 강제 리셋 완료 (REDI 정리 포함)"); }, // 완전한 정리 메서드 (REDI 포함) async completeCleanup(): Promise { const state = getGlobalState(); if (state.isDisposing) { console.log("🔄 이미 정리 진행 중..."); return; } state.isDisposing = true; try { console.log("🗑️ 완전한 정리 시작 (REDI 포함)"); if (state.instance) { await state.instance.dispose(); } // 약간의 대기 후 REDI 상태 정리 await new Promise((resolve) => setTimeout(resolve, 100)); // REDI 전역 상태 정리 시도 this.forceReset(); console.log("✅ 완전한 정리 완료"); } catch (error) { console.error("❌ 완전한 정리 실패:", error); } finally { state.isDisposing = false; } }, }; // 전역 디버그 객체 설정을 위한 헬퍼 함수 (모듈 레벨 실행 방지) const setupDebugTools = (): void => { if (typeof window !== "undefined" && !window.__UNIVER_DEBUG__) { // 전역 상태 초기화 (필요한 경우에만) initializeGlobalState(); // 디버그 객체 설정 window.__UNIVER_DEBUG__ = { getGlobalUniver: () => UniverseManager.getInstance(), getGlobalState: () => getGlobalState(), clearGlobalState: () => { const state = getGlobalState(); Object.assign(state, { instance: null, univerAPI: null, isInitializing: false, isDisposing: false, initializationPromise: null, lastContainerId: null, }); window[GLOBAL_UNIVER_KEY] = null; console.log("🧹 전역 상태 정리 완료"); }, forceReset: () => UniverseManager.forceReset(), completeCleanup: () => UniverseManager.completeCleanup(), }; console.log("🐛 디버그 객체 설정 완료: window.__UNIVER_DEBUG__"); } }; /** * Univer CE + 파일 업로드 오버레이 * Window 객체 기반 완전한 단일 인스턴스 관리 */ const TestSheetViewer: React.FC = () => { const containerRef = useRef(null); const fileInputRef = useRef(null); const mountedRef = useRef(false); const [isInitialized, setIsInitialized] = useState(false); const [showUploadOverlay, setShowUploadOverlay] = useState(true); const [isDragOver, setIsDragOver] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [currentFile, setCurrentFile] = useState(null); const [prompt, setPrompt] = useState(""); const appStore = useAppStore(); // CellSelectionHandler 인스턴스 생성 const cellSelectionHandler = useRef(new CellSelectionHandler()); // 히스토리 관련 상태 추가 const [isHistoryOpen, setIsHistoryOpen] = useState(false); const [history, setHistory] = useState([]); // 히스토리 관련 핸들러 const handleHistoryToggle = () => { console.log("🔄 히스토리 토글:", !isHistoryOpen); setIsHistoryOpen(!isHistoryOpen); }; const handleHistoryClear = () => { if (window.confirm("모든 히스토리를 삭제하시겠습니까?")) { setHistory([]); } }; const handleHistoryReapply = (entry: HistoryEntry) => { // 히스토리 항목 재적용 로직 setPrompt(entry.prompt); console.log("🔄 히스토리 재적용:", entry); // TODO: 실제 액션 재실행 로직 구현 }; // 새 히스토리 항목 추가 함수 const addHistoryEntry = ( prompt: string, range: string, sheetName: string, actions: any[], status: "success" | "error" | "pending", error?: string, ) => { const newEntry: HistoryEntry = { id: `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, timestamp: new Date(), prompt, range, sheetName, actions, status, error, }; setHistory((prev) => [newEntry, ...prev]); // 최신 항목을 맨 위에 }; // Univer 초기화 함수 (Presets 기반) const initializeUniver = useCallback( async (workbookData?: any) => { if (!containerRef.current || !mountedRef.current) { console.error("❌ 컨테이너가 준비되지 않았습니다!"); return; } try { console.log("🚀 Presets 기반 Univer 초기화 시작"); // 전역 인스턴스 생성 또는 재사용 (Presets 사용) const { univer, univerAPI } = await UniverseManager.createInstance( containerRef.current, ); // 기본 워크북 데이터 const defaultWorkbook = { id: `workbook-${Date.now()}`, locale: LocaleType.EN_US, name: "Sample Workbook", sheetOrder: ["sheet-01"], sheets: { "sheet-01": { type: 0, id: "sheet-01", name: "Sheet1", tabColor: "", hidden: 0, rowCount: 100, columnCount: 20, zoomRatio: 1, scrollTop: 0, scrollLeft: 0, defaultColumnWidth: 93, defaultRowHeight: 27, cellData: {}, rowData: {}, columnData: {}, showGridlines: 1, rowHeader: { width: 46, hidden: 0 }, columnHeader: { height: 20, hidden: 0 }, selections: ["A1"], rightToLeft: 0, }, }, }; const workbookToUse = workbookData || defaultWorkbook; // Presets에서는 univerAPI가 자동으로 제공됨 try { if (univerAPI) { console.log("✅ Presets 기반 univerAPI 초기화 완료"); // 새 워크북 생성 (presets univerAPI 방식) const workbook = univerAPI.createWorkbook(workbookToUse); console.log("✅ Presets 기반 워크북 생성 완료:", workbook?.getId()); // 셀 선택 핸들러 초기화 - SRP에 맞춰 별도 클래스로 분리 cellSelectionHandler.current.initialize(univer); setIsInitialized(true); } else { console.warn("⚠️ univerAPI가 제공되지 않음"); setIsInitialized(false); } } catch (error) { console.error("❌ Presets 기반 워크북 생성 오류:", error); setIsInitialized(false); } } catch (error) { console.error("❌ Presets 기반 Univer 초기화 실패:", error); setIsInitialized(false); } }, [appStore], ); // 파일 처리 함수 const processFile = useCallback( async (file: File) => { // 강화된 상태 확인으로 중복 실행 완전 차단 if ( isProcessing || UniverseManager.isInitializing() || UniverseManager.isDisposing() ) { console.log("⏸️ 처리 중이거나 상태 변경 중입니다. 현재 상태:", { isProcessing, isInitializing: UniverseManager.isInitializing(), isDisposing: UniverseManager.isDisposing(), }); return; } if (!file.name.toLowerCase().endsWith(".xlsx")) { throw new Error("XLSX 파일만 지원됩니다."); } setIsProcessing(true); console.log("📁 파일 처리 시작:", file.name); try { // LuckyExcel을 사용하여 Excel 파일을 Univer 데이터로 변환 await new Promise((resolve, reject) => { LuckyExcel.transformExcelToUniver( file, async (exportJson: any) => { try { console.log("📊 LuckyExcel 변환 완료:", exportJson); // 변환된 데이터로 워크북 업데이트 if (exportJson && typeof exportJson === "object") { await initializeUniver(exportJson); } else { console.warn( "⚠️ 변환된 데이터가 유효하지 않음, 기본 워크북 사용", ); await initializeUniver(); } setCurrentFile(file); setShowUploadOverlay(false); resolve(); } catch (error) { console.error("❌ 워크북 업데이트 오류:", error); try { await initializeUniver(); setCurrentFile(file); setShowUploadOverlay(false); resolve(); } catch (fallbackError) { reject(fallbackError); } } }, (error: any) => { console.error("❌ LuckyExcel 변환 오류:", error); reject(new Error("파일 변환 중 오류가 발생했습니다.")); }, ); }); } catch (error) { console.error("❌ 파일 처리 오류:", error); throw error; } finally { setIsProcessing(false); console.log("✅ 파일 처리 완료"); } }, [initializeUniver], ); // 파일 입력 변경 처리 const handleFileInputChange = useCallback( async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; try { await processFile(file); } catch (error) { console.error("❌ 파일 처리 오류:", error); alert( error instanceof Error ? error.message : "파일 처리 중 오류가 발생했습니다.", ); } finally { // 파일 입력 초기화로 중복 실행 방지 if (event.target) { event.target.value = ""; } } }, [processFile], ); // 파일 피커 클릭 const handleFilePickerClick = useCallback(() => { if (isProcessing || UniverseManager.isInitializing()) return; fileInputRef.current?.click(); }, [isProcessing]); // 새 업로드 처리 const handleNewUpload = useCallback(() => { if (isProcessing || UniverseManager.isInitializing()) return; setShowUploadOverlay(true); setCurrentFile(null); }, [isProcessing]); // 드래그 앤 드롭 이벤트 핸들러 const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); }, []); const handleDragEnter = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); }, []); const handleDrop = useCallback( async (e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); const files = e.dataTransfer.files; if (files.length === 0) return; const file = files[0]; if (!file.name.toLowerCase().endsWith(".xlsx")) { alert("XLSX 파일만 업로드 가능합니다."); return; } if (file.size > 50 * 1024 * 1024) { alert("파일 크기는 50MB를 초과할 수 없습니다."); return; } try { await processFile(file); } catch (error) { console.error("❌ 파일 처리 오류:", error); alert( error instanceof Error ? error.message : "파일 처리 중 오류가 발생했습니다.", ); } }, [processFile], ); // 컴포넌트 마운트 시 초기화 useEffect(() => { mountedRef.current = true; console.log("🎯 컴포넌트 마운트됨"); // 디버그 도구 설정 (컴포넌트 마운트 시에만) setupDebugTools(); // 강화된 기존 인스턴스 확인 및 재사용 const existingUniver = UniverseManager.getInstance(); const state = getGlobalState(); // 기존 인스턴스가 있고 정상 상태면 재사용 if ( existingUniver && state.univerAPI && !UniverseManager.isInitializing() && !UniverseManager.isDisposing() ) { console.log("♻️ 기존 전역 Univer 인스턴스와 univerAPI 재사용"); setIsInitialized(true); // 기존 인스턴스의 컨테이너가 현재 컨테이너와 다른 경우 갱신 if ( containerRef.current && state.lastContainerId !== containerRef.current.id ) { console.log("🔄 컨테이너 정보 갱신"); state.lastContainerId = containerRef.current.id; } return; } // 초기화 중이거나 정리 중인 경우 대기 if (UniverseManager.isInitializing() || UniverseManager.isDisposing()) { console.log("⏳ 초기화/정리 중이므로 대기"); const waitTimer = setInterval(() => { if ( !UniverseManager.isInitializing() && !UniverseManager.isDisposing() ) { clearInterval(waitTimer); const currentUniver = UniverseManager.getInstance(); if (currentUniver && getGlobalState().univerAPI) { console.log("✅ 대기 후 기존 인스턴스 재사용"); setIsInitialized(true); } else if (containerRef.current && mountedRef.current) { console.log("🚀 대기 후 새 인스턴스 초기화"); initializeUniver(); } } }, 100); return () => clearInterval(waitTimer); } // 완전히 새로운 초기화가 필요한 경우만 진행 const initTimer = setTimeout(() => { // 마운트 상태와 컨테이너 재확인 if ( containerRef.current && mountedRef.current && !UniverseManager.isInitializing() ) { console.log("🚀 컴포넌트 마운트 시 새 Univer 초기화"); initializeUniver(); } }, 100); // DOM 완전 준비 보장 return () => { clearTimeout(initTimer); mountedRef.current = false; console.log("👋 컴포넌트 언마운트됨"); }; }, []); // 의존성 배열을 빈 배열로 변경하여 한 번만 실행 // 컴포넌트 언마운트 시 정리 (전역 인스턴스는 유지) useEffect(() => { return () => { // 전역 인스턴스는 앱 종료 시에만 정리 // 여기서는 로컬 상태만 초기화 setIsInitialized(false); console.log("🧹 로컬 상태 정리 완료"); }; }, []); // 컴포넌트 언마운트 시 리소스 정리 useEffect(() => { return () => { // 셀 선택 핸들러 정리 if (cellSelectionHandler.current.isActive()) { cellSelectionHandler.current.dispose(); } }; }, []); return (
{/* 헤더 */}
{currentFile && ( 📄 {currentFile.name} )}
{/* Univer 컨테이너 (항상 렌더링) */}
{/* Univer와 입력창 사이 여백 */}
{/* 프롬프트 입력창 - Univer 하단에 이어서 */} setPrompt(e.target.value)} onHistoryToggle={handleHistoryToggle} historyCount={history.length} /> {/* 히스토리 패널 - 파일이 업로드된 후에만 표시 */} {!showUploadOverlay && ( setIsHistoryOpen(false)} history={history} onReapply={handleHistoryReapply} onClear={handleHistoryClear} /> )} {/* 파일 업로드 오버레이 - 레이어 분리 */} {showUploadOverlay && ( <> {/* 1. Univer CE 영역만 흐리게 하는 반투명 레이어 */}
{/* 2. Univer 영역 중앙의 업로드 UI */}
{/* 아이콘 및 제목 */}
{isProcessing ? ( ) : ( )}

{isProcessing ? "파일 처리 중..." : "Excel 파일을 업로드하세요"}

{isProcessing ? ( 잠시만 기다려주세요... ) : ( <> .xlsx {" "} 파일을 드래그 앤 드롭하거나 클릭하여 업로드 )}

{/* 드래그 앤 드롭 영역 */}
{isDragOver ? "📂" : "📄"}

{isDragOver ? "파일을 여기에 놓으세요" : "파일을 드래그하거나 클릭하세요"}

최대 50MB까지 업로드 가능

{/* 숨겨진 파일 입력 */} {/* 지원 형식 안내 */}

지원 형식: Excel (.xlsx)

최대 파일 크기: 50MB

💡 브라우저 콘솔에서 window.__UNIVER_DEBUG__ 로 디버깅 가능

)}
); }; export default TestSheetViewer;