AI커맨드 반영 셀 선택 완료
This commit is contained in:
@@ -1,44 +1,24 @@
|
||||
import React, { useRef, useEffect, useState, useCallback } from "react";
|
||||
import { Univer, UniverInstanceType, LocaleType } from "@univerjs/core";
|
||||
import { defaultTheme } from "@univerjs/design";
|
||||
import { UniverDocsPlugin } from "@univerjs/docs";
|
||||
import { UniverDocsUIPlugin } from "@univerjs/docs-ui";
|
||||
import { UniverFormulaEnginePlugin } from "@univerjs/engine-formula";
|
||||
import { UniverRenderEnginePlugin } from "@univerjs/engine-render";
|
||||
import { UniverSheetsPlugin } from "@univerjs/sheets";
|
||||
import { UniverSheetsFormulaPlugin } from "@univerjs/sheets-formula";
|
||||
import { UniverSheetsFormulaUIPlugin } from "@univerjs/sheets-formula-ui";
|
||||
import { UniverSheetsUIPlugin } from "@univerjs/sheets-ui";
|
||||
import { UniverSheetsNumfmtPlugin } from "@univerjs/sheets-numfmt";
|
||||
import { UniverSheetsNumfmtUIPlugin } from "@univerjs/sheets-numfmt-ui";
|
||||
import { UniverUIPlugin } from "@univerjs/ui";
|
||||
// 공식 문서 권장: 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 { useAppStore } from "../../stores/useAppStore";
|
||||
import { rangeToAddress } from "../../utils/cellUtils";
|
||||
import { CellSelectionHandler } from "../../utils/cellSelectionHandler";
|
||||
|
||||
// Facade API imports - 공식 문서 방식 (필요한 기능만 선택적 import)
|
||||
import "@univerjs/sheets/facade";
|
||||
import "@univerjs/sheets-ui/facade";
|
||||
|
||||
// 언어팩 import
|
||||
import DesignEnUS from "@univerjs/design/locale/en-US";
|
||||
import UIEnUS from "@univerjs/ui/locale/en-US";
|
||||
import DocsUIEnUS from "@univerjs/docs-ui/locale/en-US";
|
||||
import SheetsEnUS from "@univerjs/sheets/locale/en-US";
|
||||
import SheetsUIEnUS from "@univerjs/sheets-ui/locale/en-US";
|
||||
import SheetsFormulaUIEnUS from "@univerjs/sheets-formula-ui/locale/en-US";
|
||||
import SheetsNumfmtUIEnUS from "@univerjs/sheets-numfmt-ui/locale/en-US";
|
||||
|
||||
// CSS 스타일 import
|
||||
import "@univerjs/design/lib/index.css";
|
||||
import "@univerjs/ui/lib/index.css";
|
||||
import "@univerjs/docs-ui/lib/index.css";
|
||||
import "@univerjs/sheets-ui/lib/index.css";
|
||||
import "@univerjs/sheets-formula-ui/lib/index.css";
|
||||
import "@univerjs/sheets-numfmt-ui/lib/index.css";
|
||||
import { aiProcessor } from "../../utils/aiProcessor";
|
||||
|
||||
// 전역 고유 키 생성
|
||||
const GLOBAL_UNIVER_KEY = "__GLOBAL_UNIVER_INSTANCE__";
|
||||
@@ -46,20 +26,21 @@ const GLOBAL_STATE_KEY = "__GLOBAL_UNIVER_STATE__";
|
||||
|
||||
// 전역 상태 인터페이스
|
||||
interface GlobalUniverState {
|
||||
instance: Univer | null;
|
||||
instance: any | null; // createUniver 반환 타입
|
||||
univerAPI: any | null; // univerAPI 별도 저장
|
||||
isInitializing: boolean;
|
||||
isDisposing: boolean;
|
||||
initializationPromise: Promise<Univer> | null;
|
||||
initializationPromise: Promise<any> | null;
|
||||
lastContainerId: string | null;
|
||||
}
|
||||
|
||||
// Window 객체에 전역 상태 확장
|
||||
declare global {
|
||||
interface Window {
|
||||
[GLOBAL_UNIVER_KEY]: Univer | null;
|
||||
[GLOBAL_UNIVER_KEY]: any | null;
|
||||
[GLOBAL_STATE_KEY]: GlobalUniverState;
|
||||
__UNIVER_DEBUG__: {
|
||||
getGlobalUniver: () => Univer | null;
|
||||
getGlobalUniver: () => any | null;
|
||||
getGlobalState: () => GlobalUniverState;
|
||||
clearGlobalState: () => void;
|
||||
forceReset: () => void;
|
||||
@@ -72,6 +53,7 @@ const initializeGlobalState = (): GlobalUniverState => {
|
||||
if (!window[GLOBAL_STATE_KEY]) {
|
||||
window[GLOBAL_STATE_KEY] = {
|
||||
instance: null,
|
||||
univerAPI: null,
|
||||
isInitializing: false,
|
||||
isDisposing: false,
|
||||
initializationPromise: null,
|
||||
@@ -87,21 +69,25 @@ const getGlobalState = (): GlobalUniverState => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Window 객체 기반 강화된 전역 Univer 관리자
|
||||
* 모듈 재로드와 HMR에도 안전하게 작동
|
||||
* Presets 기반 강화된 전역 Univer 관리자
|
||||
* 공식 문서 권장사항에 따라 createUniver 사용
|
||||
*/
|
||||
const UniverseManager = {
|
||||
// 전역 인스턴스 생성 (완전 단일 인스턴스 보장)
|
||||
async createInstance(container: HTMLElement): Promise<Univer> {
|
||||
async createInstance(container: HTMLElement): Promise<any> {
|
||||
const state = getGlobalState();
|
||||
const containerId = container.id || `container-${Date.now()}`;
|
||||
|
||||
console.log(`🚀 Univer 인스턴스 생성 요청 - Container: ${containerId}`);
|
||||
|
||||
// 이미 존재하는 인스턴스가 있고 같은 컨테이너면 재사용
|
||||
if (state.instance && state.lastContainerId === containerId) {
|
||||
console.log("✅ 기존 전역 Univer 인스턴스 재사용");
|
||||
return state.instance;
|
||||
if (
|
||||
state.instance &&
|
||||
state.univerAPI &&
|
||||
state.lastContainerId === containerId
|
||||
) {
|
||||
console.log("✅ 기존 전역 Univer 인스턴스와 univerAPI 재사용");
|
||||
return { univer: state.instance, univerAPI: state.univerAPI };
|
||||
}
|
||||
|
||||
// 초기화가 진행 중이면 대기
|
||||
@@ -134,43 +120,30 @@ const UniverseManager = {
|
||||
container.id = containerId;
|
||||
}
|
||||
|
||||
console.log("🛠️ Univer 인스턴스 생성 중...");
|
||||
const univer = new Univer({
|
||||
theme: defaultTheme,
|
||||
console.log("🛠️ Presets 기반 Univer 인스턴스 생성 중...");
|
||||
|
||||
// 공식 문서 권장: createUniver 사용으로 대폭 간소화
|
||||
const { univer, univerAPI } = createUniver({
|
||||
locale: LocaleType.EN_US,
|
||||
locales: {
|
||||
[LocaleType.EN_US]: {
|
||||
...DesignEnUS,
|
||||
...UIEnUS,
|
||||
...DocsUIEnUS,
|
||||
...SheetsEnUS,
|
||||
...SheetsUIEnUS,
|
||||
...SheetsFormulaUIEnUS,
|
||||
...SheetsNumfmtUIEnUS,
|
||||
},
|
||||
[LocaleType.EN_US]: merge({}, UniverPresetSheetsCoreEnUS),
|
||||
},
|
||||
theme: defaultTheme,
|
||||
presets: [
|
||||
UniverSheetsCorePreset({
|
||||
container: container,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// 플러그인 등록 순서 (중요: Core → UI → Sheets → Docs → Formula → NumFmt)
|
||||
console.log("🔌 플러그인 등록 중...");
|
||||
univer.registerPlugin(UniverRenderEnginePlugin);
|
||||
univer.registerPlugin(UniverUIPlugin, { container });
|
||||
univer.registerPlugin(UniverSheetsPlugin);
|
||||
univer.registerPlugin(UniverSheetsUIPlugin);
|
||||
univer.registerPlugin(UniverDocsPlugin);
|
||||
univer.registerPlugin(UniverDocsUIPlugin);
|
||||
univer.registerPlugin(UniverSheetsFormulaPlugin);
|
||||
univer.registerPlugin(UniverSheetsFormulaUIPlugin);
|
||||
univer.registerPlugin(UniverSheetsNumfmtPlugin);
|
||||
univer.registerPlugin(UniverSheetsNumfmtUIPlugin);
|
||||
|
||||
// 전역 상태 업데이트
|
||||
// 전역 상태 업데이트 (univerAPI도 함께 저장)
|
||||
state.instance = univer;
|
||||
state.univerAPI = univerAPI;
|
||||
state.lastContainerId = containerId;
|
||||
window[GLOBAL_UNIVER_KEY] = univer;
|
||||
|
||||
console.log("✅ Univer 인스턴스 생성 완료");
|
||||
return univer;
|
||||
console.log("✅ Presets 기반 Univer 인스턴스와 univerAPI 생성 완료");
|
||||
return { univer, univerAPI };
|
||||
} catch (error) {
|
||||
console.error("❌ Univer 인스턴스 생성 실패:", error);
|
||||
throw error;
|
||||
@@ -203,6 +176,7 @@ const UniverseManager = {
|
||||
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 완료");
|
||||
@@ -214,7 +188,7 @@ const UniverseManager = {
|
||||
},
|
||||
|
||||
// 현재 인스턴스 반환
|
||||
getInstance(): Univer | null {
|
||||
getInstance(): any | null {
|
||||
const state = getGlobalState();
|
||||
return state.instance || window[GLOBAL_UNIVER_KEY] || null;
|
||||
},
|
||||
@@ -240,6 +214,7 @@ const UniverseManager = {
|
||||
}
|
||||
|
||||
state.instance = null;
|
||||
state.univerAPI = null;
|
||||
state.isInitializing = false;
|
||||
state.isDisposing = false;
|
||||
state.initializationPromise = null;
|
||||
@@ -263,6 +238,7 @@ if (typeof window !== "undefined") {
|
||||
const state = getGlobalState();
|
||||
Object.assign(state, {
|
||||
instance: null,
|
||||
univerAPI: null,
|
||||
isInitializing: false,
|
||||
isDisposing: false,
|
||||
initializationPromise: null,
|
||||
@@ -298,7 +274,7 @@ const TestSheetViewer: React.FC = () => {
|
||||
// CellSelectionHandler 인스턴스 생성
|
||||
const cellSelectionHandler = useRef(new CellSelectionHandler());
|
||||
|
||||
// Univer 초기화 함수
|
||||
// Univer 초기화 함수 (Presets 기반)
|
||||
const initializeUniver = useCallback(
|
||||
async (workbookData?: any) => {
|
||||
if (!containerRef.current || !mountedRef.current) {
|
||||
@@ -307,10 +283,10 @@ const TestSheetViewer: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("🚀 Univer 초기화 시작");
|
||||
console.log("🚀 Presets 기반 Univer 초기화 시작");
|
||||
|
||||
// 전역 인스턴스 생성 또는 재사용
|
||||
const univer = await UniverseManager.createInstance(
|
||||
// 전역 인스턴스 생성 또는 재사용 (Presets 사용)
|
||||
const { univer, univerAPI } = await UniverseManager.createInstance(
|
||||
containerRef.current,
|
||||
);
|
||||
|
||||
@@ -348,32 +324,29 @@ const TestSheetViewer: React.FC = () => {
|
||||
|
||||
const workbookToUse = workbookData || defaultWorkbook;
|
||||
|
||||
// 기존 워크북 정리 (API 호환성 고려)
|
||||
// Presets에서는 univerAPI가 자동으로 제공됨
|
||||
try {
|
||||
const existingUnits =
|
||||
(univer as any).getUnitsForType?.(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
) || [];
|
||||
for (const unit of existingUnits) {
|
||||
(univer as any).disposeUnit?.(unit.getUnitId());
|
||||
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.log("ℹ️ 기존 워크북 정리 시 오류 (무시 가능):", error);
|
||||
console.error("❌ Presets 기반 워크북 생성 오류:", error);
|
||||
setIsInitialized(false);
|
||||
}
|
||||
|
||||
// 새 워크북 생성
|
||||
const workbook = univer.createUnit(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
workbookToUse,
|
||||
);
|
||||
|
||||
console.log("✅ 워크북 생성 완료:", workbook?.getUnitId());
|
||||
setIsInitialized(true);
|
||||
|
||||
// 셀 선택 핸들러 초기화 - SRP에 맞춰 별도 클래스로 분리
|
||||
cellSelectionHandler.current.initialize(univer);
|
||||
} catch (error) {
|
||||
console.error("❌ Univer 초기화 실패:", error);
|
||||
console.error("❌ Presets 기반 Univer 초기화 실패:", error);
|
||||
setIsInitialized(false);
|
||||
}
|
||||
},
|
||||
@@ -634,12 +607,7 @@ const TestSheetViewer: React.FC = () => {
|
||||
<div style={{ height: "1rem" }} />
|
||||
|
||||
{/* 프롬프트 입력창 - Univer 하단에 이어서 */}
|
||||
<PromptInput
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
onExecute={() => {}}
|
||||
disabled={true}
|
||||
/>
|
||||
<PromptInput value={prompt} onChange={(e) => setPrompt(e.target.value)} />
|
||||
|
||||
{/* 파일 업로드 오버레이 - 레이어 분리 */}
|
||||
{showUploadOverlay && (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useAppStore } from "../../stores/useAppStore";
|
||||
import { aiProcessor } from "../../utils/aiProcessor";
|
||||
|
||||
interface PromptInputProps {
|
||||
value: string;
|
||||
@@ -15,6 +16,7 @@ interface PromptInputProps {
|
||||
* - 유니버 시트에서 셀 선택 시 자동으로 셀 주소 삽입 기능 포함
|
||||
* - 선택된 셀 정보 실시간 표시 및 시각적 피드백 제공
|
||||
* - 현재 선택된 셀 정보 상태바 표시
|
||||
* - AI 프로세서 연동으로 전송하기 버튼 기능 구현
|
||||
*/
|
||||
const PromptInput: React.FC<PromptInputProps> = ({
|
||||
value,
|
||||
@@ -29,11 +31,13 @@ const PromptInput: React.FC<PromptInputProps> = ({
|
||||
const [currentSelectedCell, setCurrentSelectedCell] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [processingMessage, setProcessingMessage] = useState<string>("");
|
||||
|
||||
const cellAddressToInsert = useAppStore((state) => state.cellAddressToInsert);
|
||||
const setCellAddressToInsert = useAppStore(
|
||||
(state) => state.setCellAddressToInsert,
|
||||
);
|
||||
const isProcessing = useAppStore((state) => state.isProcessing);
|
||||
|
||||
/**
|
||||
* 현재 선택된 셀 추적
|
||||
@@ -44,6 +48,65 @@ const PromptInput: React.FC<PromptInputProps> = ({
|
||||
}
|
||||
}, [cellAddressToInsert]);
|
||||
|
||||
/**
|
||||
* 전송하기 버튼 클릭 핸들러
|
||||
*/
|
||||
const handleExecute = async () => {
|
||||
if (!value.trim()) {
|
||||
alert("프롬프트를 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isProcessing || aiProcessor.isCurrentlyProcessing()) {
|
||||
alert("이미 처리 중입니다. 잠시 후 다시 시도해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessingMessage("AI가 요청을 처리하고 있습니다...");
|
||||
|
||||
try {
|
||||
console.log("🚀 전송하기 버튼 클릭 - 프롬프트:", value);
|
||||
|
||||
// AI 프로세서에 프롬프트 전송 (테스트 모드)
|
||||
const result = await aiProcessor.processPrompt(value, true);
|
||||
|
||||
console.log("🎉 AI 처리 결과:", result);
|
||||
|
||||
if (result.success) {
|
||||
setProcessingMessage(`✅ 완료: ${result.message}`);
|
||||
|
||||
// 성공 시 프롬프트 입력창 초기화 (선택사항)
|
||||
if (onChange && textareaRef.current) {
|
||||
textareaRef.current.value = "";
|
||||
const syntheticEvent = {
|
||||
target: textareaRef.current,
|
||||
currentTarget: textareaRef.current,
|
||||
} as React.ChangeEvent<HTMLTextAreaElement>;
|
||||
onChange(syntheticEvent);
|
||||
}
|
||||
|
||||
// 3초 후 메시지 숨김
|
||||
setTimeout(() => {
|
||||
setProcessingMessage("");
|
||||
}, 3000);
|
||||
} else {
|
||||
setProcessingMessage(`❌ 실패: ${result.message}`);
|
||||
|
||||
// 에러 메시지는 5초 후 숨김
|
||||
setTimeout(() => {
|
||||
setProcessingMessage("");
|
||||
}, 5000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ AI 프로세싱 오류:", error);
|
||||
setProcessingMessage("❌ 처리 중 오류가 발생했습니다.");
|
||||
|
||||
setTimeout(() => {
|
||||
setProcessingMessage("");
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 셀 주소 삽입 효과
|
||||
* cellAddressToInsert가 변경되면 textarea의 현재 커서 위치에 해당 주소를 삽입
|
||||
@@ -104,26 +167,6 @@ const PromptInput: React.FC<PromptInputProps> = ({
|
||||
|
||||
return (
|
||||
<div className="w-[60%] mx-auto bg-white z-10 flex flex-col items-center py-4 px-2">
|
||||
{/* 현재 선택된 셀 정보 상태바
|
||||
<div className="w-full max-w-3xl mb-2">
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-gray-600">현재 선택된 셀:</span>
|
||||
<span className="font-mono font-semibold text-blue-600">
|
||||
{currentSelectedCell || "없음"}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">셀을 클릭하여 주소 확인</span>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* 셀 선택 피드백 알림 */}
|
||||
{/* {showCellInsertFeedback && lastInsertedCell && (
|
||||
<div className="mb-2 px-3 py-2 bg-green-100 border border-green-300 rounded-lg text-sm text-green-800 animate-pulse">
|
||||
📍 셀 주소 "{lastInsertedCell}"이(가) 입력창에 삽입되었습니다
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
<div className="w-full max-w-3xl flex items-end gap-2">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
@@ -134,7 +177,7 @@ const PromptInput: React.FC<PromptInputProps> = ({
|
||||
💡 팁: 시트에서 셀을 선택하면 자동으로 주소가 입력됩니다"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={false}
|
||||
disabled={isProcessing}
|
||||
maxLength={maxLength}
|
||||
rows={5}
|
||||
/>
|
||||
@@ -142,12 +185,14 @@ const PromptInput: React.FC<PromptInputProps> = ({
|
||||
<button
|
||||
className="ml-2 px-6 py-2 rounded-lg text-white font-semibold text-base shadow transition disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: "linear-gradient(90deg, #a18fff 0%, #6f6fff 100%)",
|
||||
background: isProcessing
|
||||
? "#6b7280"
|
||||
: "linear-gradient(90deg, #a18fff 0%, #6f6fff 100%)",
|
||||
}}
|
||||
onClick={onExecute}
|
||||
disabled={disabled || !value.trim()}
|
||||
onClick={handleExecute}
|
||||
disabled={isProcessing || !value.trim()}
|
||||
>
|
||||
전송하기
|
||||
{isProcessing ? "처리 중..." : "전송하기"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="w-full max-w-3xl flex justify-between items-center mt-1 px-1">
|
||||
|
||||
Reference in New Issue
Block a user