AI커맨드 반영 셀 선택 완료
This commit is contained in:
4639
package-lock.json
generated
4639
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@
|
|||||||
"@univerjs/engine-formula": "^0.8.2",
|
"@univerjs/engine-formula": "^0.8.2",
|
||||||
"@univerjs/engine-numfmt": "^0.8.2",
|
"@univerjs/engine-numfmt": "^0.8.2",
|
||||||
"@univerjs/engine-render": "^0.8.2",
|
"@univerjs/engine-render": "^0.8.2",
|
||||||
"@univerjs/facade": "^0.5.5",
|
"@univerjs/presets": "^0.8.2",
|
||||||
"@univerjs/sheets": "^0.8.2",
|
"@univerjs/sheets": "^0.8.2",
|
||||||
"@univerjs/sheets-formula": "^0.8.2",
|
"@univerjs/sheets-formula": "^0.8.2",
|
||||||
"@univerjs/sheets-formula-ui": "^0.8.2",
|
"@univerjs/sheets-formula-ui": "^0.8.2",
|
||||||
@@ -84,5 +84,8 @@
|
|||||||
"privacy"
|
"privacy"
|
||||||
],
|
],
|
||||||
"author": "sheetEasy AI Team",
|
"author": "sheetEasy AI Team",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"overrides": {
|
||||||
|
"@wendellhu/redi": "0.18.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,24 @@
|
|||||||
import React, { useRef, useEffect, useState, useCallback } from "react";
|
import React, { useRef, useEffect, useState, useCallback } from "react";
|
||||||
import { Univer, UniverInstanceType, LocaleType } from "@univerjs/core";
|
// 공식 문서 권장: presets 패키지 사용으로 간소화
|
||||||
import { defaultTheme } from "@univerjs/design";
|
import {
|
||||||
import { UniverDocsPlugin } from "@univerjs/docs";
|
createUniver,
|
||||||
import { UniverDocsUIPlugin } from "@univerjs/docs-ui";
|
defaultTheme,
|
||||||
import { UniverFormulaEnginePlugin } from "@univerjs/engine-formula";
|
LocaleType,
|
||||||
import { UniverRenderEnginePlugin } from "@univerjs/engine-render";
|
merge,
|
||||||
import { UniverSheetsPlugin } from "@univerjs/sheets";
|
} from "@univerjs/presets";
|
||||||
import { UniverSheetsFormulaPlugin } from "@univerjs/sheets-formula";
|
import { UniverSheetsCorePreset } from "@univerjs/presets/preset-sheets-core";
|
||||||
import { UniverSheetsFormulaUIPlugin } from "@univerjs/sheets-formula-ui";
|
import UniverPresetSheetsCoreEnUS from "@univerjs/presets/preset-sheets-core/locales/en-US";
|
||||||
import { UniverSheetsUIPlugin } from "@univerjs/sheets-ui";
|
import { UniverInstanceType } from "@univerjs/core";
|
||||||
import { UniverSheetsNumfmtPlugin } from "@univerjs/sheets-numfmt";
|
|
||||||
import { UniverSheetsNumfmtUIPlugin } from "@univerjs/sheets-numfmt-ui";
|
// Presets CSS import
|
||||||
import { UniverUIPlugin } from "@univerjs/ui";
|
import "@univerjs/presets/lib/styles/preset-sheets-core.css";
|
||||||
import { cn } from "../../lib/utils";
|
import { cn } from "../../lib/utils";
|
||||||
import LuckyExcel from "@zwight/luckyexcel";
|
import LuckyExcel from "@zwight/luckyexcel";
|
||||||
import PromptInput from "./PromptInput";
|
import PromptInput from "./PromptInput";
|
||||||
import { useAppStore } from "../../stores/useAppStore";
|
import { useAppStore } from "../../stores/useAppStore";
|
||||||
import { rangeToAddress } from "../../utils/cellUtils";
|
import { rangeToAddress } from "../../utils/cellUtils";
|
||||||
import { CellSelectionHandler } from "../../utils/cellSelectionHandler";
|
import { CellSelectionHandler } from "../../utils/cellSelectionHandler";
|
||||||
|
import { aiProcessor } from "../../utils/aiProcessor";
|
||||||
// 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";
|
|
||||||
|
|
||||||
// 전역 고유 키 생성
|
// 전역 고유 키 생성
|
||||||
const GLOBAL_UNIVER_KEY = "__GLOBAL_UNIVER_INSTANCE__";
|
const GLOBAL_UNIVER_KEY = "__GLOBAL_UNIVER_INSTANCE__";
|
||||||
@@ -46,20 +26,21 @@ const GLOBAL_STATE_KEY = "__GLOBAL_UNIVER_STATE__";
|
|||||||
|
|
||||||
// 전역 상태 인터페이스
|
// 전역 상태 인터페이스
|
||||||
interface GlobalUniverState {
|
interface GlobalUniverState {
|
||||||
instance: Univer | null;
|
instance: any | null; // createUniver 반환 타입
|
||||||
|
univerAPI: any | null; // univerAPI 별도 저장
|
||||||
isInitializing: boolean;
|
isInitializing: boolean;
|
||||||
isDisposing: boolean;
|
isDisposing: boolean;
|
||||||
initializationPromise: Promise<Univer> | null;
|
initializationPromise: Promise<any> | null;
|
||||||
lastContainerId: string | null;
|
lastContainerId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window 객체에 전역 상태 확장
|
// Window 객체에 전역 상태 확장
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
[GLOBAL_UNIVER_KEY]: Univer | null;
|
[GLOBAL_UNIVER_KEY]: any | null;
|
||||||
[GLOBAL_STATE_KEY]: GlobalUniverState;
|
[GLOBAL_STATE_KEY]: GlobalUniverState;
|
||||||
__UNIVER_DEBUG__: {
|
__UNIVER_DEBUG__: {
|
||||||
getGlobalUniver: () => Univer | null;
|
getGlobalUniver: () => any | null;
|
||||||
getGlobalState: () => GlobalUniverState;
|
getGlobalState: () => GlobalUniverState;
|
||||||
clearGlobalState: () => void;
|
clearGlobalState: () => void;
|
||||||
forceReset: () => void;
|
forceReset: () => void;
|
||||||
@@ -72,6 +53,7 @@ const initializeGlobalState = (): GlobalUniverState => {
|
|||||||
if (!window[GLOBAL_STATE_KEY]) {
|
if (!window[GLOBAL_STATE_KEY]) {
|
||||||
window[GLOBAL_STATE_KEY] = {
|
window[GLOBAL_STATE_KEY] = {
|
||||||
instance: null,
|
instance: null,
|
||||||
|
univerAPI: null,
|
||||||
isInitializing: false,
|
isInitializing: false,
|
||||||
isDisposing: false,
|
isDisposing: false,
|
||||||
initializationPromise: null,
|
initializationPromise: null,
|
||||||
@@ -87,21 +69,25 @@ const getGlobalState = (): GlobalUniverState => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Window 객체 기반 강화된 전역 Univer 관리자
|
* Presets 기반 강화된 전역 Univer 관리자
|
||||||
* 모듈 재로드와 HMR에도 안전하게 작동
|
* 공식 문서 권장사항에 따라 createUniver 사용
|
||||||
*/
|
*/
|
||||||
const UniverseManager = {
|
const UniverseManager = {
|
||||||
// 전역 인스턴스 생성 (완전 단일 인스턴스 보장)
|
// 전역 인스턴스 생성 (완전 단일 인스턴스 보장)
|
||||||
async createInstance(container: HTMLElement): Promise<Univer> {
|
async createInstance(container: HTMLElement): Promise<any> {
|
||||||
const state = getGlobalState();
|
const state = getGlobalState();
|
||||||
const containerId = container.id || `container-${Date.now()}`;
|
const containerId = container.id || `container-${Date.now()}`;
|
||||||
|
|
||||||
console.log(`🚀 Univer 인스턴스 생성 요청 - Container: ${containerId}`);
|
console.log(`🚀 Univer 인스턴스 생성 요청 - Container: ${containerId}`);
|
||||||
|
|
||||||
// 이미 존재하는 인스턴스가 있고 같은 컨테이너면 재사용
|
// 이미 존재하는 인스턴스가 있고 같은 컨테이너면 재사용
|
||||||
if (state.instance && state.lastContainerId === containerId) {
|
if (
|
||||||
console.log("✅ 기존 전역 Univer 인스턴스 재사용");
|
state.instance &&
|
||||||
return 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;
|
container.id = containerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("🛠️ Univer 인스턴스 생성 중...");
|
console.log("🛠️ Presets 기반 Univer 인스턴스 생성 중...");
|
||||||
const univer = new Univer({
|
|
||||||
theme: defaultTheme,
|
// 공식 문서 권장: createUniver 사용으로 대폭 간소화
|
||||||
|
const { univer, univerAPI } = createUniver({
|
||||||
locale: LocaleType.EN_US,
|
locale: LocaleType.EN_US,
|
||||||
locales: {
|
locales: {
|
||||||
[LocaleType.EN_US]: {
|
[LocaleType.EN_US]: merge({}, UniverPresetSheetsCoreEnUS),
|
||||||
...DesignEnUS,
|
|
||||||
...UIEnUS,
|
|
||||||
...DocsUIEnUS,
|
|
||||||
...SheetsEnUS,
|
|
||||||
...SheetsUIEnUS,
|
|
||||||
...SheetsFormulaUIEnUS,
|
|
||||||
...SheetsNumfmtUIEnUS,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
theme: defaultTheme,
|
||||||
|
presets: [
|
||||||
|
UniverSheetsCorePreset({
|
||||||
|
container: container,
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 플러그인 등록 순서 (중요: Core → UI → Sheets → Docs → Formula → NumFmt)
|
// 전역 상태 업데이트 (univerAPI도 함께 저장)
|
||||||
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);
|
|
||||||
|
|
||||||
// 전역 상태 업데이트
|
|
||||||
state.instance = univer;
|
state.instance = univer;
|
||||||
|
state.univerAPI = univerAPI;
|
||||||
state.lastContainerId = containerId;
|
state.lastContainerId = containerId;
|
||||||
window[GLOBAL_UNIVER_KEY] = univer;
|
window[GLOBAL_UNIVER_KEY] = univer;
|
||||||
|
|
||||||
console.log("✅ Univer 인스턴스 생성 완료");
|
console.log("✅ Presets 기반 Univer 인스턴스와 univerAPI 생성 완료");
|
||||||
return univer;
|
return { univer, univerAPI };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Univer 인스턴스 생성 실패:", error);
|
console.error("❌ Univer 인스턴스 생성 실패:", error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -203,6 +176,7 @@ const UniverseManager = {
|
|||||||
console.log("🗑️ 전역 Univer 인스턴스 dispose 시작");
|
console.log("🗑️ 전역 Univer 인스턴스 dispose 시작");
|
||||||
await state.instance.dispose();
|
await state.instance.dispose();
|
||||||
state.instance = null;
|
state.instance = null;
|
||||||
|
state.univerAPI = null;
|
||||||
state.lastContainerId = null;
|
state.lastContainerId = null;
|
||||||
window[GLOBAL_UNIVER_KEY] = null;
|
window[GLOBAL_UNIVER_KEY] = null;
|
||||||
console.log("✅ 전역 Univer 인스턴스 dispose 완료");
|
console.log("✅ 전역 Univer 인스턴스 dispose 완료");
|
||||||
@@ -214,7 +188,7 @@ const UniverseManager = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 현재 인스턴스 반환
|
// 현재 인스턴스 반환
|
||||||
getInstance(): Univer | null {
|
getInstance(): any | null {
|
||||||
const state = getGlobalState();
|
const state = getGlobalState();
|
||||||
return state.instance || window[GLOBAL_UNIVER_KEY] || null;
|
return state.instance || window[GLOBAL_UNIVER_KEY] || null;
|
||||||
},
|
},
|
||||||
@@ -240,6 +214,7 @@ const UniverseManager = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.instance = null;
|
state.instance = null;
|
||||||
|
state.univerAPI = null;
|
||||||
state.isInitializing = false;
|
state.isInitializing = false;
|
||||||
state.isDisposing = false;
|
state.isDisposing = false;
|
||||||
state.initializationPromise = null;
|
state.initializationPromise = null;
|
||||||
@@ -263,6 +238,7 @@ if (typeof window !== "undefined") {
|
|||||||
const state = getGlobalState();
|
const state = getGlobalState();
|
||||||
Object.assign(state, {
|
Object.assign(state, {
|
||||||
instance: null,
|
instance: null,
|
||||||
|
univerAPI: null,
|
||||||
isInitializing: false,
|
isInitializing: false,
|
||||||
isDisposing: false,
|
isDisposing: false,
|
||||||
initializationPromise: null,
|
initializationPromise: null,
|
||||||
@@ -298,7 +274,7 @@ const TestSheetViewer: React.FC = () => {
|
|||||||
// CellSelectionHandler 인스턴스 생성
|
// CellSelectionHandler 인스턴스 생성
|
||||||
const cellSelectionHandler = useRef(new CellSelectionHandler());
|
const cellSelectionHandler = useRef(new CellSelectionHandler());
|
||||||
|
|
||||||
// Univer 초기화 함수
|
// Univer 초기화 함수 (Presets 기반)
|
||||||
const initializeUniver = useCallback(
|
const initializeUniver = useCallback(
|
||||||
async (workbookData?: any) => {
|
async (workbookData?: any) => {
|
||||||
if (!containerRef.current || !mountedRef.current) {
|
if (!containerRef.current || !mountedRef.current) {
|
||||||
@@ -307,10 +283,10 @@ const TestSheetViewer: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("🚀 Univer 초기화 시작");
|
console.log("🚀 Presets 기반 Univer 초기화 시작");
|
||||||
|
|
||||||
// 전역 인스턴스 생성 또는 재사용
|
// 전역 인스턴스 생성 또는 재사용 (Presets 사용)
|
||||||
const univer = await UniverseManager.createInstance(
|
const { univer, univerAPI } = await UniverseManager.createInstance(
|
||||||
containerRef.current,
|
containerRef.current,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -348,32 +324,29 @@ const TestSheetViewer: React.FC = () => {
|
|||||||
|
|
||||||
const workbookToUse = workbookData || defaultWorkbook;
|
const workbookToUse = workbookData || defaultWorkbook;
|
||||||
|
|
||||||
// 기존 워크북 정리 (API 호환성 고려)
|
// Presets에서는 univerAPI가 자동으로 제공됨
|
||||||
try {
|
try {
|
||||||
const existingUnits =
|
if (univerAPI) {
|
||||||
(univer as any).getUnitsForType?.(
|
console.log("✅ Presets 기반 univerAPI 초기화 완료");
|
||||||
UniverInstanceType.UNIVER_SHEET,
|
|
||||||
) || [];
|
// 새 워크북 생성 (presets univerAPI 방식)
|
||||||
for (const unit of existingUnits) {
|
const workbook = univerAPI.createWorkbook(workbookToUse);
|
||||||
(univer as any).disposeUnit?.(unit.getUnitId());
|
console.log("✅ Presets 기반 워크북 생성 완료:", workbook?.getId());
|
||||||
|
|
||||||
|
// 셀 선택 핸들러 초기화 - SRP에 맞춰 별도 클래스로 분리
|
||||||
|
cellSelectionHandler.current.initialize(univer);
|
||||||
|
|
||||||
|
setIsInitialized(true);
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ univerAPI가 제공되지 않음");
|
||||||
|
setIsInitialized(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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) {
|
} catch (error) {
|
||||||
console.error("❌ Univer 초기화 실패:", error);
|
console.error("❌ Presets 기반 Univer 초기화 실패:", error);
|
||||||
setIsInitialized(false);
|
setIsInitialized(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -634,12 +607,7 @@ const TestSheetViewer: React.FC = () => {
|
|||||||
<div style={{ height: "1rem" }} />
|
<div style={{ height: "1rem" }} />
|
||||||
|
|
||||||
{/* 프롬프트 입력창 - Univer 하단에 이어서 */}
|
{/* 프롬프트 입력창 - Univer 하단에 이어서 */}
|
||||||
<PromptInput
|
<PromptInput value={prompt} onChange={(e) => setPrompt(e.target.value)} />
|
||||||
value={prompt}
|
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
|
||||||
onExecute={() => {}}
|
|
||||||
disabled={true}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 파일 업로드 오버레이 - 레이어 분리 */}
|
{/* 파일 업로드 오버레이 - 레이어 분리 */}
|
||||||
{showUploadOverlay && (
|
{showUploadOverlay && (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { useAppStore } from "../../stores/useAppStore";
|
import { useAppStore } from "../../stores/useAppStore";
|
||||||
|
import { aiProcessor } from "../../utils/aiProcessor";
|
||||||
|
|
||||||
interface PromptInputProps {
|
interface PromptInputProps {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -15,6 +16,7 @@ interface PromptInputProps {
|
|||||||
* - 유니버 시트에서 셀 선택 시 자동으로 셀 주소 삽입 기능 포함
|
* - 유니버 시트에서 셀 선택 시 자동으로 셀 주소 삽입 기능 포함
|
||||||
* - 선택된 셀 정보 실시간 표시 및 시각적 피드백 제공
|
* - 선택된 셀 정보 실시간 표시 및 시각적 피드백 제공
|
||||||
* - 현재 선택된 셀 정보 상태바 표시
|
* - 현재 선택된 셀 정보 상태바 표시
|
||||||
|
* - AI 프로세서 연동으로 전송하기 버튼 기능 구현
|
||||||
*/
|
*/
|
||||||
const PromptInput: React.FC<PromptInputProps> = ({
|
const PromptInput: React.FC<PromptInputProps> = ({
|
||||||
value,
|
value,
|
||||||
@@ -29,11 +31,13 @@ const PromptInput: React.FC<PromptInputProps> = ({
|
|||||||
const [currentSelectedCell, setCurrentSelectedCell] = useState<string | null>(
|
const [currentSelectedCell, setCurrentSelectedCell] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [processingMessage, setProcessingMessage] = useState<string>("");
|
||||||
|
|
||||||
const cellAddressToInsert = useAppStore((state) => state.cellAddressToInsert);
|
const cellAddressToInsert = useAppStore((state) => state.cellAddressToInsert);
|
||||||
const setCellAddressToInsert = useAppStore(
|
const setCellAddressToInsert = useAppStore(
|
||||||
(state) => state.setCellAddressToInsert,
|
(state) => state.setCellAddressToInsert,
|
||||||
);
|
);
|
||||||
|
const isProcessing = useAppStore((state) => state.isProcessing);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 현재 선택된 셀 추적
|
* 현재 선택된 셀 추적
|
||||||
@@ -44,6 +48,65 @@ const PromptInput: React.FC<PromptInputProps> = ({
|
|||||||
}
|
}
|
||||||
}, [cellAddressToInsert]);
|
}, [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의 현재 커서 위치에 해당 주소를 삽입
|
* cellAddressToInsert가 변경되면 textarea의 현재 커서 위치에 해당 주소를 삽입
|
||||||
@@ -104,26 +167,6 @@ const PromptInput: React.FC<PromptInputProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[60%] mx-auto bg-white z-10 flex flex-col items-center py-4 px-2">
|
<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">
|
<div className="w-full max-w-3xl flex items-end gap-2">
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
@@ -134,7 +177,7 @@ const PromptInput: React.FC<PromptInputProps> = ({
|
|||||||
💡 팁: 시트에서 셀을 선택하면 자동으로 주소가 입력됩니다"
|
💡 팁: 시트에서 셀을 선택하면 자동으로 주소가 입력됩니다"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
disabled={false}
|
disabled={isProcessing}
|
||||||
maxLength={maxLength}
|
maxLength={maxLength}
|
||||||
rows={5}
|
rows={5}
|
||||||
/>
|
/>
|
||||||
@@ -142,12 +185,14 @@ const PromptInput: React.FC<PromptInputProps> = ({
|
|||||||
<button
|
<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"
|
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={{
|
style={{
|
||||||
background: "linear-gradient(90deg, #a18fff 0%, #6f6fff 100%)",
|
background: isProcessing
|
||||||
|
? "#6b7280"
|
||||||
|
: "linear-gradient(90deg, #a18fff 0%, #6f6fff 100%)",
|
||||||
}}
|
}}
|
||||||
onClick={onExecute}
|
onClick={handleExecute}
|
||||||
disabled={disabled || !value.trim()}
|
disabled={isProcessing || !value.trim()}
|
||||||
>
|
>
|
||||||
전송하기
|
{isProcessing ? "처리 중..." : "전송하기"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full max-w-3xl flex justify-between items-center mt-1 px-1">
|
<div className="w-full max-w-3xl flex justify-between items-center mt-1 px-1">
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ interface AppState {
|
|||||||
|
|
||||||
// AI 상태
|
// AI 상태
|
||||||
aiHistory: AIHistory[];
|
aiHistory: AIHistory[];
|
||||||
isProcessingAI: boolean;
|
isProcessing: boolean;
|
||||||
|
|
||||||
// 액션들
|
// 액션들
|
||||||
setUser: (user: User | null) => void;
|
setUser: (user: User | null) => void;
|
||||||
@@ -68,7 +68,7 @@ interface AppState {
|
|||||||
clearFileUploadErrors: () => void;
|
clearFileUploadErrors: () => void;
|
||||||
|
|
||||||
addAIHistory: (history: AIHistory) => void;
|
addAIHistory: (history: AIHistory) => void;
|
||||||
setProcessingAI: (processing: boolean) => void;
|
setProcessing: (processing: boolean) => void;
|
||||||
|
|
||||||
// 복합 액션들
|
// 복합 액션들
|
||||||
uploadFile: (result: FileUploadResult) => void;
|
uploadFile: (result: FileUploadResult) => void;
|
||||||
@@ -89,7 +89,7 @@ const initialState = {
|
|||||||
error: null,
|
error: null,
|
||||||
fileUploadErrors: [],
|
fileUploadErrors: [],
|
||||||
aiHistory: [],
|
aiHistory: [],
|
||||||
isProcessingAI: false,
|
isProcessing: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAppStore = create<AppState>()(
|
export const useAppStore = create<AppState>()(
|
||||||
@@ -133,7 +133,7 @@ export const useAppStore = create<AppState>()(
|
|||||||
set((state) => ({
|
set((state) => ({
|
||||||
aiHistory: [history, ...state.aiHistory].slice(0, 50), // 최대 50개 유지
|
aiHistory: [history, ...state.aiHistory].slice(0, 50), // 최대 50개 유지
|
||||||
})),
|
})),
|
||||||
setProcessingAI: (processing) => set({ isProcessingAI: processing }),
|
setProcessing: (processing) => set({ isProcessing: processing }),
|
||||||
|
|
||||||
// 복합 액션
|
// 복합 액션
|
||||||
uploadFile: (result) => {
|
uploadFile: (result) => {
|
||||||
|
|||||||
377
src/utils/aiProcessor.ts
Normal file
377
src/utils/aiProcessor.ts
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
/**
|
||||||
|
* AI 프로세서 - AI와의 통신 및 셀 적용 로직 처리
|
||||||
|
* - 프롬프트를 AI에게 전송하고 응답 받기
|
||||||
|
* - 응답 데이터를 파싱하여 각 셀에 수식 적용
|
||||||
|
* - 10ms 인터벌로 순차적 셀 적용
|
||||||
|
*/
|
||||||
|
|
||||||
|
// presets 환경에서는 FUniver import 불필요 (deprecated)
|
||||||
|
// import { FUniver } from "@univerjs/core/facade";
|
||||||
|
import { useAppStore } from "../stores/useAppStore";
|
||||||
|
|
||||||
|
// AI 응답 데이터 타입 정의
|
||||||
|
export interface AIResponse {
|
||||||
|
targetCell: string;
|
||||||
|
formula: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI 프로세서 결과 타입
|
||||||
|
export interface ProcessResult {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
appliedCells?: string[];
|
||||||
|
errors?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 프로세서 클래스
|
||||||
|
* - SRP: AI 통신과 셀 적용만 담당
|
||||||
|
* - 테스트 모드와 실제 AI 모드 지원
|
||||||
|
*/
|
||||||
|
export class AIProcessor {
|
||||||
|
private isProcessing = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 처리 중인지 확인
|
||||||
|
*/
|
||||||
|
public isCurrentlyProcessing(): boolean {
|
||||||
|
return this.isProcessing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테스트용 고정 데이터 생성
|
||||||
|
* 실제 AI 연동 전 테스트를 위한 더미 데이터
|
||||||
|
*/
|
||||||
|
private generateTestData(): AIResponse[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
targetCell: "E13",
|
||||||
|
formula: "=C13+D13",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targetCell: "E14",
|
||||||
|
formula: "=C14+D14",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targetCell: "E15",
|
||||||
|
formula: "=C15+D15",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI에게 프롬프트 전송 (현재는 테스트 모드)
|
||||||
|
* @param prompt - 사용자 입력 프롬프트
|
||||||
|
* @param isTestMode - 테스트 모드 여부 (기본값: true)
|
||||||
|
*/
|
||||||
|
private async sendToAI(
|
||||||
|
prompt: string,
|
||||||
|
isTestMode: boolean = true,
|
||||||
|
): Promise<AIResponse[]> {
|
||||||
|
console.log("🤖 AI 프로세싱 시작:", prompt);
|
||||||
|
|
||||||
|
if (isTestMode) {
|
||||||
|
console.log("🧪 테스트 모드: 고정 데이터 사용");
|
||||||
|
|
||||||
|
// 실제 AI 호출을 시뮬레이션하기 위한 딜레이
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
|
return this.generateTestData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 실제 AI API 호출 로직
|
||||||
|
// const response = await fetch('/api/ai', {
|
||||||
|
// method: 'POST',
|
||||||
|
// headers: { 'Content-Type': 'application/json' },
|
||||||
|
// body: JSON.stringify({ prompt })
|
||||||
|
// });
|
||||||
|
// return await response.json();
|
||||||
|
|
||||||
|
throw new Error("실제 AI API 연동은 아직 구현되지 않았습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 단일 셀에 수식 적용
|
||||||
|
* @param univerAPI - FUniver API 인스턴스
|
||||||
|
* @param targetCell - 대상 셀 주소 (예: "E13")
|
||||||
|
* @param formula - 적용할 수식 (예: "=C13+D13")
|
||||||
|
*/
|
||||||
|
private async applyCellFormula(
|
||||||
|
univerAPI: any,
|
||||||
|
targetCell: string,
|
||||||
|
formula: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (!univerAPI) {
|
||||||
|
throw new Error("Univer API가 생성되지 않았습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeWorkbook = univerAPI.getActiveWorkbook();
|
||||||
|
if (!activeWorkbook) {
|
||||||
|
throw new Error("활성 워크북을 찾을 수 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeSheet = activeWorkbook.getActiveSheet();
|
||||||
|
if (!activeSheet) {
|
||||||
|
throw new Error("활성 시트를 찾을 수 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 셀 주소를 행/열 좌표로 변환
|
||||||
|
const cellCoords = this.parseCellAddress(targetCell);
|
||||||
|
|
||||||
|
// 👉 셀 선택(하이라이트) 추가
|
||||||
|
// const selectionService = activeSheet.getSelection?.();
|
||||||
|
// if (selectionService?.setSelections) {
|
||||||
|
// selectionService.setSelections([
|
||||||
|
// {
|
||||||
|
// startRow: cellCoords.row,
|
||||||
|
// startColumn: cellCoords.col,
|
||||||
|
// endRow: cellCoords.row,
|
||||||
|
// endColumn: cellCoords.col,
|
||||||
|
// },
|
||||||
|
// ]);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 👉 셀 선택 (Facade API 기반)
|
||||||
|
try {
|
||||||
|
const activeWorkbook = univerAPI.getActiveWorkbook();
|
||||||
|
if (activeWorkbook) {
|
||||||
|
const activeSheet = activeWorkbook.getActiveSheet();
|
||||||
|
if (activeSheet) {
|
||||||
|
try {
|
||||||
|
// Facade API의 표준 방식: Range를 통한 셀 선택
|
||||||
|
const cellRange = activeSheet.getRange(targetCell);
|
||||||
|
if (cellRange) {
|
||||||
|
// 다양한 선택 방법 시도
|
||||||
|
if (typeof cellRange.select === "function") {
|
||||||
|
cellRange.select();
|
||||||
|
console.log(`🎯 셀 ${targetCell} 선택 완료 (range.select)`);
|
||||||
|
} else if (
|
||||||
|
typeof activeWorkbook.setActiveRange === "function"
|
||||||
|
) {
|
||||||
|
activeWorkbook.setActiveRange(cellRange);
|
||||||
|
console.log(`🎯 셀 ${targetCell} 활성 범위 설정 완료`);
|
||||||
|
} else if (typeof activeSheet.activate === "function") {
|
||||||
|
// 워크시트 활성화 후 셀 포커스
|
||||||
|
activeSheet.activate();
|
||||||
|
console.log(`🎯 워크시트 활성화 후 셀 ${targetCell} 포커스`);
|
||||||
|
} else {
|
||||||
|
console.log(`🔍 지원하는 선택 메소드를 찾을 수 없습니다.`);
|
||||||
|
|
||||||
|
// 디버깅을 위해 사용 가능한 메소드 출력
|
||||||
|
const rangeMethods = Object.getOwnPropertyNames(cellRange)
|
||||||
|
.filter(
|
||||||
|
(name) => typeof (cellRange as any)[name] === "function",
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
(name) =>
|
||||||
|
name.toLowerCase().includes("select") ||
|
||||||
|
name.toLowerCase().includes("active"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const workbookMethods = Object.getOwnPropertyNames(
|
||||||
|
activeWorkbook,
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
(name) =>
|
||||||
|
typeof (activeWorkbook as any)[name] === "function",
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
(name) =>
|
||||||
|
name.toLowerCase().includes("select") ||
|
||||||
|
name.toLowerCase().includes("active"),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`🔍 Range 선택 관련 메소드:`, rangeMethods);
|
||||||
|
console.log(`🔍 Workbook 선택 관련 메소드:`, workbookMethods);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ ${targetCell} 범위를 가져올 수 없음`);
|
||||||
|
}
|
||||||
|
} catch (rangeError) {
|
||||||
|
console.warn(`⚠️ Range 기반 선택 실패:`, rangeError);
|
||||||
|
|
||||||
|
// 폴백: 좌표 기반 시도
|
||||||
|
try {
|
||||||
|
if (typeof activeSheet.setActiveCell === "function") {
|
||||||
|
activeSheet.setActiveCell(cellCoords.row, cellCoords.col);
|
||||||
|
console.log(`🎯 셀 ${targetCell} 좌표 기반 선택 완료`);
|
||||||
|
} else {
|
||||||
|
console.log(`🔍 setActiveCell 메소드 없음`);
|
||||||
|
}
|
||||||
|
} catch (coordError) {
|
||||||
|
console.warn(`⚠️ 좌표 기반 선택도 실패:`, coordError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ ActiveSheet를 가져올 수 없음`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ ActiveWorkbook을 가져올 수 없음`);
|
||||||
|
}
|
||||||
|
} catch (selectionError) {
|
||||||
|
console.warn(`⚠️ 셀 ${targetCell} 선택 전체 실패:`, selectionError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 셀에 수식 설정
|
||||||
|
const range = activeSheet.getRange(cellCoords.row, cellCoords.col, 1, 1);
|
||||||
|
range.setValue(formula);
|
||||||
|
|
||||||
|
console.log(`✅ 셀 ${targetCell} 수식 적용 및 하이라이트 완료`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 셀 ${targetCell} 수식 적용 실패:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 셀 주소를 행/열 좌표로 변환
|
||||||
|
* @param cellAddress - 셀 주소 (예: "E13")
|
||||||
|
* @returns 행/열 좌표 객체
|
||||||
|
*/
|
||||||
|
private parseCellAddress(cellAddress: string): { row: number; col: number } {
|
||||||
|
const match = cellAddress.match(/^([A-Z]+)(\d+)$/);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`잘못된 셀 주소 형식: ${cellAddress}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const colLetters = match[1];
|
||||||
|
const rowNumber = parseInt(match[2]);
|
||||||
|
|
||||||
|
// 컬럼 문자를 숫자로 변환 (A=0, B=1, ..., Z=25, AA=26, ...)
|
||||||
|
let col = 0;
|
||||||
|
for (let i = 0; i < colLetters.length; i++) {
|
||||||
|
col = col * 26 + (colLetters.charCodeAt(i) - 65 + 1);
|
||||||
|
}
|
||||||
|
col -= 1; // 0-based 인덱스로 변환
|
||||||
|
|
||||||
|
return {
|
||||||
|
row: rowNumber - 1, // 0-based 인덱스로 변환
|
||||||
|
col: col,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메인 프로세싱 함수
|
||||||
|
* @param prompt - 사용자 프롬프트
|
||||||
|
* @param isTestMode - 테스트 모드 여부
|
||||||
|
*/
|
||||||
|
public async processPrompt(
|
||||||
|
prompt: string,
|
||||||
|
isTestMode: boolean = true,
|
||||||
|
): Promise<ProcessResult> {
|
||||||
|
if (this.isProcessing) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "이미 처리 중입니다. 잠시 후 다시 시도해주세요.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// presets 환경에서는 전역 상태에서 univerAPI를 직접 가져옴
|
||||||
|
const globalState = (window as any)["__GLOBAL_UNIVER_STATE__"];
|
||||||
|
if (!globalState?.instance || !globalState?.univerAPI) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Univer 인스턴스 또는 API를 찾을 수 없습니다.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const univerAPI = globalState.univerAPI;
|
||||||
|
|
||||||
|
this.isProcessing = true;
|
||||||
|
// zustand store에 연산 시작 알림
|
||||||
|
try {
|
||||||
|
useAppStore.getState().setProcessing(true);
|
||||||
|
} catch (e) {
|
||||||
|
/* SSR/테스트 환경 무시 */
|
||||||
|
}
|
||||||
|
const appliedCells: string[] = [];
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("🚀 AI 프롬프트 처리 시작");
|
||||||
|
console.log("📝 프롬프트:", prompt);
|
||||||
|
|
||||||
|
// 1. AI에게 프롬프트 전송
|
||||||
|
const aiResponses = await this.sendToAI(prompt, isTestMode);
|
||||||
|
console.log("🤖 AI 응답 받음:", aiResponses);
|
||||||
|
|
||||||
|
if (!Array.isArray(aiResponses) || aiResponses.length === 0) {
|
||||||
|
throw new Error("AI 응답이 올바르지 않습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 각 셀에 10ms 간격으로 순차 적용
|
||||||
|
console.log(`📊 총 ${aiResponses.length}개 셀에 수식 적용 시작`);
|
||||||
|
|
||||||
|
for (let i = 0; i < aiResponses.length; i++) {
|
||||||
|
const response = aiResponses[i];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await this.applyCellFormula(
|
||||||
|
univerAPI,
|
||||||
|
response.targetCell,
|
||||||
|
response.formula,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
appliedCells.push(response.targetCell);
|
||||||
|
console.log(
|
||||||
|
`✅ [${i + 1}/${aiResponses.length}] ${response.targetCell} 적용 완료`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
errors.push(`${response.targetCell}: 수식 적용 실패`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = `${response.targetCell}: ${error instanceof Error ? error.message : "알 수 없는 오류"}`;
|
||||||
|
errors.push(errorMsg);
|
||||||
|
console.error(`❌ [${i + 1}/${aiResponses.length}] ${errorMsg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 마지막 항목이 아니면 10ms 대기
|
||||||
|
if (i < aiResponses.length - 1) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const successCount = appliedCells.length;
|
||||||
|
const errorCount = errors.length;
|
||||||
|
|
||||||
|
console.log(`🎉 처리 완료: 성공 ${successCount}개, 실패 ${errorCount}개`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: successCount > 0,
|
||||||
|
message: `총 ${aiResponses.length}개 중 ${successCount}개 셀에 수식을 성공적으로 적용했습니다.${errorCount > 0 ? ` (실패: ${errorCount}개)` : ""}`,
|
||||||
|
appliedCells,
|
||||||
|
errors: errorCount > 0 ? errors : undefined,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "알 수 없는 오류가 발생했습니다.";
|
||||||
|
console.error("❌ AI 프롬프트 처리 실패:", error);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `처리 실패: ${errorMessage}`,
|
||||||
|
appliedCells: appliedCells.length > 0 ? appliedCells : undefined,
|
||||||
|
errors: [...errors, errorMessage],
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
this.isProcessing = false;
|
||||||
|
// zustand store에 연산 종료 알림
|
||||||
|
try {
|
||||||
|
useAppStore.getState().setProcessing(false);
|
||||||
|
} catch (e) {
|
||||||
|
/* SSR/테스트 환경 무시 */
|
||||||
|
}
|
||||||
|
console.log("🏁 AI 프롬프트 처리 종료");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전역 AI 프로세서 인스턴스
|
||||||
|
* - 싱글톤 패턴으로 관리
|
||||||
|
*/
|
||||||
|
export const aiProcessor = new AIProcessor();
|
||||||
Reference in New Issue
Block a user