diff --git a/.cursor/rules/tutorial-navigation-fix.mdc b/.cursor/rules/tutorial-navigation-fix.mdc new file mode 100644 index 0000000..b93c988 --- /dev/null +++ b/.cursor/rules/tutorial-navigation-fix.mdc @@ -0,0 +1,5 @@ +--- +description: +globs: +alwaysApply: false +--- diff --git a/.cursor/rules/tutorial-simulation.mdc b/.cursor/rules/tutorial-simulation.mdc new file mode 100644 index 0000000..b93c988 --- /dev/null +++ b/.cursor/rules/tutorial-simulation.mdc @@ -0,0 +1,5 @@ +--- +description: +globs: +alwaysApply: false +--- diff --git a/.cursor/rules/univer-presets-api.mdc b/.cursor/rules/univer-presets-api.mdc new file mode 100644 index 0000000..b93c988 --- /dev/null +++ b/.cursor/rules/univer-presets-api.mdc @@ -0,0 +1,5 @@ +--- +description: +globs: +alwaysApply: false +--- diff --git a/src/App.tsx b/src/App.tsx index d2d722b..f0ce46c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,12 @@ import LandingPage from "./components/LandingPage"; import { SignUpPage } from "./components/auth/SignUpPage"; import { SignInPage } from "./components/auth/SignInPage"; import { useAppStore } from "./stores/useAppStore"; +import type { TutorialItem } from "./types/tutorial"; + +// TutorialSheetViewer 동적 import +const TutorialSheetViewer = lazy( + () => import("./components/TutorialSheetViewer"), +); // 동적 import로 EditSheetViewer 로드 (필요할 때만) const EditSheetViewer = lazy( @@ -13,12 +19,24 @@ const EditSheetViewer = lazy( ); // 앱 상태 타입 정의 -type AppView = "landing" | "signUp" | "signIn" | "editor" | "account"; +type AppView = + | "landing" + | "signUp" + | "signIn" + | "editor" + | "account" + | "tutorial"; function App() { const [currentView, setCurrentView] = useState("landing"); - const { isAuthenticated, setAuthenticated, setUser, user, currentFile } = - useAppStore(); + const { + isAuthenticated, + setAuthenticated, + setUser, + user, + currentFile, + startTutorial, + } = useAppStore(); // CTA 버튼 클릭 핸들러 - 인증 상태에 따른 분기 처리 const handleGetStarted = () => { @@ -65,6 +83,68 @@ function App() { setCurrentView("editor"); }; + // 튜토리얼 페이지 이동 핸들러 - 페이지 리로드 기능 추가 + const handleTutorialClick = () => { + if (currentView === "tutorial") { + // 이미 튜토리얼 페이지에 있는 경우 컴포넌트 리마운트를 위해 key 변경 + setCurrentView("landing"); + setTimeout(() => { + setCurrentView("tutorial"); + }, 10); + } else { + setCurrentView("tutorial"); + } + }; + + // 네비게이션 핸들러들 - 라우팅 기반 + const handleHomeClick = () => { + setCurrentView("landing"); + }; + + const handleFeaturesClick = () => { + setCurrentView("landing"); + // 라우팅 후 스크롤 + setTimeout(() => { + const element = document.getElementById("features"); + if (element) { + element.scrollIntoView({ behavior: "smooth" }); + } + }, 100); + }; + + const handleFAQClick = () => { + setCurrentView("landing"); + // 라우팅 후 스크롤 + setTimeout(() => { + const element = document.getElementById("faq"); + if (element) { + element.scrollIntoView({ behavior: "smooth" }); + } + }, 100); + }; + + const handlePricingClick = () => { + setCurrentView("landing"); + // 라우팅 후 스크롤 + setTimeout(() => { + const element = document.getElementById("pricing"); + if (element) { + element.scrollIntoView({ behavior: "smooth" }); + } + }, 100); + }; + + // 튜토리얼 선택 핸들러 - 튜토리얼 시작 후 튜토리얼 페이지로 전환 + const handleTutorialSelect = (tutorial: TutorialItem) => { + console.log("🎯 튜토리얼 선택됨:", tutorial.metadata.title); + + // 앱 스토어에 선택된 튜토리얼 설정 + startTutorial(tutorial); + + // 튜토리얼 페이지로 전환 (통합된 진입점) + setCurrentView("tutorial"); + }; + // 가입 처리 핸들러 const handleSignUp = (email: string, password: string) => { // TODO: 실제 API 연동 @@ -318,6 +398,41 @@ function App() { ); + case "tutorial": + return ( +
+ +
+
+ +
+
+

📚 튜토리얼 로딩 중...

+
+
+ } + > + + +
+ + + ); + case "editor": return (
@@ -361,12 +476,18 @@ function App() { onSignInClick={handleGoToSignIn} onGetStartedClick={handleGetStarted} onAccountClick={handleAccountClick} + onTutorialClick={handleTutorialClick} + onHomeClick={handleHomeClick} + onFeaturesClick={handleFeaturesClick} + onFAQClick={handleFAQClick} + onPricingClick={handlePricingClick} />
); diff --git a/src/components/LandingPage.tsx b/src/components/LandingPage.tsx index 223ddd0..2471524 100644 --- a/src/components/LandingPage.tsx +++ b/src/components/LandingPage.tsx @@ -1,15 +1,18 @@ import * as React from "react"; import { HeroSection } from "./ui/hero-section"; import { FeaturesSection } from "./ui/features-section"; +import { TutorialSection } from "./ui/tutorial-section"; import { FAQSection } from "./ui/faq-section"; import { PricingSection } from "./ui/pricing-section"; import { Footer } from "./ui/footer"; +import type { TutorialItem } from "../types/tutorial"; interface LandingPageProps { onGetStarted?: () => void; onDownloadClick?: () => void; onAccountClick?: () => void; onDemoClick?: () => void; + onTutorialSelect?: (tutorial: TutorialItem) => void; } /** @@ -21,9 +24,8 @@ interface LandingPageProps { */ const LandingPage: React.FC = ({ onGetStarted, - onDownloadClick, - onAccountClick, onDemoClick, + onTutorialSelect, }) => { return (
@@ -35,6 +37,9 @@ const LandingPage: React.FC = ({ {/* Features Section - 주요 기능 소개 */} + {/* Tutorial Section - Excel 함수 튜토리얼 */} + + {/* FAQ Section - 자주 묻는 질문 */} diff --git a/src/components/TutorialSheetViewer.tsx b/src/components/TutorialSheetViewer.tsx new file mode 100644 index 0000000..33d0276 --- /dev/null +++ b/src/components/TutorialSheetViewer.tsx @@ -0,0 +1,608 @@ +import React, { useRef, useEffect, useState, useCallback } from "react"; +import { LocaleType } from "@univerjs/presets"; + +// Presets CSS import +import "@univerjs/presets/lib/styles/preset-sheets-core.css"; +import { cn } from "../lib/utils"; +import PromptInput from "./sheet/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 { TutorialExecutor } from "../utils/tutorialExecutor"; +import { TutorialDataGenerator } from "../utils/tutorialDataGenerator"; +import { TutorialCard } from "./ui/tutorial-card"; +import type { HistoryEntry } from "../types/ai"; +import type { TutorialItem } from "../types/tutorial"; +import { UniverseManager } from "./sheet/EditSheetViewer"; + +// 튜토리얼 단계 타입 정의 +type TutorialStep = "select" | "loaded" | "prompted" | "executed"; + +/** + * 튜토리얼 전용 시트 뷰어 + * - EditSheetViewer 기반, 파일 업로드 기능 완전 제거 + * - 단계별 플로우: values → prompt → send → result + * - Univer 인스턴스 중복 초기화 방지 + * - 일관된 네비게이션 경험 제공 + */ +const TutorialSheetViewer: React.FC = () => { + const containerRef = useRef(null); + const mountedRef = useRef(false); + + const [isInitialized, setIsInitialized] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + const [prompt, setPrompt] = useState(""); + const [showPromptInput, setShowPromptInput] = useState(false); + const [currentStep, setCurrentStep] = useState("select"); + const [executedTutorialPrompt, setExecutedTutorialPrompt] = + useState(""); + + // 선택된 튜토리얼과 튜토리얼 목록 + const [selectedTutorial, setSelectedTutorial] = useState( + null, + ); + const [tutorialList] = useState(() => + TutorialDataGenerator.generateAllTutorials(), + ); + + const appStore = useAppStore(); + + // CellSelectionHandler 및 TutorialExecutor 인스턴스 + const cellSelectionHandler = useRef(new CellSelectionHandler()); + const tutorialExecutor = useRef(new TutorialExecutor()); + + // 히스토리 관련 상태 + const [isHistoryOpen, setIsHistoryOpen] = useState(false); + const [history, setHistory] = useState([]); + + // 안전한 Univer 초기화 함수 + const initializeUniver = useCallback(async () => { + if (!containerRef.current) { + console.warn("⚠️ 컨테이너가 준비되지 않음"); + return false; + } + + if (isInitialized) { + console.log("✅ 이미 초기화됨 - 스킵"); + return true; + } + + try { + console.log("🚀 Univer 초기화 시작 (Tutorial)"); + setIsProcessing(true); + + // UniverseManager를 통한 전역 인스턴스 생성 + const { univer, univerAPI } = await UniverseManager.createInstance( + containerRef.current, + ); + + // 기본 워크북 데이터 (튜토리얼용) + const defaultWorkbook = { + id: `tutorial-workbook-${Date.now()}`, + locale: LocaleType.EN_US, + name: "Tutorial Workbook", + sheetOrder: ["tutorial-sheet"], + sheets: { + "tutorial-sheet": { + type: 0, + id: "tutorial-sheet", + name: "Tutorial Sheet", + 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, + }, + }, + }; + + // 워크북 생성 (TutorialSheetViewer에서 누락된 부분) + if (univerAPI) { + console.log("✅ Presets 기반 univerAPI 초기화 완료"); + + // 새 워크북 생성 (presets univerAPI 방식) + const workbook = univerAPI.createWorkbook(defaultWorkbook); + console.log("✅ 튜토리얼용 워크북 생성 완료:", workbook?.getId()); + + // TutorialExecutor 초기화 + tutorialExecutor.current.setUniverAPI(univerAPI); + + // CellSelectionHandler 초기화 + cellSelectionHandler.current.initialize(univer); + + console.log("✅ Univer 초기화 완료 (Tutorial)"); + setIsInitialized(true); + return true; + } else { + console.warn("⚠️ univerAPI가 제공되지 않음"); + setIsInitialized(false); + return false; + } + } catch (error) { + console.error("❌ Univer 초기화 실패:", error); + setIsInitialized(false); + return false; + } finally { + setIsProcessing(false); + } + }, [isInitialized]); + + // 히스토리 관련 핸들러들 + const handleHistoryToggle = () => { + console.log("🔄 히스토리 토글:", !isHistoryOpen); + setIsHistoryOpen(!isHistoryOpen); + }; + + const handleHistoryClear = () => { + if (window.confirm("모든 히스토리를 삭제하시겠습니까?")) { + setHistory([]); + } + }; + + // 히스토리 항목 추가 함수 + const addHistoryEntry = ( + prompt: string, + range: string, + sheetName: string, + actions: any[], + status: "success" | "error" | "pending", + error?: string, + ) => { + const newEntry: HistoryEntry = { + id: `tutorial-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + timestamp: new Date(), + prompt, + range, + sheetName, + actions, + status, + error, + }; + + setHistory((prev) => [newEntry, ...prev]); + return newEntry.id; + }; + + // 히스토리 항목 업데이트 함수 + const updateHistoryEntry = ( + id: string, + updates: Partial>, + ) => { + setHistory((prev) => + prev.map((entry) => (entry.id === id ? { ...entry, ...updates } : entry)), + ); + }; + + // 튜토리얼 공식 적용 함수 - 사전 정의된 결과를 Univer에 적용 + const applyTutorialFormula = useCallback(async (tutorial: TutorialItem) => { + try { + if (!tutorialExecutor.current.isReady()) { + throw new Error("TutorialExecutor가 준비되지 않았습니다."); + } + + console.log(`🎯 튜토리얼 "${tutorial.metadata.title}" 공식 적용 시작`); + + // TutorialExecutor의 applyTutorialResult 메서드 사용 + const result = + await tutorialExecutor.current.applyTutorialResult(tutorial); + + console.log(`✅ 튜토리얼 "${tutorial.metadata.title}" 공식 적용 완료`); + return result; + } catch (error) { + console.error("❌ 튜토리얼 공식 적용 실패:", error); + return { + success: false, + message: + error instanceof Error + ? error.message + : "알 수 없는 오류가 발생했습니다.", + appliedActions: [], + }; + } + }, []); + + // 히스토리 재적용 핸들러 + const handleHistoryReapply = useCallback(async (entry: HistoryEntry) => { + console.log("🔄 히스토리 재적용 시작:", entry); + + setPrompt(entry.prompt); + + const confirmReapply = window.confirm( + `다음 프롬프트를 다시 실행하시겠습니까?\n\n"${entry.prompt}"\n\n범위: ${entry.range} | 시트: ${entry.sheetName}`, + ); + + if (!confirmReapply) return; + + const reapplyHistoryId = addHistoryEntry( + `[재적용] ${entry.prompt}`, + entry.range, + entry.sheetName, + [], + "pending", + ); + + try { + const result = await aiProcessor.processPrompt(entry.prompt, true); + + if (result.success) { + updateHistoryEntry(reapplyHistoryId, { + status: "success", + actions: + result.appliedCells?.map((cell) => ({ + type: "formula" as const, + range: cell, + formula: `=재적용 수식`, + })) || [], + }); + console.log(`✅ 재적용 성공: ${result.message}`); + } else { + updateHistoryEntry(reapplyHistoryId, { + status: "error", + error: result.message, + actions: [], + }); + alert(`❌ 재적용 실패: ${result.message}`); + } + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : "알 수 없는 오류가 발생했습니다."; + updateHistoryEntry(reapplyHistoryId, { + status: "error", + error: errorMessage, + actions: [], + }); + alert(`❌ 재적용 오류: ${errorMessage}`); + } + }, []); + + // 단계별 튜토리얼 플로우 구현 + const handleTutorialSelect = useCallback( + async (tutorial: TutorialItem) => { + if (isProcessing) { + console.log("⏳ 이미 처리 중이므로 스킵"); + return; + } + + console.log("🎯 튜토리얼 선택:", tutorial.metadata.title); + setIsProcessing(true); + setSelectedTutorial(tutorial); + + // 새 튜토리얼 선택 시 이전 실행 프롬프트 초기화 + setExecutedTutorialPrompt(""); + + try { + // Step 1: 초기화 확인 + if (!isInitialized) { + const initSuccess = await initializeUniver(); + if (!initSuccess) { + throw new Error("Univer 초기화 실패"); + } + } + + // Step 2: 값만 로드 (수식 없이) + console.log("📊 Step 1: 예제 데이터 로드 중..."); + await tutorialExecutor.current.populateSampleData(tutorial.sampleData); + + // Step 3: 프롬프트 미리 설정 (실행하지 않음) + console.log("💭 Step 2: 프롬프트 미리 설정"); + setPrompt(tutorial.prompt); + + // Step 4: 플로우 상태 업데이트 + setCurrentStep("loaded"); + setShowPromptInput(true); + + console.log("✅ 튜토리얼 준비 완료 - 사용자가 Send 버튼을 눌러야 함"); + } catch (error) { + console.error("❌ 튜토리얼 설정 실패:", error); + const errorMessage = + error instanceof Error ? error.message : "알 수 없는 오류"; + alert(`튜토리얼을 준비하는 중 오류가 발생했습니다: ${errorMessage}`); + setCurrentStep("select"); + } finally { + setIsProcessing(false); + } + }, + [isProcessing, isInitialized, initializeUniver], + ); + + // 튜토리얼 시뮬레이션 핸들러 (Send 버튼용) - AI 대신 사전 정의된 공식 사용 + const handlePromptExecute = useCallback(async () => { + if (!prompt.trim()) { + alert("프롬프트를 입력해주세요."); + return; + } + + if (!selectedTutorial) { + alert("먼저 튜토리얼을 선택해주세요."); + return; + } + + console.log("🎯 Step 3: 튜토리얼 시뮬레이션 시작"); + setIsProcessing(true); + setCurrentStep("prompted"); + + const currentRange = appStore.selectedRange + ? rangeToAddress(appStore.selectedRange.range) + : selectedTutorial.targetCell; + const currentSheetName = selectedTutorial.metadata.title; + + // 히스토리에 pending 상태로 추가 + const historyId = addHistoryEntry( + prompt.trim(), + currentRange, + currentSheetName, + [], + "pending", + ); + + try { + // AI 처리 시뮬레이션 (0.8초 지연으로 자연스러운 처리 시간 연출) + console.log("🤖 AI 처리 시뮬레이션 중..."); + await new Promise((resolve) => setTimeout(resolve, 800)); + + // 선택된 튜토리얼의 사전 정의된 결과 적용 + const tutorialResult = await applyTutorialFormula(selectedTutorial); + + if (tutorialResult.success) { + updateHistoryEntry(historyId, { + status: "success", + actions: tutorialResult.appliedActions || [], + }); + + // Step 4: 결과 완료 상태 + setCurrentStep("executed"); + console.log("✅ Step 4: 튜토리얼 시뮬레이션 완료"); + + // 실행된 프롬프트를 저장하고 입력창 비우기 + setExecutedTutorialPrompt(prompt.trim()); + setPrompt(""); + } else { + updateHistoryEntry(historyId, { + status: "error", + error: tutorialResult.message, + actions: [], + }); + alert(`❌ 실행 실패: ${tutorialResult.message}`); + } + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : "알 수 없는 오류가 발생했습니다."; + + updateHistoryEntry(historyId, { + status: "error", + error: errorMessage, + actions: [], + }); + + alert(`❌ 실행 오류: ${errorMessage}`); + } finally { + setIsProcessing(false); + } + }, [prompt, selectedTutorial, appStore.selectedRange]); + + // 컴포넌트 마운트 시 초기화 + useEffect(() => { + if (mountedRef.current) return; + mountedRef.current = true; + + const setupTutorial = async () => { + try { + // DOM과 컨테이너 준비 완료 대기 + await new Promise((resolve) => { + const checkContainer = () => { + if ( + containerRef.current && + containerRef.current.offsetParent !== null + ) { + resolve(); + } else { + requestAnimationFrame(checkContainer); + } + }; + checkContainer(); + }); + + // 앱 스토어에서 선택된 튜토리얼 확인 + const activeTutorial = appStore.tutorialSession.activeTutorial; + if (activeTutorial) { + console.log( + "📚 앱 스토어에서 활성 튜토리얼 발견:", + activeTutorial.metadata.title, + ); + await handleTutorialSelect(activeTutorial); + } else { + // Univer만 초기화하고 대기 + await initializeUniver(); + } + } catch (error) { + console.error("❌ 튜토리얼 설정 실패:", error); + } + }; + + setupTutorial(); + }, [ + initializeUniver, + handleTutorialSelect, + appStore.tutorialSession.activeTutorial, + ]); + + // 컴포넌트 언마운트 시 리소스 정리 + useEffect(() => { + return () => { + if (cellSelectionHandler.current.isActive()) { + cellSelectionHandler.current.dispose(); + } + }; + }, []); + + // 단계별 UI 렌더링 도우미 + const renderStepIndicator = () => { + const steps = [ + { key: "select", label: "튜토리얼 선택", icon: "🎯" }, + { key: "loaded", label: "데이터 로드됨", icon: "📊" }, + { key: "prompted", label: "프롬프트 준비", icon: "💭" }, + { key: "executed", label: "실행 완료", icon: "✅" }, + ]; + + const currentIndex = steps.findIndex((step) => step.key === currentStep); + + return ( +
+ {steps.map((step, index) => ( +
+ {step.icon} + {step.label} +
+ ))} +
+ ); + }; + + return ( +
+ {/* 튜토리얼 헤더 */} +
+
+

+ 📚 Excel 함수 튜토리얼 +

+ + {/* 단계 표시기 */} + {renderStepIndicator()} + +

+ {currentStep === "select" && "튜토리얼을 선택하여 시작하세요."} + {currentStep === "loaded" && + "데이터가 로드되었습니다. 아래 프롬프트를 확인하고 Send 버튼을 클릭하세요."} + {currentStep === "prompted" && "프롬프트를 실행 중입니다..."} + {currentStep === "executed" && + "실행이 완료되었습니다! 결과를 확인해보세요."} +

+ + {/* 현재 선택된 튜토리얼 정보 */} + {selectedTutorial && ( +
+

+ 현재 튜토리얼: {selectedTutorial.metadata.title} +

+

+ {selectedTutorial.metadata.description} +

+
+ + 함수: {selectedTutorial.functionName} + + + 난이도: {selectedTutorial.metadata.difficulty} + + + 소요시간: {selectedTutorial.metadata.estimatedTime} + +
+
+ )} + + {/* 튜토리얼 선택 그리드 */} + {currentStep === "select" && ( +
+ {tutorialList.map((tutorial) => ( + + ))} +
+ )} +
+
+ + {/* 스프레드시트 영역 */} +
+
+ + {/* 히스토리 패널 */} + setIsHistoryOpen(false)} + history={history} + onReapply={handleHistoryReapply} + onClear={handleHistoryClear} + /> + + {/* 프롬프트 입력창 - 단계별 표시 */} + {showPromptInput && + (currentStep === "loaded" || currentStep === "executed") && ( +
+
+ setPrompt(e.target.value)} + onExecute={handlePromptExecute} + onHistoryToggle={handleHistoryToggle} + historyCount={history.length} + disabled={isProcessing || currentStep !== "loaded"} + tutorialPrompt={executedTutorialPrompt} + /> +
+
+ )} + + {/* 로딩 오버레이 */} + {isProcessing && ( +
+
+
+

+ {currentStep === "select" && "튜토리얼 준비 중..."} + {currentStep === "loaded" && "데이터 로딩 중..."} + {currentStep === "prompted" && + "🤖 AI가 수식을 생성하고 있습니다..."} +

+
+
+ )} +
+
+ ); +}; + +export default TutorialSheetViewer; diff --git a/src/components/sheet/EditSheetViewer.tsx b/src/components/sheet/EditSheetViewer.tsx index 08e61ff..ebe66d0 100644 --- a/src/components/sheet/EditSheetViewer.tsx +++ b/src/components/sheet/EditSheetViewer.tsx @@ -19,6 +19,7 @@ import { useAppStore } from "../../stores/useAppStore"; import { rangeToAddress } from "../../utils/cellUtils"; import { CellSelectionHandler } from "../../utils/cellSelectionHandler"; import { aiProcessor } from "../../utils/aiProcessor"; +import { TutorialExecutor } from "../../utils/tutorialExecutor"; import type { HistoryEntry } from "../../types/ai"; // 전역 고유 키 생성 @@ -74,7 +75,7 @@ const getGlobalState = (): GlobalUniverState => { * Presets 기반 강화된 전역 Univer 관리자 * 공식 문서 권장사항에 따라 createUniver 사용 */ -const UniverseManager = { +export const UniverseManager = { // 전역 인스턴스 생성 (완전 단일 인스턴스 보장) async createInstance(container: HTMLElement): Promise { const state = getGlobalState(); @@ -339,6 +340,9 @@ const TestSheetViewer: React.FC = () => { // CellSelectionHandler 인스턴스 생성 const cellSelectionHandler = useRef(new CellSelectionHandler()); + // TutorialExecutor 인스턴스 생성 + const tutorialExecutor = useRef(new TutorialExecutor()); + // 히스토리 관련 상태 추가 const [isHistoryOpen, setIsHistoryOpen] = useState(false); const [history, setHistory] = useState([]); @@ -587,6 +591,9 @@ const TestSheetViewer: React.FC = () => { // 셀 선택 핸들러 초기화 - SRP에 맞춰 별도 클래스로 분리 cellSelectionHandler.current.initialize(univer); + // TutorialExecutor에 Univer API 설정 + tutorialExecutor.current.setUniverAPI(univerAPI); + setIsInitialized(true); } else { console.warn("⚠️ univerAPI가 제공되지 않음"); @@ -862,6 +869,92 @@ const TestSheetViewer: React.FC = () => { }; }, []); + // 튜토리얼 자동 실행 로직 + useEffect(() => { + const { tutorialSession } = appStore; + + // 튜토리얼이 선택되었고 아직 실행되지 않은 경우 + if ( + tutorialSession.activeTutorial && + tutorialSession.execution?.status === "준비중" && + tutorialSession.isAutoMode && + !tutorialExecutor.current.isCurrentlyExecuting() + ) { + console.log( + "🎯 튜토리얼 자동 실행 시작:", + tutorialSession.activeTutorial.metadata.title, + ); + + const executeTutorial = async () => { + try { + // 상태를 실행중으로 업데이트 + appStore.updateTutorialExecution("실행중", 1); + + // 튜토리얼 실행 + const result = await tutorialExecutor.current.startTutorial( + tutorialSession.activeTutorial!, + { + autoExecute: true, + stepDelay: 1500, + highlightDuration: 2000, + showFormula: true, + enableAnimation: true, + }, + ); + + if (result.success) { + // 성공 시 상태 업데이트 + appStore.updateTutorialExecution("완료", 3); + + // 프롬프트 자동 입력 + if (tutorialSession.activeTutorial!.prompt) { + setPrompt(tutorialSession.activeTutorial!.prompt); + setShowPromptInput(true); + } + + console.log("✅ 튜토리얼 실행 완료:", result); + } else { + appStore.updateTutorialExecution( + "오류", + undefined, + "튜토리얼 실행 실패", + ); + } + } catch (error) { + console.error("❌ 튜토리얼 실행 오류:", error); + appStore.updateTutorialExecution( + "오류", + undefined, + error instanceof Error ? error.message : "알 수 없는 오류", + ); + } + }; + + // Univer가 초기화된 후 실행 + if (getGlobalState().univerAPI) { + executeTutorial(); + } else { + // Univer API가 준비될 때까지 대기 + const checkInterval = setInterval(() => { + if (getGlobalState().univerAPI) { + clearInterval(checkInterval); + executeTutorial(); + } + }, 500); + + // 10초 후 타임아웃 + setTimeout(() => { + clearInterval(checkInterval); + appStore.updateTutorialExecution( + "오류", + undefined, + "Univer API 초기화 타임아웃", + ); + }, 10000); + } + } + }, [appStore.tutorialSession, appStore]); + return (
{/* 헤더 - App.tsx와 동일한 스타일 적용 */} diff --git a/src/components/sheet/PromptInput.tsx b/src/components/sheet/PromptInput.tsx index 89066e0..b26446d 100644 --- a/src/components/sheet/PromptInput.tsx +++ b/src/components/sheet/PromptInput.tsx @@ -11,6 +11,7 @@ interface PromptInputProps { maxLength?: number; onHistoryToggle?: () => void; historyCount?: number; + tutorialPrompt?: string; } /** @@ -29,6 +30,7 @@ const PromptInput: React.FC = ({ maxLength = 500, onHistoryToggle, historyCount, + tutorialPrompt, }) => { const textareaRef = useRef(null); const [, setShowCellInsertFeedback] = useState(false); @@ -182,8 +184,11 @@ const PromptInput: React.FC = ({