🔧 주요 개선사항: 1. Topbar 네비게이션 문제 해결 - 튜토리얼 페이지에서 메뉴 항목 클릭 시 올바른 라우팅 구현 - Tutorial 메뉴 클릭 시 페이지 리로드 기능 추가 (컴포넌트 리마운트) - 라우팅 우선, 스크롤 폴백 패턴 적용 2. PromptInput 플레이스홀더 개선 - 튜토리얼 실행 후 실제 사용된 프롬프트를 플레이스홀더에 표시 - 명확한 프롬프트 → 실행 → 결과 추적 가능 - 새 튜토리얼 선택 시 이전 프롬프트 초기화 3. 새로운 튜토리얼 시스템 구축 - TutorialSheetViewer: 단계별 튜토리얼 플로우 구현 - TutorialCard: 개별 튜토리얼 카드 컴포넌트 - TutorialExecutor: 튜토리얼 실행 엔진 - TutorialDataGenerator: 10개 Excel 함수 데이터 생성 📁 변경된 파일들: - src/App.tsx: 네비게이션 핸들러 추가 - src/components/ui/topbar.tsx: 라우팅 기반 네비게이션 구현 - src/components/sheet/PromptInput.tsx: 동적 플레이스홀더 추가 - src/components/TutorialSheetViewer.tsx: 튜토리얼 전용 뷰어 구현 - src/types/tutorial.ts: 튜토리얼 타입 정의 - .cursor/rules/tutorial-navigation-fix.mdc: 구현 패턴 문서화 ✅ 검증 완료: - 모든 topbar 메뉴 정상 네비게이션 - 튜토리얼 페이지 리로드 기능 작동 - 실행된 프롬프트 플레이스홀더 표시 - AI 워크플로우 시뮬레이션 완성
609 lines
20 KiB
TypeScript
609 lines
20 KiB
TypeScript
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<HTMLDivElement>(null);
|
|
const mountedRef = useRef<boolean>(false);
|
|
|
|
const [isInitialized, setIsInitialized] = useState<boolean>(false);
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
const [prompt, setPrompt] = useState("");
|
|
const [showPromptInput, setShowPromptInput] = useState(false);
|
|
const [currentStep, setCurrentStep] = useState<TutorialStep>("select");
|
|
const [executedTutorialPrompt, setExecutedTutorialPrompt] =
|
|
useState<string>("");
|
|
|
|
// 선택된 튜토리얼과 튜토리얼 목록
|
|
const [selectedTutorial, setSelectedTutorial] = useState<TutorialItem | null>(
|
|
null,
|
|
);
|
|
const [tutorialList] = useState<TutorialItem[]>(() =>
|
|
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<HistoryEntry[]>([]);
|
|
|
|
// 안전한 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<Omit<HistoryEntry, "id" | "timestamp">>,
|
|
) => {
|
|
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<void>((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 (
|
|
<div className="flex items-center space-x-2 mb-4">
|
|
{steps.map((step, index) => (
|
|
<div
|
|
key={step.key}
|
|
className={cn(
|
|
"flex items-center space-x-1 px-3 py-1 rounded-full text-sm",
|
|
index <= currentIndex
|
|
? "bg-blue-100 text-blue-800"
|
|
: "bg-gray-100 text-gray-500",
|
|
)}
|
|
>
|
|
<span>{step.icon}</span>
|
|
<span>{step.label}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="h-full flex flex-col bg-gray-50">
|
|
{/* 튜토리얼 헤더 */}
|
|
<div className="bg-white border-b border-gray-200 p-4 flex-shrink-0">
|
|
<div className="max-w-7xl mx-auto">
|
|
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
|
📚 Excel 함수 튜토리얼
|
|
</h1>
|
|
|
|
{/* 단계 표시기 */}
|
|
{renderStepIndicator()}
|
|
|
|
<p className="text-gray-600 mb-4">
|
|
{currentStep === "select" && "튜토리얼을 선택하여 시작하세요."}
|
|
{currentStep === "loaded" &&
|
|
"데이터가 로드되었습니다. 아래 프롬프트를 확인하고 Send 버튼을 클릭하세요."}
|
|
{currentStep === "prompted" && "프롬프트를 실행 중입니다..."}
|
|
{currentStep === "executed" &&
|
|
"실행이 완료되었습니다! 결과를 확인해보세요."}
|
|
</p>
|
|
|
|
{/* 현재 선택된 튜토리얼 정보 */}
|
|
{selectedTutorial && (
|
|
<div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
|
<h3 className="font-semibold text-blue-900 mb-2">
|
|
현재 튜토리얼: {selectedTutorial.metadata.title}
|
|
</h3>
|
|
<p className="text-blue-700 text-sm mb-2">
|
|
{selectedTutorial.metadata.description}
|
|
</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
<span className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded">
|
|
함수: {selectedTutorial.functionName}
|
|
</span>
|
|
<span className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded">
|
|
난이도: {selectedTutorial.metadata.difficulty}
|
|
</span>
|
|
<span className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded">
|
|
소요시간: {selectedTutorial.metadata.estimatedTime}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 튜토리얼 선택 그리드 */}
|
|
{currentStep === "select" && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mb-4">
|
|
{tutorialList.map((tutorial) => (
|
|
<TutorialCard
|
|
key={tutorial.metadata.id}
|
|
tutorial={tutorial}
|
|
onClick={handleTutorialSelect}
|
|
isActive={
|
|
selectedTutorial?.metadata.id === tutorial.metadata.id
|
|
}
|
|
showBadge={true}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 스프레드시트 영역 */}
|
|
<div className="flex-1 relative">
|
|
<div
|
|
ref={containerRef}
|
|
className="absolute inset-0 bg-white"
|
|
style={{
|
|
minHeight: "0",
|
|
width: "100%",
|
|
height: "100%",
|
|
}}
|
|
/>
|
|
|
|
{/* 히스토리 패널 */}
|
|
<HistoryPanel
|
|
isOpen={isHistoryOpen}
|
|
onClose={() => setIsHistoryOpen(false)}
|
|
history={history}
|
|
onReapply={handleHistoryReapply}
|
|
onClear={handleHistoryClear}
|
|
/>
|
|
|
|
{/* 프롬프트 입력창 - 단계별 표시 */}
|
|
{showPromptInput &&
|
|
(currentStep === "loaded" || currentStep === "executed") && (
|
|
<div className="absolute bottom-6 left-1/2 transform -translate-x-1/2 z-40">
|
|
<div className="bg-white/95 backdrop-blur-sm border border-gray-200 rounded-2xl shadow-2xl p-4 max-w-4xl w-[90vw] sm:w-[80vw] md:w-[70vw] lg:w-[60vw]">
|
|
<PromptInput
|
|
value={prompt}
|
|
onChange={(e) => setPrompt(e.target.value)}
|
|
onExecute={handlePromptExecute}
|
|
onHistoryToggle={handleHistoryToggle}
|
|
historyCount={history.length}
|
|
disabled={isProcessing || currentStep !== "loaded"}
|
|
tutorialPrompt={executedTutorialPrompt}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 로딩 오버레이 */}
|
|
{isProcessing && (
|
|
<div className="absolute inset-0 bg-white/50 backdrop-blur-sm flex items-center justify-center z-50">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
<p className="text-gray-600">
|
|
{currentStep === "select" && "튜토리얼 준비 중..."}
|
|
{currentStep === "loaded" && "데이터 로딩 중..."}
|
|
{currentStep === "prompted" &&
|
|
"🤖 AI가 수식을 생성하고 있습니다..."}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TutorialSheetViewer;
|