diff --git a/.cursor/rules/cursor-step-by-step-rule.mdc b/.cursor/rules/cursor-step-by-step-rule.mdc new file mode 100644 index 0000000..7c2294b --- /dev/null +++ b/.cursor/rules/cursor-step-by-step-rule.mdc @@ -0,0 +1,103 @@ +--- +description: +globs: +alwaysApply: true +--- +--- +description: +globs: +alwaysApply: true +--- + +## Core Directive +You are a senior software engineer AI assistant. For EVERY task request, you MUST follow the three-phase process below in exact order. Each phase must be completed with expert-level precision and detail. + +## Guiding Principles +- **Minimalistic Approach**: Implement high-quality, clean solutions while avoiding unnecessary complexity +- **Expert-Level Standards**: Every output must meet professional software engineering standards +- **Concrete Results**: Provide specific, actionable details at each step + +--- + +## Phase 1: Codebase Exploration & Analysis +**REQUIRED ACTIONS:** +1. **Systematic File Discovery** + - List ALL potentially relevant files, directories, and modules + - Search for related keywords, functions, classes, and patterns + - Examine each identified file thoroughly + +2. **Convention & Style Analysis** + - Document coding conventions (naming, formatting, architecture patterns) + - Identify existing code style guidelines + - Note framework/library usage patterns + - Catalog error handling approaches + +**OUTPUT FORMAT:** +``` +### Codebase Analysis Results +**Relevant Files Found:** +- [file_path]: [brief description of relevance] + +**Code Conventions Identified:** +- Naming: [convention details] +- Architecture: [pattern details] +- Styling: [format details] + +**Key Dependencies & Patterns:** +- [library/framework]: [usage pattern] +``` + +--- + +## Phase 2: Implementation Planning +**REQUIRED ACTIONS:** +Based on Phase 1 findings, create a detailed implementation roadmap. + +**OUTPUT FORMAT:** +```markdown +## Implementation Plan + +### Module: [Module Name] +**Summary:** [1-2 sentence description of what needs to be implemented] + +**Tasks:** +- [ ] [Specific implementation task] +- [ ] [Specific implementation task] + +**Acceptance Criteria:** +- [ ] [Measurable success criterion] +- [ ] [Measurable success criterion] +- [ ] [Performance/quality requirement] + +### Module: [Next Module Name] +[Repeat structure above] +``` + +--- + +## Phase 3: Implementation Execution +**REQUIRED ACTIONS:** +1. Implement each module following the plan from Phase 2 +2. Verify ALL acceptance criteria are met before proceeding +3. Ensure code adheres to conventions identified in Phase 1 + +**QUALITY GATES:** +- [ ] All acceptance criteria validated +- [ ] Code follows established conventions +- [ ] Minimalistic approach maintained +- [ ] Expert-level implementation standards met + +--- + +## Success Validation +Before completing any task, confirm: +- ✅ All three phases completed sequentially +- ✅ Each phase output meets specified format requirements +- ✅ Implementation satisfies all acceptance criteria +- ✅ Code quality meets professional standards + +## Response Structure +Always structure your response as: +1. **Phase 1 Results**: [Codebase analysis findings] +2. **Phase 2 Plan**: [Implementation roadmap] +3. **Phase 3 Implementation**: [Actual code with validation] \ No newline at end of file diff --git a/.cursor/rules/univer-redi-management.mdc b/.cursor/rules/univer-redi-management.mdc new file mode 100644 index 0000000..dd34e0f --- /dev/null +++ b/.cursor/rules/univer-redi-management.mdc @@ -0,0 +1,154 @@ +--- +description: +globs: +alwaysApply: false +--- +## REDI Duplicate Identifier Prevention in Univer CE + +### **Problem Analysis** +- **Root Cause**: REDI dependency injection system retains global service identifiers in memory even after Univer instance disposal +- **Trigger**: Component remounting causes duplicate service registration attempts +- **Symptoms**: "Identifier [service-name] already exists" console errors + +### **Core Prevention Patterns** + +#### **1. Global Instance Management** +```typescript +// ✅ DO: Use singleton pattern with proper state tracking +const UniverseManager = { + async createInstance(container: HTMLElement): Promise { + const state = getGlobalState(); + + // Check existing instance with complete validation + if (state.instance && state.univerAPI && + !state.isInitializing && !state.isDisposing) { + return { univer: state.instance, univerAPI: state.univerAPI }; + } + + // Wait for ongoing operations + if (state.isInitializing && state.initializationPromise) { + return state.initializationPromise; + } + } +}; + +// ❌ DON'T: Create new instances without checking global state +const univer = createUniver({ /* config */ }); // Direct creation +``` + +#### **2. Component Mount Lifecycle** +```typescript +// ✅ DO: Smart initialization with state checks +useEffect(() => { + const existingUniver = UniverseManager.getInstance(); + const state = getGlobalState(); + + // Reuse existing instance if available and stable + if (existingUniver && state.univerAPI && + !UniverseManager.isInitializing() && !UniverseManager.isDisposing()) { + setIsInitialized(true); + return; + } + + // Wait for ongoing operations + if (UniverseManager.isInitializing() || UniverseManager.isDisposing()) { + const waitTimer = setInterval(() => { + if (!UniverseManager.isInitializing() && !UniverseManager.isDisposing()) { + clearInterval(waitTimer); + // Check and initialize as needed + } + }, 100); + return () => clearInterval(waitTimer); + } +}, []); // Empty dependency array to prevent re-execution + +// ❌ DON'T: Include dependencies that cause re-initialization +useEffect(() => { + initializeUniver(); +}, [initializeUniver]); // This causes re-execution on every render +``` + +#### **3. Complete Cleanup Strategy** +```typescript +// ✅ DO: Implement thorough REDI state cleanup +forceReset(): void { + // Standard disposal + if (state.instance) { + state.instance.dispose(); + } + + // REDI global state cleanup attempt + if (typeof window !== "undefined") { + 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(`Global key ${key} cleanup failed:`, e); + } + }); + } + + // Reset all state flags + Object.assign(state, { + instance: null, + univerAPI: null, + isInitializing: false, + isDisposing: false, + initializationPromise: null, + lastContainerId: null, + }); +} +``` + +### **4. State Validation Patterns** +```typescript +// ✅ DO: Multi-level state validation +const isInstanceReady = (state: GlobalUniverState): boolean => { + return !!( + state.instance && + state.univerAPI && + !state.isInitializing && + !state.isDisposing && + state.lastContainerId + ); +}; + +// ❌ DON'T: Simple existence check +if (state.instance) { /* insufficient validation */ } +``` + +### **5. Debug Tools Integration** +```typescript +// ✅ DO: Provide comprehensive debugging tools +window.__UNIVER_DEBUG__ = { + getGlobalUniver: () => UniverseManager.getInstance(), + getGlobalState: () => getGlobalState(), + forceReset: () => UniverseManager.forceReset(), + completeCleanup: () => UniverseManager.completeCleanup(), +}; +``` + +### **Error Resolution Steps** +1. **Immediate**: Call `window.__UNIVER_DEBUG__.completeCleanup()` in console +2. **Prevention**: Use empty dependency arrays in useEffect for initialization +3. **Validation**: Always check both `instance` and `univerAPI` existence +4. **Cleanup**: Implement thorough REDI global state cleanup in disposal methods + +### **Common Anti-Patterns to Avoid** +- ❌ Re-initializing on every component render +- ❌ Not waiting for ongoing initialization/disposal operations +- ❌ Incomplete state validation before reuse +- ❌ Missing REDI global state cleanup +- ❌ Using complex dependency arrays in initialization useEffect + +### **Best Practices** +- ✅ Implement singleton pattern with proper state management +- ✅ Use polling-based waiting for state transitions +- ✅ Provide debug tools for runtime state inspection +- ✅ Separate initialization concerns from component lifecycle +- ✅ Implement both standard and emergency cleanup methods diff --git a/src/App.tsx b/src/App.tsx index 02b9bca..2dadd71 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,12 @@ import { useState } from "react"; import { Button } from "./components/ui/button"; import HomeButton from "./components/ui/homeButton"; -import EditSheetViewer from "./components/sheet/EditSheetViewer"; +import { lazy, Suspense } from "react"; + +// 동적 import로 EditSheetViewer 로드 (필요할 때만) +const EditSheetViewer = lazy( + () => import("./components/sheet/EditSheetViewer"), +); function App() { const [showTestViewer, setShowTestViewer] = useState(false); @@ -52,9 +57,20 @@ function App() { {/* 메인 콘텐츠 */}
{showTestViewer ? ( - // 테스트 뷰어 표시 + // 동적 로딩된 테스트 뷰어 표시
- + +
+
+

🚀 그리드 그리는 중...

+
+
+ } + > + + ) : ( // 메인 페이지 diff --git a/src/components/sheet/EditSheetViewer.tsx b/src/components/sheet/EditSheetViewer.tsx index c1a1fea..76ab60f 100644 --- a/src/components/sheet/EditSheetViewer.tsx +++ b/src/components/sheet/EditSheetViewer.tsx @@ -44,6 +44,7 @@ declare global { getGlobalState: () => GlobalUniverState; clearGlobalState: () => void; forceReset: () => void; + completeCleanup: () => void; }; } } @@ -213,6 +214,33 @@ const UniverseManager = { } } + // 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; @@ -221,37 +249,72 @@ const UniverseManager = { state.lastContainerId = null; window[GLOBAL_UNIVER_KEY] = null; - console.log("🔄 전역 Univer 상태 강제 리셋 완료"); + 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; + } }, }; -// 전역 디버그 객체 설정 -if (typeof window !== "undefined") { - // 전역 상태 초기화 - initializeGlobalState(); +// 전역 디버그 객체 설정을 위한 헬퍼 함수 (모듈 레벨 실행 방지) +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(), - }; + // 디버그 객체 설정 + 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__"); -} + console.log("🐛 디버그 객체 설정 완료: window.__UNIVER_DEBUG__"); + } +}; /** * Univer CE + 파일 업로드 오버레이 @@ -520,28 +583,76 @@ const TestSheetViewer: React.FC = () => { mountedRef.current = true; console.log("🎯 컴포넌트 마운트됨"); - // 기존 전역 인스턴스 확인 및 재사용 + // 디버그 도구 설정 (컴포넌트 마운트 시에만) + setupDebugTools(); + + // 강화된 기존 인스턴스 확인 및 재사용 const existingUniver = UniverseManager.getInstance(); - if (existingUniver && !UniverseManager.isInitializing()) { - console.log("♻️ 기존 전역 Univer 인스턴스 재사용"); + 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 && !UniverseManager.isInitializing()) { - console.log("🚀 컴포넌트 마운트 시 Univer 초기화"); + // 마운트 상태와 컨테이너 재확인 + if ( + containerRef.current && + mountedRef.current && + !UniverseManager.isInitializing() + ) { + console.log("🚀 컴포넌트 마운트 시 새 Univer 초기화"); initializeUniver(); } - }, 100); // 짧은 지연으로 DOM 완전 준비 보장 + }, 100); // DOM 완전 준비 보장 return () => { clearTimeout(initTimer); mountedRef.current = false; console.log("👋 컴포넌트 언마운트됨"); }; - }, [initializeUniver]); + }, []); // 의존성 배열을 빈 배열로 변경하여 한 번만 실행 // 컴포넌트 언마운트 시 정리 (전역 인스턴스는 유지) useEffect(() => {