셀선택시 프롬프트 창에 자동 입력 완료
This commit is contained in:
72
package-lock.json
generated
72
package-lock.json
generated
@@ -3801,12 +3801,6 @@
|
|||||||
"rxjs": ">=7.8"
|
"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": {
|
"node_modules/@univerjs/data-validation/node_modules/nanoid": {
|
||||||
"version": "5.0.9",
|
"version": "5.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||||
@@ -4355,12 +4349,6 @@
|
|||||||
"rxjs": ">=7.0.0"
|
"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": {
|
"node_modules/@univerjs/facade/node_modules/nanoid": {
|
||||||
"version": "5.0.9",
|
"version": "5.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||||
@@ -4451,12 +4439,6 @@
|
|||||||
"rxjs": ">=7.8"
|
"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": {
|
"node_modules/@univerjs/network/node_modules/nanoid": {
|
||||||
"version": "5.0.9",
|
"version": "5.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||||
@@ -4676,12 +4658,6 @@
|
|||||||
"rxjs": ">=7.0.0"
|
"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": {
|
"node_modules/@univerjs/sheets-conditional-formatting/node_modules/nanoid": {
|
||||||
"version": "5.0.9",
|
"version": "5.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||||
@@ -4852,12 +4828,6 @@
|
|||||||
"rxjs": ">=7.0.0"
|
"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": {
|
"node_modules/@univerjs/sheets-data-validation/node_modules/nanoid": {
|
||||||
"version": "5.0.9",
|
"version": "5.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||||
@@ -5005,12 +4975,6 @@
|
|||||||
"rxjs": ">=7.0.0"
|
"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": {
|
"node_modules/@univerjs/sheets-filter/node_modules/nanoid": {
|
||||||
"version": "5.0.9",
|
"version": "5.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||||
@@ -5478,12 +5442,6 @@
|
|||||||
"rxjs": ">=7.0.0"
|
"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": {
|
"node_modules/@univerjs/sheets-hyper-link-ui/node_modules/nanoid": {
|
||||||
"version": "5.0.9",
|
"version": "5.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||||
@@ -5652,12 +5610,6 @@
|
|||||||
"rxjs": ">=7.0.0"
|
"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": {
|
"node_modules/@univerjs/sheets-hyper-link/node_modules/nanoid": {
|
||||||
"version": "5.0.9",
|
"version": "5.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||||
@@ -5851,12 +5803,6 @@
|
|||||||
"rxjs": ">=7.0.0"
|
"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": {
|
"node_modules/@univerjs/sheets-thread-comment/node_modules/nanoid": {
|
||||||
"version": "5.0.9",
|
"version": "5.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||||
@@ -6234,12 +6180,6 @@
|
|||||||
"rxjs": ">=7.0.0"
|
"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": {
|
"node_modules/@univerjs/sheets-zen-editor/node_modules/nanoid": {
|
||||||
"version": "5.0.9",
|
"version": "5.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||||
@@ -6343,12 +6283,6 @@
|
|||||||
"rxjs": ">=7.8"
|
"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": {
|
"node_modules/@univerjs/thread-comment/node_modules/nanoid": {
|
||||||
"version": "5.0.9",
|
"version": "5.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||||
@@ -6842,12 +6776,6 @@
|
|||||||
"rxjs": ">=7.8"
|
"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": {
|
"node_modules/@zwight/luckyexcel/node_modules/numfmt": {
|
||||||
"version": "2.5.2",
|
"version": "2.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/numfmt/-/numfmt-2.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/numfmt/-/numfmt-2.5.2.tgz",
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ import { UniverUIPlugin } from "@univerjs/ui";
|
|||||||
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 { rangeToAddress } from "../../utils/cellUtils";
|
||||||
|
import { CellSelectionHandler } from "../../utils/cellSelectionHandler";
|
||||||
|
|
||||||
|
// Facade API imports - 공식 문서 방식 (필요한 기능만 선택적 import)
|
||||||
|
import "@univerjs/sheets/facade";
|
||||||
|
import "@univerjs/sheets-ui/facade";
|
||||||
|
|
||||||
// 언어팩 import
|
// 언어팩 import
|
||||||
import DesignEnUS from "@univerjs/design/locale/en-US";
|
import DesignEnUS from "@univerjs/design/locale/en-US";
|
||||||
@@ -279,85 +286,99 @@ const TestSheetViewer: React.FC = () => {
|
|||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const mountedRef = useRef<boolean>(false);
|
const mountedRef = useRef<boolean>(false);
|
||||||
|
|
||||||
const [isInitialized, setIsInitialized] = useState(false);
|
const [isInitialized, setIsInitialized] = useState<boolean>(false);
|
||||||
const [showUploadOverlay, setShowUploadOverlay] = useState(true);
|
const [showUploadOverlay, setShowUploadOverlay] = useState(true);
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [currentFile, setCurrentFile] = useState<File | null>(null);
|
const [currentFile, setCurrentFile] = useState<File | null>(null);
|
||||||
const [prompt, setPrompt] = useState("");
|
const [prompt, setPrompt] = useState("");
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
// CellSelectionHandler 인스턴스 생성
|
||||||
|
const cellSelectionHandler = useRef(new CellSelectionHandler());
|
||||||
|
|
||||||
// Univer 초기화 함수
|
// Univer 초기화 함수
|
||||||
const initializeUniver = useCallback(async (workbookData?: any) => {
|
const initializeUniver = useCallback(
|
||||||
if (!containerRef.current || !mountedRef.current) {
|
async (workbookData?: any) => {
|
||||||
console.error("❌ 컨테이너가 준비되지 않았습니다!");
|
if (!containerRef.current || !mountedRef.current) {
|
||||||
return;
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 새 워크북 생성
|
try {
|
||||||
const workbook = univer.createUnit(
|
console.log("🚀 Univer 초기화 시작");
|
||||||
UniverInstanceType.UNIVER_SHEET,
|
|
||||||
workbookToUse,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("✅ 워크북 생성 완료:", workbook?.getUnitId());
|
// 전역 인스턴스 생성 또는 재사용
|
||||||
setIsInitialized(true);
|
const univer = await UniverseManager.createInstance(
|
||||||
} catch (error) {
|
containerRef.current,
|
||||||
console.error("❌ Univer 초기화 실패:", error);
|
);
|
||||||
setIsInitialized(false);
|
|
||||||
}
|
// 기본 워크북 데이터
|
||||||
}, []);
|
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(
|
const processFile = useCallback(
|
||||||
@@ -431,7 +452,7 @@ const TestSheetViewer: React.FC = () => {
|
|||||||
console.log("✅ 파일 처리 완료");
|
console.log("✅ 파일 처리 완료");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isProcessing, initializeUniver],
|
[initializeUniver],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 파일 입력 변경 처리
|
// 파일 입력 변경 처리
|
||||||
@@ -559,6 +580,16 @@ const TestSheetViewer: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 컴포넌트 언마운트 시 리소스 정리
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
// 셀 선택 핸들러 정리
|
||||||
|
if (cellSelectionHandler.current.isActive()) {
|
||||||
|
cellSelectionHandler.current.dispose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-screen flex flex-col relative">
|
<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 {
|
interface PromptInputProps {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -11,6 +12,9 @@ interface PromptInputProps {
|
|||||||
/**
|
/**
|
||||||
* 에디트 화면 하단 고정 프롬프트 입력창 컴포넌트
|
* 에디트 화면 하단 고정 프롬프트 입력창 컴포넌트
|
||||||
* - 이미지 참고: 입력창, Execute 버튼, 안내문구, 글자수 카운트, 하단 고정
|
* - 이미지 참고: 입력창, Execute 버튼, 안내문구, 글자수 카운트, 하단 고정
|
||||||
|
* - 유니버 시트에서 셀 선택 시 자동으로 셀 주소 삽입 기능 포함
|
||||||
|
* - 선택된 셀 정보 실시간 표시 및 시각적 피드백 제공
|
||||||
|
* - 현재 선택된 셀 정보 상태바 표시
|
||||||
*/
|
*/
|
||||||
const PromptInput: React.FC<PromptInputProps> = ({
|
const PromptInput: React.FC<PromptInputProps> = ({
|
||||||
value,
|
value,
|
||||||
@@ -19,13 +23,115 @@ const PromptInput: React.FC<PromptInputProps> = ({
|
|||||||
disabled = true,
|
disabled = true,
|
||||||
maxLength = 500,
|
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 (
|
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}
|
||||||
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"
|
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="질문을 입력하세요.
|
placeholder="질문을 입력하세요.
|
||||||
예시) A1부터 A10까지 합계를 구해서 B1에 입력하는 수식을 입력해줘"
|
예시) A1부터 A10까지 합계를 구해서 B1에 입력하는 수식을 입력해줘
|
||||||
|
|
||||||
|
💡 팁: 시트에서 셀을 선택하면 자동으로 주소가 입력됩니다"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
@@ -46,7 +152,8 @@ const PromptInput: React.FC<PromptInputProps> = ({
|
|||||||
</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">
|
||||||
<span className="text-xs text-gray-500">
|
<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>
|
||||||
<span className="text-xs text-gray-400">
|
<span className="text-xs text-gray-400">
|
||||||
{value.length}/{maxLength}
|
{value.length}/{maxLength}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ interface AppState {
|
|||||||
activeSheetId: string | null;
|
activeSheetId: string | null;
|
||||||
selectedRange: SelectedRange | null;
|
selectedRange: SelectedRange | null;
|
||||||
|
|
||||||
|
// 셀 주소 삽입 상태 (입력창용)
|
||||||
|
cellAddressToInsert: string | null;
|
||||||
|
|
||||||
// UI 상태
|
// UI 상태
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
loadingMessage: string;
|
loadingMessage: string;
|
||||||
@@ -53,6 +56,9 @@ interface AppState {
|
|||||||
setActiveSheetId: (sheetId: string | null) => void;
|
setActiveSheetId: (sheetId: string | null) => void;
|
||||||
setSelectedRange: (range: SelectedRange | null) => void;
|
setSelectedRange: (range: SelectedRange | null) => void;
|
||||||
|
|
||||||
|
// 셀 주소 삽입 액션
|
||||||
|
setCellAddressToInsert: (address: string | null) => void;
|
||||||
|
|
||||||
setLoading: (loading: boolean, message?: string) => void;
|
setLoading: (loading: boolean, message?: string) => void;
|
||||||
setHistoryPanelOpen: (open: boolean) => void;
|
setHistoryPanelOpen: (open: boolean) => void;
|
||||||
|
|
||||||
@@ -76,6 +82,7 @@ const initialState = {
|
|||||||
sheets: [],
|
sheets: [],
|
||||||
activeSheetId: null,
|
activeSheetId: null,
|
||||||
selectedRange: null,
|
selectedRange: null,
|
||||||
|
cellAddressToInsert: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
loadingMessage: "",
|
loadingMessage: "",
|
||||||
isHistoryPanelOpen: false,
|
isHistoryPanelOpen: false,
|
||||||
@@ -101,6 +108,10 @@ export const useAppStore = create<AppState>()(
|
|||||||
setActiveSheetId: (sheetId) => set({ activeSheetId: sheetId }),
|
setActiveSheetId: (sheetId) => set({ activeSheetId: sheetId }),
|
||||||
setSelectedRange: (range) => set({ selectedRange: range }),
|
setSelectedRange: (range) => set({ selectedRange: range }),
|
||||||
|
|
||||||
|
// 셀 주소 삽입 액션
|
||||||
|
setCellAddressToInsert: (address) =>
|
||||||
|
set({ cellAddressToInsert: address }),
|
||||||
|
|
||||||
// UI 액션
|
// UI 액션
|
||||||
setLoading: (loading, message = "") =>
|
setLoading: (loading, message = "") =>
|
||||||
set({
|
set({
|
||||||
|
|||||||
182
src/utils/cellSelectionHandler.ts
Normal file
182
src/utils/cellSelectionHandler.ts
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
// Facade API imports - 공식 문서 방식 (필요한 기능만 선택적 import)
|
||||||
|
import "@univerjs/sheets/facade";
|
||||||
|
import "@univerjs/sheets-ui/facade";
|
||||||
|
|
||||||
|
import { FUniver } from "@univerjs/core/facade";
|
||||||
|
import { coordsToAddress, rangeToAddress } from "./cellUtils";
|
||||||
|
import { useAppStore } from "../stores/useAppStore";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 셀 선택 이벤트 핸들러 클래스
|
||||||
|
* - Univer 셀 선택 이벤트를 감지하고 처리
|
||||||
|
* - 선택된 셀 주소를 프롬프트 입력창에 자동 삽입
|
||||||
|
* - SRP: 셀 선택과 관련된 로직만 담당
|
||||||
|
*/
|
||||||
|
export class CellSelectionHandler {
|
||||||
|
private disposable: any = null;
|
||||||
|
private univerAPI: any = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 셀 선택 이벤트 리스너 초기화
|
||||||
|
* @param univer - Univer 인스턴스
|
||||||
|
*/
|
||||||
|
public initialize(univer: any): void {
|
||||||
|
try {
|
||||||
|
// Univer CE 공식 방식으로 FUniver API 생성
|
||||||
|
this.univerAPI = FUniver.newAPI(univer);
|
||||||
|
|
||||||
|
// 셀 선택 변경 이벤트 구독 - 공식 문서 패턴 사용
|
||||||
|
this.disposable = this.univerAPI.addEvent(
|
||||||
|
this.univerAPI.Event.SelectionChanged,
|
||||||
|
this.handleSelectionChange.bind(this),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("📌 CellSelectionHandler: 셀 선택 이벤트 리스너 등록 완료");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ CellSelectionHandler 초기화 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 셀 선택 변경 이벤트 핸들러
|
||||||
|
* @param params - 선택 변경 이벤트 파라미터
|
||||||
|
*/
|
||||||
|
private handleSelectionChange(params: any): void {
|
||||||
|
try {
|
||||||
|
console.log("🎯 셀 선택 변경 감지 - 전체 파라미터:", params);
|
||||||
|
console.log("🔍 파라미터 타입:", typeof params);
|
||||||
|
console.log("🔍 파라미터 키들:", Object.keys(params || {}));
|
||||||
|
|
||||||
|
// 다양한 경로로 선택 정보 탐색
|
||||||
|
let selectionData = null;
|
||||||
|
let cellAddress: string | null = null;
|
||||||
|
let selectionType: "single" | "range" = "single";
|
||||||
|
|
||||||
|
// 방법 1: params.selections 경로
|
||||||
|
if (params && params.selections && params.selections.length > 0) {
|
||||||
|
console.log("📍 방법 1: params.selections 경로에서 데이터 발견");
|
||||||
|
selectionData = params.selections[0];
|
||||||
|
console.log("📍 selections[0]:", selectionData);
|
||||||
|
console.log("📍 selections[0] 키들:", Object.keys(selectionData || {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 방법 2: params.selection 경로 (단수형)
|
||||||
|
else if (params && params.selection) {
|
||||||
|
console.log("📍 방법 2: params.selection 경로에서 데이터 발견");
|
||||||
|
selectionData = params.selection;
|
||||||
|
console.log("📍 selection:", selectionData);
|
||||||
|
console.log("📍 selection 키들:", Object.keys(selectionData || {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 방법 3: params 자체가 선택 데이터인 경우
|
||||||
|
else if (
|
||||||
|
params &&
|
||||||
|
(params.startRow !== undefined || params.row !== undefined)
|
||||||
|
) {
|
||||||
|
console.log("📍 방법 3: params 자체가 선택 데이터");
|
||||||
|
selectionData = params;
|
||||||
|
console.log("📍 직접 params:", selectionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택 데이터에서 좌표 추출
|
||||||
|
if (selectionData) {
|
||||||
|
let startRow, startCol, endRow, endCol;
|
||||||
|
|
||||||
|
// 패턴 A: range 객체 (기존 방식)
|
||||||
|
if (selectionData.range) {
|
||||||
|
console.log("🎯 패턴 A: range 객체 발견");
|
||||||
|
const range = selectionData.range;
|
||||||
|
startRow = range.startRow;
|
||||||
|
startCol = range.startColumn || range.startCol;
|
||||||
|
endRow = range.endRow;
|
||||||
|
endCol = range.endColumn || range.endCol;
|
||||||
|
}
|
||||||
|
// 패턴 B: 직접 좌표 (startRow, startCol 등)
|
||||||
|
else if (selectionData.startRow !== undefined) {
|
||||||
|
console.log("🎯 패턴 B: 직접 좌표 발견 (startRow/startCol)");
|
||||||
|
startRow = selectionData.startRow;
|
||||||
|
startCol = selectionData.startColumn || selectionData.startCol;
|
||||||
|
endRow = selectionData.endRow || startRow;
|
||||||
|
endCol = selectionData.endColumn || selectionData.endCol || startCol;
|
||||||
|
}
|
||||||
|
// 패턴 C: row, col 형태
|
||||||
|
else if (selectionData.row !== undefined) {
|
||||||
|
console.log("🎯 패턴 C: row/col 형태 발견");
|
||||||
|
startRow = selectionData.row;
|
||||||
|
startCol = selectionData.col || selectionData.column || 0;
|
||||||
|
endRow = selectionData.endRow || startRow;
|
||||||
|
endCol = selectionData.endCol || selectionData.endColumn || startCol;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 좌표가 발견되었으면 주소 변환
|
||||||
|
if (startRow !== undefined && startCol !== undefined) {
|
||||||
|
console.log(
|
||||||
|
`📍 좌표 발견: (${startRow}, ${startCol}) → (${endRow}, ${endCol})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (startRow === endRow && startCol === endCol) {
|
||||||
|
// 단일 셀 선택
|
||||||
|
cellAddress = coordsToAddress(startRow, startCol);
|
||||||
|
selectionType = "single";
|
||||||
|
console.log(
|
||||||
|
`📍 단일 셀 선택: ${cellAddress} (row: ${startRow}, col: ${startCol})`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 범위 선택
|
||||||
|
cellAddress = rangeToAddress({
|
||||||
|
startRow,
|
||||||
|
startCol,
|
||||||
|
endRow: endRow || startRow,
|
||||||
|
endCol: endCol || startCol,
|
||||||
|
});
|
||||||
|
selectionType = "range";
|
||||||
|
console.log(
|
||||||
|
`📍 범위 선택: ${cellAddress} (${startRow},${startCol} → ${endRow},${endCol})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🎯 선택 타입: ${selectionType}, 주소: ${cellAddress}`);
|
||||||
|
|
||||||
|
// zustand store에 셀 주소 저장
|
||||||
|
const appStore = useAppStore.getState();
|
||||||
|
appStore.setCellAddressToInsert(cellAddress);
|
||||||
|
|
||||||
|
console.log(`✅ 셀 주소 "${cellAddress}" 스토어에 저장 완료`);
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ 좌표 정보를 찾을 수 없습니다:");
|
||||||
|
console.warn("⚠️ selectionData:", selectionData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ 선택 데이터를 찾을 수 없습니다:");
|
||||||
|
console.warn("⚠️ 전체 params:", params);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 셀 선택 이벤트 처리 실패:", error);
|
||||||
|
console.error("❌ 이벤트 파라미터:", params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 리스너 정리
|
||||||
|
*/
|
||||||
|
public dispose(): void {
|
||||||
|
if (this.disposable) {
|
||||||
|
this.disposable.dispose();
|
||||||
|
this.disposable = null;
|
||||||
|
console.log("🧹 CellSelectionHandler: 이벤트 리스너 정리 완료");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 핸들러가 활성 상태인지 확인
|
||||||
|
*/
|
||||||
|
public isActive(): boolean {
|
||||||
|
return this.disposable !== null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전역 셀 선택 핸들러 인스턴스
|
||||||
|
* - 싱글톤 패턴으로 관리
|
||||||
|
*/
|
||||||
|
export const cellSelectionHandler = new CellSelectionHandler();
|
||||||
53
src/utils/cellUtils.ts
Normal file
53
src/utils/cellUtils.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 셀 좌표 관련 유틸리티 함수들
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컬럼 번호를 Excel 컬럼 문자로 변환 (0-based)
|
||||||
|
* 예: 0 -> A, 1 -> B, 25 -> Z, 26 -> AA
|
||||||
|
*/
|
||||||
|
export const numberToColumnLetter = (colNum: number): string => {
|
||||||
|
let result = "";
|
||||||
|
let num = colNum;
|
||||||
|
|
||||||
|
while (num >= 0) {
|
||||||
|
result = String.fromCharCode(65 + (num % 26)) + result;
|
||||||
|
num = Math.floor(num / 26) - 1;
|
||||||
|
if (num < 0) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* row/col 좌표를 Excel 셀 주소로 변환 (0-based)
|
||||||
|
* 예: (0, 0) -> A1, (19, 1) -> B20
|
||||||
|
*/
|
||||||
|
export const coordsToAddress = (row: number, col: number): string => {
|
||||||
|
const columnLetter = numberToColumnLetter(col);
|
||||||
|
const rowNumber = row + 1; // Excel은 1-based
|
||||||
|
return `${columnLetter}${rowNumber}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CellRange를 셀 주소 문자열로 변환
|
||||||
|
* 단일 셀인 경우: "A1"
|
||||||
|
* 범위인 경우: "A1:B3"
|
||||||
|
*/
|
||||||
|
export const rangeToAddress = (range: {
|
||||||
|
startRow: number;
|
||||||
|
startCol: number;
|
||||||
|
endRow: number;
|
||||||
|
endCol: number;
|
||||||
|
}): string => {
|
||||||
|
const startAddress = coordsToAddress(range.startRow, range.startCol);
|
||||||
|
|
||||||
|
// 단일 셀인 경우
|
||||||
|
if (range.startRow === range.endRow && range.startCol === range.endCol) {
|
||||||
|
return startAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 범위인 경우
|
||||||
|
const endAddress = coordsToAddress(range.endRow, range.endCol);
|
||||||
|
return `${startAddress}:${endAddress}`;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user