diff --git a/package-lock.json b/package-lock.json index 3cd6841..a9b23d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3801,12 +3801,6 @@ "rxjs": ">=7.8" } }, - "node_modules/@univerjs/data-validation/node_modules/@wendellhu/redi": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@wendellhu/redi/-/redi-0.17.0.tgz", - "integrity": "sha512-lWO5N4u24rYItkiP8Atu63ItHnakIX5XV+jhG6V4EAoGHdo74ssx4PlQa/Nklh9vl2/3odb65BmW4cOHrimhXA==", - "license": "MIT" - }, "node_modules/@univerjs/data-validation/node_modules/nanoid": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", @@ -4355,12 +4349,6 @@ "rxjs": ">=7.0.0" } }, - "node_modules/@univerjs/facade/node_modules/@wendellhu/redi": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@wendellhu/redi/-/redi-0.17.0.tgz", - "integrity": "sha512-lWO5N4u24rYItkiP8Atu63ItHnakIX5XV+jhG6V4EAoGHdo74ssx4PlQa/Nklh9vl2/3odb65BmW4cOHrimhXA==", - "license": "MIT" - }, "node_modules/@univerjs/facade/node_modules/nanoid": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", @@ -4451,12 +4439,6 @@ "rxjs": ">=7.8" } }, - "node_modules/@univerjs/network/node_modules/@wendellhu/redi": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@wendellhu/redi/-/redi-0.17.0.tgz", - "integrity": "sha512-lWO5N4u24rYItkiP8Atu63ItHnakIX5XV+jhG6V4EAoGHdo74ssx4PlQa/Nklh9vl2/3odb65BmW4cOHrimhXA==", - "license": "MIT" - }, "node_modules/@univerjs/network/node_modules/nanoid": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", @@ -4676,12 +4658,6 @@ "rxjs": ">=7.0.0" } }, - "node_modules/@univerjs/sheets-conditional-formatting/node_modules/@wendellhu/redi": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@wendellhu/redi/-/redi-0.17.0.tgz", - "integrity": "sha512-lWO5N4u24rYItkiP8Atu63ItHnakIX5XV+jhG6V4EAoGHdo74ssx4PlQa/Nklh9vl2/3odb65BmW4cOHrimhXA==", - "license": "MIT" - }, "node_modules/@univerjs/sheets-conditional-formatting/node_modules/nanoid": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", @@ -4852,12 +4828,6 @@ "rxjs": ">=7.0.0" } }, - "node_modules/@univerjs/sheets-data-validation/node_modules/@wendellhu/redi": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@wendellhu/redi/-/redi-0.17.0.tgz", - "integrity": "sha512-lWO5N4u24rYItkiP8Atu63ItHnakIX5XV+jhG6V4EAoGHdo74ssx4PlQa/Nklh9vl2/3odb65BmW4cOHrimhXA==", - "license": "MIT" - }, "node_modules/@univerjs/sheets-data-validation/node_modules/nanoid": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", @@ -5005,12 +4975,6 @@ "rxjs": ">=7.0.0" } }, - "node_modules/@univerjs/sheets-filter/node_modules/@wendellhu/redi": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@wendellhu/redi/-/redi-0.17.0.tgz", - "integrity": "sha512-lWO5N4u24rYItkiP8Atu63ItHnakIX5XV+jhG6V4EAoGHdo74ssx4PlQa/Nklh9vl2/3odb65BmW4cOHrimhXA==", - "license": "MIT" - }, "node_modules/@univerjs/sheets-filter/node_modules/nanoid": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", @@ -5478,12 +5442,6 @@ "rxjs": ">=7.0.0" } }, - "node_modules/@univerjs/sheets-hyper-link-ui/node_modules/@wendellhu/redi": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@wendellhu/redi/-/redi-0.17.0.tgz", - "integrity": "sha512-lWO5N4u24rYItkiP8Atu63ItHnakIX5XV+jhG6V4EAoGHdo74ssx4PlQa/Nklh9vl2/3odb65BmW4cOHrimhXA==", - "license": "MIT" - }, "node_modules/@univerjs/sheets-hyper-link-ui/node_modules/nanoid": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", @@ -5652,12 +5610,6 @@ "rxjs": ">=7.0.0" } }, - "node_modules/@univerjs/sheets-hyper-link/node_modules/@wendellhu/redi": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@wendellhu/redi/-/redi-0.17.0.tgz", - "integrity": "sha512-lWO5N4u24rYItkiP8Atu63ItHnakIX5XV+jhG6V4EAoGHdo74ssx4PlQa/Nklh9vl2/3odb65BmW4cOHrimhXA==", - "license": "MIT" - }, "node_modules/@univerjs/sheets-hyper-link/node_modules/nanoid": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", @@ -5851,12 +5803,6 @@ "rxjs": ">=7.0.0" } }, - "node_modules/@univerjs/sheets-thread-comment/node_modules/@wendellhu/redi": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@wendellhu/redi/-/redi-0.17.0.tgz", - "integrity": "sha512-lWO5N4u24rYItkiP8Atu63ItHnakIX5XV+jhG6V4EAoGHdo74ssx4PlQa/Nklh9vl2/3odb65BmW4cOHrimhXA==", - "license": "MIT" - }, "node_modules/@univerjs/sheets-thread-comment/node_modules/nanoid": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", @@ -6234,12 +6180,6 @@ "rxjs": ">=7.0.0" } }, - "node_modules/@univerjs/sheets-zen-editor/node_modules/@wendellhu/redi": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@wendellhu/redi/-/redi-0.17.0.tgz", - "integrity": "sha512-lWO5N4u24rYItkiP8Atu63ItHnakIX5XV+jhG6V4EAoGHdo74ssx4PlQa/Nklh9vl2/3odb65BmW4cOHrimhXA==", - "license": "MIT" - }, "node_modules/@univerjs/sheets-zen-editor/node_modules/nanoid": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", @@ -6343,12 +6283,6 @@ "rxjs": ">=7.8" } }, - "node_modules/@univerjs/thread-comment/node_modules/@wendellhu/redi": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@wendellhu/redi/-/redi-0.17.0.tgz", - "integrity": "sha512-lWO5N4u24rYItkiP8Atu63ItHnakIX5XV+jhG6V4EAoGHdo74ssx4PlQa/Nklh9vl2/3odb65BmW4cOHrimhXA==", - "license": "MIT" - }, "node_modules/@univerjs/thread-comment/node_modules/nanoid": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", @@ -6842,12 +6776,6 @@ "rxjs": ">=7.8" } }, - "node_modules/@zwight/luckyexcel/node_modules/@wendellhu/redi": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@wendellhu/redi/-/redi-0.17.1.tgz", - "integrity": "sha512-wKEa1+kJi2KdRWerX6wxCqOdcyVfRSqopp9/BrWxEom5JXElUWNepgMB0Kvg0r+93BNTj0IgyNN7+bIGg11l+g==", - "license": "MIT" - }, "node_modules/@zwight/luckyexcel/node_modules/numfmt": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/numfmt/-/numfmt-2.5.2.tgz", diff --git a/src/components/sheet/EditSheetViewer.tsx b/src/components/sheet/EditSheetViewer.tsx index 458745b..db2e2e9 100644 --- a/src/components/sheet/EditSheetViewer.tsx +++ b/src/components/sheet/EditSheetViewer.tsx @@ -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(null); const mountedRef = useRef(false); - const [isInitialized, setIsInitialized] = useState(false); + const [isInitialized, setIsInitialized] = useState(false); const [showUploadOverlay, setShowUploadOverlay] = useState(true); const [isDragOver, setIsDragOver] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [currentFile, setCurrentFile] = useState(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 (
{/* 헤더 */} diff --git a/src/components/sheet/PromptInput.tsx b/src/components/sheet/PromptInput.tsx index 5e271fc..c55c276 100644 --- a/src/components/sheet/PromptInput.tsx +++ b/src/components/sheet/PromptInput.tsx @@ -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 = ({ value, @@ -19,13 +23,115 @@ const PromptInput: React.FC = ({ disabled = true, maxLength = 500, }) => { + const textareaRef = useRef(null); + const [showCellInsertFeedback, setShowCellInsertFeedback] = useState(false); + const [lastInsertedCell, setLastInsertedCell] = useState(null); + const [currentSelectedCell, setCurrentSelectedCell] = useState( + 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; + onChange(syntheticEvent); + + // 포커스를 textarea로 이동 + textarea.focus(); + + // 시각적 피드백 표시 + setLastInsertedCell(cellAddressToInsert); + setShowCellInsertFeedback(true); + + // 2초 후 피드백 숨김 + setTimeout(() => { + setShowCellInsertFeedback(false); + }, 2000); + + console.log(`✅ PromptInput: 셀 주소 "${cellAddressToInsert}" 삽입 완료`); + + // 셀 주소 삽입 상태 초기화 (중복 삽입 방지) + setCellAddressToInsert(null); + } + }, [cellAddressToInsert, onChange, setCellAddressToInsert]); + return ( -
+
+ {/* 현재 선택된 셀 정보 상태바 +
+
+
+ 현재 선택된 셀: + + {currentSelectedCell || "없음"} + +
+ 셀을 클릭하여 주소 확인 +
+
*/} + + {/* 셀 선택 피드백 알림 */} + {/* {showCellInsertFeedback && lastInsertedCell && ( +
+ 📍 셀 주소 "{lastInsertedCell}"이(가) 입력창에 삽입되었습니다 +
+ )} */} +