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;