셀선택시 프롬프트 창에 자동 입력 완료
This commit is contained in:
@@ -15,6 +15,13 @@ import { UniverUIPlugin } from "@univerjs/ui";
|
||||
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";
|
||||
@@ -279,85 +286,99 @@ const TestSheetViewer: React.FC = () => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const mountedRef = useRef<boolean>(false);
|
||||
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [isInitialized, setIsInitialized] = useState<boolean>(false);
|
||||
const [showUploadOverlay, setShowUploadOverlay] = useState(true);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [currentFile, setCurrentFile] = useState<File | null>(null);
|
||||
const [prompt, setPrompt] = useState("");
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
// CellSelectionHandler 인스턴스 생성
|
||||
const cellSelectionHandler = useRef(new CellSelectionHandler());
|
||||
|
||||
// Univer 초기화 함수
|
||||
const initializeUniver = useCallback(async (workbookData?: any) => {
|
||||
if (!containerRef.current || !mountedRef.current) {
|
||||
console.error("❌ 컨테이너가 준비되지 않았습니다!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("🚀 Univer 초기화 시작");
|
||||
|
||||
// 전역 인스턴스 생성 또는 재사용
|
||||
const univer = await UniverseManager.createInstance(containerRef.current);
|
||||
|
||||
// 기본 워크북 데이터
|
||||
const defaultWorkbook = {
|
||||
id: `workbook-${Date.now()}`,
|
||||
locale: LocaleType.EN_US,
|
||||
name: "Sample Workbook",
|
||||
sheetOrder: ["sheet-01"],
|
||||
sheets: {
|
||||
"sheet-01": {
|
||||
type: 0,
|
||||
id: "sheet-01",
|
||||
name: "Sheet1",
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const workbookToUse = workbookData || defaultWorkbook;
|
||||
|
||||
// 기존 워크북 정리 (API 호환성 고려)
|
||||
try {
|
||||
const existingUnits =
|
||||
(univer as any).getUnitsForType?.(UniverInstanceType.UNIVER_SHEET) ||
|
||||
[];
|
||||
for (const unit of existingUnits) {
|
||||
(univer as any).disposeUnit?.(unit.getUnitId());
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("ℹ️ 기존 워크북 정리 시 오류 (무시 가능):", error);
|
||||
const initializeUniver = useCallback(
|
||||
async (workbookData?: any) => {
|
||||
if (!containerRef.current || !mountedRef.current) {
|
||||
console.error("❌ 컨테이너가 준비되지 않았습니다!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 새 워크북 생성
|
||||
const workbook = univer.createUnit(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
workbookToUse,
|
||||
);
|
||||
try {
|
||||
console.log("🚀 Univer 초기화 시작");
|
||||
|
||||
console.log("✅ 워크북 생성 완료:", workbook?.getUnitId());
|
||||
setIsInitialized(true);
|
||||
} catch (error) {
|
||||
console.error("❌ Univer 초기화 실패:", error);
|
||||
setIsInitialized(false);
|
||||
}
|
||||
}, []);
|
||||
// 전역 인스턴스 생성 또는 재사용
|
||||
const univer = await UniverseManager.createInstance(
|
||||
containerRef.current,
|
||||
);
|
||||
|
||||
// 기본 워크북 데이터
|
||||
const defaultWorkbook = {
|
||||
id: `workbook-${Date.now()}`,
|
||||
locale: LocaleType.EN_US,
|
||||
name: "Sample Workbook",
|
||||
sheetOrder: ["sheet-01"],
|
||||
sheets: {
|
||||
"sheet-01": {
|
||||
type: 0,
|
||||
id: "sheet-01",
|
||||
name: "Sheet1",
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const workbookToUse = workbookData || defaultWorkbook;
|
||||
|
||||
// 기존 워크북 정리 (API 호환성 고려)
|
||||
try {
|
||||
const existingUnits =
|
||||
(univer as any).getUnitsForType?.(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
) || [];
|
||||
for (const unit of existingUnits) {
|
||||
(univer as any).disposeUnit?.(unit.getUnitId());
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("ℹ️ 기존 워크북 정리 시 오류 (무시 가능):", error);
|
||||
}
|
||||
|
||||
// 새 워크북 생성
|
||||
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);
|
||||
setIsInitialized(false);
|
||||
}
|
||||
},
|
||||
[appStore],
|
||||
);
|
||||
|
||||
// 파일 처리 함수
|
||||
const processFile = useCallback(
|
||||
@@ -431,7 +452,7 @@ const TestSheetViewer: React.FC = () => {
|
||||
console.log("✅ 파일 처리 완료");
|
||||
}
|
||||
},
|
||||
[isProcessing, initializeUniver],
|
||||
[initializeUniver],
|
||||
);
|
||||
|
||||
// 파일 입력 변경 처리
|
||||
@@ -559,6 +580,16 @@ const TestSheetViewer: React.FC = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 컴포넌트 언마운트 시 리소스 정리
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// 셀 선택 핸들러 정리
|
||||
if (cellSelectionHandler.current.isActive()) {
|
||||
cellSelectionHandler.current.dispose();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen flex flex-col relative">
|
||||
{/* 헤더 */}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useAppStore } from "../../stores/useAppStore";
|
||||
|
||||
interface PromptInputProps {
|
||||
value: string;
|
||||
@@ -11,6 +12,9 @@ interface PromptInputProps {
|
||||
/**
|
||||
* 에디트 화면 하단 고정 프롬프트 입력창 컴포넌트
|
||||
* - 이미지 참고: 입력창, Execute 버튼, 안내문구, 글자수 카운트, 하단 고정
|
||||
* - 유니버 시트에서 셀 선택 시 자동으로 셀 주소 삽입 기능 포함
|
||||
* - 선택된 셀 정보 실시간 표시 및 시각적 피드백 제공
|
||||
* - 현재 선택된 셀 정보 상태바 표시
|
||||
*/
|
||||
const PromptInput: React.FC<PromptInputProps> = ({
|
||||
value,
|
||||
@@ -19,13 +23,115 @@ const PromptInput: React.FC<PromptInputProps> = ({
|
||||
disabled = true,
|
||||
maxLength = 500,
|
||||
}) => {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [showCellInsertFeedback, setShowCellInsertFeedback] = useState(false);
|
||||
const [lastInsertedCell, setLastInsertedCell] = useState<string | null>(null);
|
||||
const [currentSelectedCell, setCurrentSelectedCell] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const cellAddressToInsert = useAppStore((state) => state.cellAddressToInsert);
|
||||
const setCellAddressToInsert = useAppStore(
|
||||
(state) => state.setCellAddressToInsert,
|
||||
);
|
||||
|
||||
/**
|
||||
* 현재 선택된 셀 추적
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (cellAddressToInsert) {
|
||||
setCurrentSelectedCell(cellAddressToInsert);
|
||||
}
|
||||
}, [cellAddressToInsert]);
|
||||
|
||||
/**
|
||||
* 셀 주소 삽입 효과
|
||||
* cellAddressToInsert가 변경되면 textarea의 현재 커서 위치에 해당 주소를 삽입
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (cellAddressToInsert && textareaRef.current && onChange) {
|
||||
console.log(`🎯 PromptInput: 셀 주소 "${cellAddressToInsert}" 삽입 시작`);
|
||||
|
||||
const textarea = textareaRef.current;
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const currentValue = textarea.value;
|
||||
|
||||
console.log(
|
||||
`📍 PromptInput: 현재 커서 위치 ${start}-${end}, 현재 값: "${currentValue}"`,
|
||||
);
|
||||
|
||||
// 현재 커서 위치에 셀 주소 삽입
|
||||
const newValue =
|
||||
currentValue.slice(0, start) +
|
||||
cellAddressToInsert +
|
||||
currentValue.slice(end);
|
||||
|
||||
console.log(`✏️ PromptInput: 새 값: "${newValue}"`);
|
||||
|
||||
// textarea 값 업데이트
|
||||
textarea.value = newValue;
|
||||
|
||||
// 커서 위치를 삽입된 텍스트 뒤로 이동
|
||||
const newCursorPosition = start + cellAddressToInsert.length;
|
||||
textarea.selectionStart = textarea.selectionEnd = newCursorPosition;
|
||||
|
||||
// 상위 컴포넌트의 onChange 콜백 호출 (상태 동기화)
|
||||
const syntheticEvent = {
|
||||
target: textarea,
|
||||
currentTarget: textarea,
|
||||
} as React.ChangeEvent<HTMLTextAreaElement>;
|
||||
onChange(syntheticEvent);
|
||||
|
||||
// 포커스를 textarea로 이동
|
||||
textarea.focus();
|
||||
|
||||
// 시각적 피드백 표시
|
||||
setLastInsertedCell(cellAddressToInsert);
|
||||
setShowCellInsertFeedback(true);
|
||||
|
||||
// 2초 후 피드백 숨김
|
||||
setTimeout(() => {
|
||||
setShowCellInsertFeedback(false);
|
||||
}, 2000);
|
||||
|
||||
console.log(`✅ PromptInput: 셀 주소 "${cellAddressToInsert}" 삽입 완료`);
|
||||
|
||||
// 셀 주소 삽입 상태 초기화 (중복 삽입 방지)
|
||||
setCellAddressToInsert(null);
|
||||
}
|
||||
}, [cellAddressToInsert, onChange, setCellAddressToInsert]);
|
||||
|
||||
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">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="flex-1 resize-none rounded-lg border border-gray-300 bg-gray-50 px-4 py-3 text-base text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-400 disabled:bg-gray-100 disabled:cursor-not-allowed min-h-[48px] max-h-32 shadow-sm"
|
||||
placeholder="질문을 입력하세요.
|
||||
예시) A1부터 A10까지 합계를 구해서 B1에 입력하는 수식을 입력해줘"
|
||||
예시) A1부터 A10까지 합계를 구해서 B1에 입력하는 수식을 입력해줘
|
||||
|
||||
💡 팁: 시트에서 셀을 선택하면 자동으로 주소가 입력됩니다"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={false}
|
||||
@@ -46,7 +152,8 @@ const PromptInput: React.FC<PromptInputProps> = ({
|
||||
</div>
|
||||
<div className="w-full max-w-3xl flex justify-between items-center mt-1 px-1">
|
||||
<span className="text-xs text-gray-500">
|
||||
Press Enter to send, Shift+Enter for new line
|
||||
Press Enter to send, Shift+Enter for new line | 시트에서 셀 클릭하여
|
||||
주소 자동 입력
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{value.length}/{maxLength}
|
||||
|
||||
Reference in New Issue
Block a user