xlsx 파일 주입 완료
This commit is contained in:
326
.cursor/rules/univer-initialization.mdc
Normal file
326
.cursor/rules/univer-initialization.mdc
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Univer CE 초기화 및 인스턴스 관리 규칙
|
||||||
|
|
||||||
|
## **핵심 원칙**
|
||||||
|
- **단일 인스턴스**: 컴포넌트당 하나의 Univer 인스턴스만 유지
|
||||||
|
- **중복 방지**: 플러그인 등록과 초기화 중복 실행 방지
|
||||||
|
- **적절한 정리**: 메모리 누수 방지를 위한 dispose 패턴
|
||||||
|
|
||||||
|
## **초기화 패턴**
|
||||||
|
|
||||||
|
### ✅ DO: 안전한 초기화
|
||||||
|
```typescript
|
||||||
|
// 인스턴스 존재 확인 후 초기화
|
||||||
|
useEffect(() => {
|
||||||
|
if (!univerRef.current) {
|
||||||
|
initializeUniver();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 처리 중 상태 확인으로 중복 방지
|
||||||
|
const processFile = useCallback(async (file: File) => {
|
||||||
|
if (isProcessing) {
|
||||||
|
console.log("이미 파일 처리 중입니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
|
// ... 파일 처리 로직
|
||||||
|
}, [isProcessing]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ DON'T: 중복 초기화 유발
|
||||||
|
```typescript
|
||||||
|
// 조건 없는 초기화 (중복 실행 위험)
|
||||||
|
useEffect(() => {
|
||||||
|
initializeUniver(); // 매번 실행됨
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 상태 확인 없는 처리
|
||||||
|
const processFile = useCallback(async (file: File) => {
|
||||||
|
// isProcessing 확인 없이 바로 실행
|
||||||
|
setIsProcessing(true);
|
||||||
|
}, []);
|
||||||
|
```
|
||||||
|
|
||||||
|
## **인스턴스 관리**
|
||||||
|
|
||||||
|
### ✅ DO: 적절한 dispose 패턴
|
||||||
|
```typescript
|
||||||
|
// 새 인스턴스 생성 전 기존 인스턴스 정리
|
||||||
|
if (univerRef.current) {
|
||||||
|
univerRef.current.dispose();
|
||||||
|
univerRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 컴포넌트 언마운트 시 정리
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (univerRef.current) {
|
||||||
|
univerRef.current.dispose();
|
||||||
|
univerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ DON'T: 메모리 누수 위험
|
||||||
|
```typescript
|
||||||
|
// dispose 없이 새 인스턴스 생성
|
||||||
|
univerRef.current = univer.createUnit(UnitType.UNIVER_SHEET, workbook);
|
||||||
|
|
||||||
|
// 정리 로직 없는 컴포넌트
|
||||||
|
// useEffect cleanup 누락
|
||||||
|
```
|
||||||
|
|
||||||
|
## **파일 처리 최적화**
|
||||||
|
|
||||||
|
### ✅ DO: 중복 처리 방지
|
||||||
|
```typescript
|
||||||
|
// 파일 입력 초기화로 재선택 가능
|
||||||
|
finally {
|
||||||
|
if (event.target) {
|
||||||
|
event.target.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 의존성 배열에 상태 포함
|
||||||
|
const processFile = useCallback(async (file: File) => {
|
||||||
|
// ... 로직
|
||||||
|
}, [isProcessing]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## **REDI 중복 로드 방지**
|
||||||
|
|
||||||
|
### ✅ DO: 라이브러리 중복 확인
|
||||||
|
```typescript
|
||||||
|
// 개발 환경에서 REDI 중복 로드 경고 모니터링
|
||||||
|
// 동일한 라이브러리 버전 사용 확인
|
||||||
|
// 번들링 시 중복 제거 설정
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ DON'T: 무시하면 안 되는 경고
|
||||||
|
```typescript
|
||||||
|
// REDI 중복 로드 경고 무시
|
||||||
|
// 서로 다른 버전의 동일 라이브러리 사용
|
||||||
|
// 번들 중복 제거 설정 누락
|
||||||
|
```
|
||||||
|
|
||||||
|
## **디버깅 및 로깅**
|
||||||
|
|
||||||
|
### ✅ DO: 의미있는 로그
|
||||||
|
```typescript
|
||||||
|
console.log("초기화할 워크북 데이터:", workbookData);
|
||||||
|
console.log("Univer 인스턴스 생성 완료");
|
||||||
|
console.log("플러그인 등록 완료");
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ DON'T: 과도한 로깅
|
||||||
|
```typescript
|
||||||
|
// 매 렌더링마다 로그 출력
|
||||||
|
// 민감한 데이터 로깅
|
||||||
|
// 프로덕션 환경 디버그 로그 유지
|
||||||
|
```
|
||||||
|
|
||||||
|
## **성능 최적화**
|
||||||
|
|
||||||
|
- **useCallback**: 이벤트 핸들러 메모이제이션
|
||||||
|
- **의존성 최적화**: 필요한 의존성만 포함
|
||||||
|
- **조건부 실행**: 불필요한 재실행 방지
|
||||||
|
- **메모리 관리**: 적절한 dispose와 cleanup
|
||||||
|
|
||||||
|
이 규칙을 따르면 Univer CE가 안정적으로 작동하고 메모리 누수 없이 파일 처리가 가능합니다.
|
||||||
|
|
||||||
|
# Univer CE REDI 중복 로드 오류 완전 방지 규칙
|
||||||
|
|
||||||
|
## **문제 원인**
|
||||||
|
- Univer CE의 REDI 시스템에서 "Identifier already exists" 오류는 동일한 서비스가 중복으로 등록될 때 발생
|
||||||
|
- 컴포넌트 재마운트, HMR(Hot Module Reload), 또는 동시 초기화 시도로 인한 중복 인스턴스 생성
|
||||||
|
- 브라우저 캐시에 잔존하는 이전 REDI 등록 정보
|
||||||
|
|
||||||
|
## **필수 해결 패턴**
|
||||||
|
|
||||||
|
### **1. 전역 인스턴스 관리자 패턴 사용**
|
||||||
|
```typescript
|
||||||
|
// ✅ DO: 모듈 레벨에서 단일 인스턴스 보장
|
||||||
|
let globalUniver: Univer | null = null;
|
||||||
|
let globalInitializing = false;
|
||||||
|
let globalDisposing = false;
|
||||||
|
|
||||||
|
const UniverseManager = {
|
||||||
|
async createInstance(container: HTMLElement): Promise<Univer> {
|
||||||
|
if (globalUniver) return globalUniver;
|
||||||
|
|
||||||
|
if (globalInitializing) {
|
||||||
|
// 초기화 완료까지 대기
|
||||||
|
while (globalInitializing) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
}
|
||||||
|
if (globalUniver) return globalUniver;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalInitializing = true;
|
||||||
|
try {
|
||||||
|
// Univer 인스턴스 생성 로직
|
||||||
|
globalUniver = new Univer({...});
|
||||||
|
return globalUniver;
|
||||||
|
} finally {
|
||||||
|
globalInitializing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getInstance(): Univer | null {
|
||||||
|
return globalUniver;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ❌ DON'T: 컴포넌트마다 개별 인스턴스 생성
|
||||||
|
const MyComponent = () => {
|
||||||
|
const [univer, setUniver] = useState<Univer | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newUniver = new Univer({...}); // 중복 생성 위험
|
||||||
|
setUniver(newUniver);
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. 상태 기반 중복 초기화 방지**
|
||||||
|
```typescript
|
||||||
|
// ✅ DO: 강화된 상태 확인으로 중복 실행 완전 차단
|
||||||
|
const processFile = useCallback(async (file: File) => {
|
||||||
|
if (
|
||||||
|
isProcessing ||
|
||||||
|
UniverseManager.isInitializing() ||
|
||||||
|
UniverseManager.isDisposing()
|
||||||
|
) {
|
||||||
|
console.log("처리 중이거나 상태 변경 중입니다.");
|
||||||
|
return; // 중복 실행 차단
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
|
try {
|
||||||
|
// 파일 처리 로직
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
|
}, [isProcessing]); // 의존성 배열에 상태 포함
|
||||||
|
|
||||||
|
// ❌ DON'T: 간단한 상태 확인만으로는 불충분
|
||||||
|
const processFile = useCallback(async (file: File) => {
|
||||||
|
if (isProcessing) return; // 다른 상태는 확인하지 않음
|
||||||
|
}, []);
|
||||||
|
```
|
||||||
|
|
||||||
|
### **3. 컴포넌트 마운트 시 기존 인스턴스 재사용**
|
||||||
|
```typescript
|
||||||
|
// ✅ DO: 기존 전역 인스턴스 우선 재사용
|
||||||
|
useEffect(() => {
|
||||||
|
const existingUniver = UniverseManager.getInstance();
|
||||||
|
if (existingUniver && !UniverseManager.isInitializing()) {
|
||||||
|
setIsInitialized(true);
|
||||||
|
return; // 재사용으로 중복 초기화 방지
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containerRef.current && !UniverseManager.isInitializing()) {
|
||||||
|
initializeUniver();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ❌ DON'T: 매번 새로운 초기화 시도
|
||||||
|
useEffect(() => {
|
||||||
|
initializeUniver(); // 기존 인스턴스 무시하고 새로 생성
|
||||||
|
}, []);
|
||||||
|
```
|
||||||
|
|
||||||
|
### **4. 디버깅을 위한 전역 상태 제공**
|
||||||
|
```typescript
|
||||||
|
// ✅ DO: 전역 디버그 객체로 상태 추적 가능
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
(window as any).__UNIVER_DEBUG__ = {
|
||||||
|
getGlobalUniver: () => globalUniver,
|
||||||
|
getGlobalInitializing: () => globalInitializing,
|
||||||
|
clearGlobalState: () => {
|
||||||
|
globalUniver = null;
|
||||||
|
globalInitializing = false;
|
||||||
|
globalDisposing = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **5. 기존 워크북 정리 시 API 호환성 처리**
|
||||||
|
```typescript
|
||||||
|
// ✅ DO: try-catch로 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ DON'T: API 호환성 고려하지 않은 직접 호출
|
||||||
|
univer.getUnitsForType(UniverInstanceType.UNIVER_SHEET); // 버전에 따라 실패 가능
|
||||||
|
```
|
||||||
|
|
||||||
|
## **브라우저 캐시 완전 삭제**
|
||||||
|
|
||||||
|
### **개발 환경에서 REDI 오류 발생 시 필수 작업**
|
||||||
|
```bash
|
||||||
|
# ✅ DO: 브라우저 캐시 완전 삭제
|
||||||
|
rm -rf node_modules/.vite && rm -rf dist
|
||||||
|
|
||||||
|
# 추가적으로 브라우저에서:
|
||||||
|
# - 강제 새로고침 (Ctrl+Shift+R 또는 Cmd+Shift+R)
|
||||||
|
# - 개발자 도구 > Application > Storage > Clear storage
|
||||||
|
```
|
||||||
|
|
||||||
|
## **오류 패턴 및 해결책**
|
||||||
|
|
||||||
|
### **"Identifier already exists" 오류**
|
||||||
|
- **원인**: REDI 시스템에서 동일한 식별자의 서비스가 중복 등록
|
||||||
|
- **해결**: 전역 인스턴스 관리자 패턴으로 단일 인스턴스 보장
|
||||||
|
|
||||||
|
### **컴포넌트 재마운트 시 중복 초기화**
|
||||||
|
- **원인**: useEffect가 매번 새로운 초기화 시도
|
||||||
|
- **해결**: 기존 전역 인스턴스 존재 확인 후 재사용
|
||||||
|
|
||||||
|
### **HMR 환경에서 상태 불안정**
|
||||||
|
- **원인**: 모듈 재로드 시 전역 상태 초기화
|
||||||
|
- **해결**: 브라우저 캐시 삭제 + window 객체 기반 상태 추적
|
||||||
|
|
||||||
|
## **성능 최적화**
|
||||||
|
|
||||||
|
### **초기화 대기 로직**
|
||||||
|
```typescript
|
||||||
|
// ✅ DO: Promise 기반 비동기 대기
|
||||||
|
while (globalInitializing) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ DON'T: 동기 대기나 긴 interval
|
||||||
|
setInterval(() => { /* 체크 로직 */ }, 1000); // 너무 느림
|
||||||
|
```
|
||||||
|
|
||||||
|
### **메모리 누수 방지**
|
||||||
|
```typescript
|
||||||
|
// ✅ DO: 컴포넌트 언마운트 시 상태만 정리
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setIsInitialized(false);
|
||||||
|
// 전역 인스턴스는 앱 종료 시에만 정리
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
```
|
||||||
|
|
||||||
|
## **참고사항**
|
||||||
|
- 이 패턴은 Univer CE의 REDI 아키텍처 특성상 필수적임
|
||||||
|
- 전역 인스턴스 관리로 브라우저 재로드 없이 안정적 운용 가능
|
||||||
|
- 개발 환경에서 오류 발생 시 반드시 캐시 삭제 후 재시작
|
||||||
915
package-lock.json
generated
915
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@
|
|||||||
"@univerjs/sheets-ui": "^0.8.2",
|
"@univerjs/sheets-ui": "^0.8.2",
|
||||||
"@univerjs/ui": "^0.8.2",
|
"@univerjs/ui": "^0.8.2",
|
||||||
"@univerjs/uniscript": "^0.8.2",
|
"@univerjs/uniscript": "^0.8.2",
|
||||||
|
"@zwight/luckyexcel": "^1.1.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { UniverSheetsNumfmtPlugin } from "@univerjs/sheets-numfmt";
|
|||||||
import { UniverSheetsNumfmtUIPlugin } from "@univerjs/sheets-numfmt-ui";
|
import { UniverSheetsNumfmtUIPlugin } from "@univerjs/sheets-numfmt-ui";
|
||||||
import { UniverUIPlugin } from "@univerjs/ui";
|
import { UniverUIPlugin } from "@univerjs/ui";
|
||||||
import { cn } from "../../lib/utils";
|
import { cn } from "../../lib/utils";
|
||||||
|
import LuckyExcel from "@zwight/luckyexcel";
|
||||||
|
|
||||||
// 언어팩 import
|
// 언어팩 import
|
||||||
import DesignEnUS from "@univerjs/design/locale/en-US";
|
import DesignEnUS from "@univerjs/design/locale/en-US";
|
||||||
@@ -31,40 +32,101 @@ import "@univerjs/sheets-ui/lib/index.css";
|
|||||||
import "@univerjs/sheets-formula-ui/lib/index.css";
|
import "@univerjs/sheets-formula-ui/lib/index.css";
|
||||||
import "@univerjs/sheets-numfmt-ui/lib/index.css";
|
import "@univerjs/sheets-numfmt-ui/lib/index.css";
|
||||||
|
|
||||||
|
// 전역 고유 키 생성
|
||||||
|
const GLOBAL_UNIVER_KEY = "__GLOBAL_UNIVER_INSTANCE__";
|
||||||
|
const GLOBAL_STATE_KEY = "__GLOBAL_UNIVER_STATE__";
|
||||||
|
|
||||||
|
// 전역 상태 인터페이스
|
||||||
|
interface GlobalUniverState {
|
||||||
|
instance: Univer | null;
|
||||||
|
isInitializing: boolean;
|
||||||
|
isDisposing: boolean;
|
||||||
|
initializationPromise: Promise<Univer> | null;
|
||||||
|
lastContainerId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window 객체에 전역 상태 확장
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
[GLOBAL_UNIVER_KEY]: Univer | null;
|
||||||
|
[GLOBAL_STATE_KEY]: GlobalUniverState;
|
||||||
|
__UNIVER_DEBUG__: {
|
||||||
|
getGlobalUniver: () => Univer | null;
|
||||||
|
getGlobalState: () => GlobalUniverState;
|
||||||
|
clearGlobalState: () => void;
|
||||||
|
forceReset: () => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전역 상태 초기화 함수
|
||||||
|
const initializeGlobalState = (): GlobalUniverState => {
|
||||||
|
if (!window[GLOBAL_STATE_KEY]) {
|
||||||
|
window[GLOBAL_STATE_KEY] = {
|
||||||
|
instance: null,
|
||||||
|
isInitializing: false,
|
||||||
|
isDisposing: false,
|
||||||
|
initializationPromise: null,
|
||||||
|
lastContainerId: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return window[GLOBAL_STATE_KEY];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 전역 상태 가져오기
|
||||||
|
const getGlobalState = (): GlobalUniverState => {
|
||||||
|
return window[GLOBAL_STATE_KEY] || initializeGlobalState();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Univer CE + 파일 업로드 오버레이
|
* Window 객체 기반 강화된 전역 Univer 관리자
|
||||||
* - Univer CE는 항상 렌더링 (기본 레이어)
|
* 모듈 재로드와 HMR에도 안전하게 작동
|
||||||
* - 오버레이로 파일 업로드 UI 표시
|
|
||||||
* - disposeUnit()으로 메모리 관리
|
|
||||||
*/
|
*/
|
||||||
const TestSheetViewer: React.FC = () => {
|
const UniverseManager = {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
// 전역 인스턴스 생성 (완전 단일 인스턴스 보장)
|
||||||
const univerRef = useRef<Univer | null>(null);
|
async createInstance(container: HTMLElement): Promise<Univer> {
|
||||||
const initializingRef = useRef<boolean>(false);
|
const state = getGlobalState();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const containerId = container.id || `container-${Date.now()}`;
|
||||||
|
|
||||||
const [isInitialized, setIsInitialized] = useState(false);
|
console.log(`🚀 Univer 인스턴스 생성 요청 - Container: ${containerId}`);
|
||||||
const [showUploadOverlay, setShowUploadOverlay] = useState(true); // 초기에는 오버레이 표시
|
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
|
||||||
const [currentFile, setCurrentFile] = useState<File | null>(null);
|
|
||||||
|
|
||||||
// Univer 초기화 - 공식 문서 패턴 따라서
|
// 이미 존재하는 인스턴스가 있고 같은 컨테이너면 재사용
|
||||||
useEffect(() => {
|
if (state.instance && state.lastContainerId === containerId) {
|
||||||
if (
|
console.log("✅ 기존 전역 Univer 인스턴스 재사용");
|
||||||
!containerRef.current ||
|
return state.instance;
|
||||||
isInitialized ||
|
}
|
||||||
univerRef.current ||
|
|
||||||
initializingRef.current
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const initializeUniver = async () => {
|
// 초기화가 진행 중이면 대기
|
||||||
|
if (state.isInitializing && state.initializationPromise) {
|
||||||
|
console.log("⏳ 기존 초기화 프로세스 대기 중...");
|
||||||
|
return state.initializationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 새로운 초기화 시작
|
||||||
|
state.isInitializing = true;
|
||||||
|
console.log("🔄 새로운 Univer 인스턴스 생성 시작");
|
||||||
|
|
||||||
|
// 기존 인스턴스 정리
|
||||||
|
if (state.instance) {
|
||||||
try {
|
try {
|
||||||
initializingRef.current = true;
|
console.log("🗑️ 기존 인스턴스 정리 중...");
|
||||||
console.log("🚀 Univer CE 초기화 시작");
|
await state.instance.dispose();
|
||||||
|
state.instance = null;
|
||||||
|
window[GLOBAL_UNIVER_KEY] = null;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("⚠️ 기존 인스턴스 정리 중 오류:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Univer 인스턴스 생성
|
// 초기화 Promise 생성
|
||||||
|
state.initializationPromise = (async () => {
|
||||||
|
try {
|
||||||
|
// 컨테이너 ID 설정
|
||||||
|
if (!container.id) {
|
||||||
|
container.id = containerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🛠️ Univer 인스턴스 생성 중...");
|
||||||
const univer = new Univer({
|
const univer = new Univer({
|
||||||
theme: defaultTheme,
|
theme: defaultTheme,
|
||||||
locale: LocaleType.EN_US,
|
locale: LocaleType.EN_US,
|
||||||
@@ -81,232 +143,427 @@ const TestSheetViewer: React.FC = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. 필수 플러그인 등록 (공식 문서 순서)
|
// 플러그인 등록 순서 (중요: Core → UI → Sheets → Docs → Formula → NumFmt)
|
||||||
|
console.log("🔌 플러그인 등록 중...");
|
||||||
univer.registerPlugin(UniverRenderEnginePlugin);
|
univer.registerPlugin(UniverRenderEnginePlugin);
|
||||||
univer.registerPlugin(UniverFormulaEnginePlugin);
|
univer.registerPlugin(UniverUIPlugin, { container });
|
||||||
|
|
||||||
univer.registerPlugin(UniverUIPlugin, {
|
|
||||||
container: containerRef.current!,
|
|
||||||
});
|
|
||||||
|
|
||||||
univer.registerPlugin(UniverDocsPlugin);
|
|
||||||
univer.registerPlugin(UniverDocsUIPlugin);
|
|
||||||
|
|
||||||
univer.registerPlugin(UniverSheetsPlugin);
|
univer.registerPlugin(UniverSheetsPlugin);
|
||||||
univer.registerPlugin(UniverSheetsUIPlugin);
|
univer.registerPlugin(UniverSheetsUIPlugin);
|
||||||
|
univer.registerPlugin(UniverDocsPlugin);
|
||||||
|
univer.registerPlugin(UniverDocsUIPlugin);
|
||||||
univer.registerPlugin(UniverSheetsFormulaPlugin);
|
univer.registerPlugin(UniverSheetsFormulaPlugin);
|
||||||
univer.registerPlugin(UniverSheetsFormulaUIPlugin);
|
univer.registerPlugin(UniverSheetsFormulaUIPlugin);
|
||||||
univer.registerPlugin(UniverSheetsNumfmtPlugin);
|
univer.registerPlugin(UniverSheetsNumfmtPlugin);
|
||||||
univer.registerPlugin(UniverSheetsNumfmtUIPlugin);
|
univer.registerPlugin(UniverSheetsNumfmtUIPlugin);
|
||||||
|
|
||||||
// 3. 기본 워크북 생성
|
// 전역 상태 업데이트
|
||||||
univer.createUnit(UniverInstanceType.UNIVER_SHEET, {
|
state.instance = univer;
|
||||||
id: "default-workbook",
|
state.lastContainerId = containerId;
|
||||||
name: "New Workbook",
|
window[GLOBAL_UNIVER_KEY] = univer;
|
||||||
sheetOrder: ["sheet1"],
|
|
||||||
sheets: {
|
|
||||||
sheet1: {
|
|
||||||
id: "sheet1",
|
|
||||||
name: "Sheet1",
|
|
||||||
cellData: {
|
|
||||||
0: {
|
|
||||||
0: { v: "Ready for Excel Import" },
|
|
||||||
1: { v: "파일을 업로드하세요" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rowCount: 100,
|
|
||||||
columnCount: 26,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
univerRef.current = univer;
|
console.log("✅ Univer 인스턴스 생성 완료");
|
||||||
setIsInitialized(true);
|
return univer;
|
||||||
|
|
||||||
console.log("✅ Univer CE 초기화 완료");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Univer CE 초기화 실패:", error);
|
console.error("❌ Univer 인스턴스 생성 실패:", error);
|
||||||
initializingRef.current = false;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
state.isInitializing = false;
|
||||||
|
state.initializationPromise = null;
|
||||||
}
|
}
|
||||||
};
|
})();
|
||||||
|
|
||||||
initializeUniver();
|
return state.initializationPromise;
|
||||||
}, []);
|
},
|
||||||
|
|
||||||
// 컴포넌트 언마운트 시 정리
|
// 전역 인스턴스 정리
|
||||||
useEffect(() => {
|
async disposeInstance(): Promise<void> {
|
||||||
return () => {
|
const state = getGlobalState();
|
||||||
try {
|
|
||||||
if (univerRef.current) {
|
|
||||||
univerRef.current.dispose();
|
|
||||||
univerRef.current = null;
|
|
||||||
}
|
|
||||||
initializingRef.current = false;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Univer dispose 오류:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 파일 처리 로직
|
if (state.isDisposing) {
|
||||||
const handleFileProcessing = useCallback(async (file: File) => {
|
console.log("🔄 이미 dispose 진행 중...");
|
||||||
if (!univerRef.current) return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsProcessing(true);
|
if (!state.instance) {
|
||||||
console.log("📁 파일 처리 시작:", file.name);
|
console.log("ℹ️ 정리할 인스턴스가 없음");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.isDisposing = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 파일을 ArrayBuffer로 읽기
|
console.log("🗑️ 전역 Univer 인스턴스 dispose 시작");
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
await state.instance.dispose();
|
||||||
const fileSize = (arrayBuffer.byteLength / 1024).toFixed(2);
|
state.instance = null;
|
||||||
|
state.lastContainerId = null;
|
||||||
|
window[GLOBAL_UNIVER_KEY] = null;
|
||||||
|
console.log("✅ 전역 Univer 인스턴스 dispose 완료");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ dispose 실패:", error);
|
||||||
|
} finally {
|
||||||
|
state.isDisposing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// 기존 워크북 제거 (메모리 관리)
|
// 현재 인스턴스 반환
|
||||||
|
getInstance(): Univer | null {
|
||||||
|
const state = getGlobalState();
|
||||||
|
return state.instance || window[GLOBAL_UNIVER_KEY] || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 상태 확인 메서드들
|
||||||
|
isInitializing(): boolean {
|
||||||
|
return getGlobalState().isInitializing || false;
|
||||||
|
},
|
||||||
|
|
||||||
|
isDisposing(): boolean {
|
||||||
|
return getGlobalState().isDisposing || false;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 강제 리셋 (디버깅용)
|
||||||
|
forceReset(): void {
|
||||||
|
const state = getGlobalState();
|
||||||
|
if (state.instance) {
|
||||||
try {
|
try {
|
||||||
// TODO: 실제 disposeUnit API 확인 후 구현
|
state.instance.dispose();
|
||||||
// univerRef.current.disposeUnit('default-workbook');
|
|
||||||
console.log("🗑️ 기존 워크북 정리 완료");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("⚠️ 기존 워크북 정리 실패:", error);
|
console.warn("강제 리셋 중 dispose 오류:", error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 새 워크북 생성 (실제 Excel 파싱은 추후 구현)
|
state.instance = null;
|
||||||
const newWorkbook = {
|
state.isInitializing = false;
|
||||||
|
state.isDisposing = false;
|
||||||
|
state.initializationPromise = null;
|
||||||
|
state.lastContainerId = null;
|
||||||
|
window[GLOBAL_UNIVER_KEY] = null;
|
||||||
|
|
||||||
|
console.log("🔄 전역 Univer 상태 강제 리셋 완료");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 전역 디버그 객체 설정
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
// 전역 상태 초기화
|
||||||
|
initializeGlobalState();
|
||||||
|
|
||||||
|
// 디버그 객체 설정
|
||||||
|
window.__UNIVER_DEBUG__ = {
|
||||||
|
getGlobalUniver: () => UniverseManager.getInstance(),
|
||||||
|
getGlobalState: () => getGlobalState(),
|
||||||
|
clearGlobalState: () => {
|
||||||
|
const state = getGlobalState();
|
||||||
|
Object.assign(state, {
|
||||||
|
instance: null,
|
||||||
|
isInitializing: false,
|
||||||
|
isDisposing: false,
|
||||||
|
initializationPromise: null,
|
||||||
|
lastContainerId: null,
|
||||||
|
});
|
||||||
|
window[GLOBAL_UNIVER_KEY] = null;
|
||||||
|
console.log("🧹 전역 상태 정리 완료");
|
||||||
|
},
|
||||||
|
forceReset: () => UniverseManager.forceReset(),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("🐛 디버그 객체 설정 완료: window.__UNIVER_DEBUG__");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Univer CE + 파일 업로드 오버레이
|
||||||
|
* Window 객체 기반 완전한 단일 인스턴스 관리
|
||||||
|
*/
|
||||||
|
const TestSheetViewer: React.FC = () => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const mountedRef = useRef<boolean>(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<File | null>(null);
|
||||||
|
|
||||||
|
// 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()}`,
|
id: `workbook-${Date.now()}`,
|
||||||
name: file.name,
|
locale: LocaleType.EN_US,
|
||||||
sheetOrder: ["imported-sheet"],
|
name: "Sample Workbook",
|
||||||
|
sheetOrder: ["sheet-01"],
|
||||||
sheets: {
|
sheets: {
|
||||||
"imported-sheet": {
|
"sheet-01": {
|
||||||
id: "imported-sheet",
|
type: 0,
|
||||||
name: "Imported Data",
|
id: "sheet-01",
|
||||||
cellData: {
|
name: "Sheet1",
|
||||||
0: {
|
tabColor: "",
|
||||||
0: { v: "파일명" },
|
hidden: 0,
|
||||||
1: { v: file.name },
|
|
||||||
},
|
|
||||||
1: {
|
|
||||||
0: { v: "파일 크기" },
|
|
||||||
1: { v: `${fileSize} KB` },
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: { v: "업로드 시간" },
|
|
||||||
1: { v: new Date().toLocaleString() },
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
0: { v: "상태" },
|
|
||||||
1: { v: "업로드 완료 - 실제 Excel 파싱은 추후 구현" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rowCount: 100,
|
rowCount: 100,
|
||||||
columnCount: 26,
|
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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
univerRef.current.createUnit(
|
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,
|
UniverInstanceType.UNIVER_SHEET,
|
||||||
newWorkbook,
|
workbookToUse,
|
||||||
);
|
);
|
||||||
|
|
||||||
setCurrentFile(file);
|
console.log("✅ 워크북 생성 완료:", workbook?.getUnitId());
|
||||||
setShowUploadOverlay(false); // 오버레이 숨기기
|
setIsInitialized(true);
|
||||||
|
|
||||||
console.log("✅ 파일 처리 완료");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ 파일 처리 실패:", error);
|
console.error("❌ Univer 초기화 실패:", error);
|
||||||
} finally {
|
setIsInitialized(false);
|
||||||
setIsProcessing(false);
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 파일 선택 처리
|
// 파일 처리 함수
|
||||||
const handleFileSelection = useCallback(
|
const processFile = useCallback(
|
||||||
async (files: FileList) => {
|
async (file: File) => {
|
||||||
if (files.length === 0) return;
|
// 강화된 상태 확인으로 중복 실행 완전 차단
|
||||||
|
if (
|
||||||
const file = files[0];
|
isProcessing ||
|
||||||
|
UniverseManager.isInitializing() ||
|
||||||
// 파일 타입 검증
|
UniverseManager.isDisposing()
|
||||||
if (!file.name.match(/\.(xlsx|xls)$/i)) {
|
) {
|
||||||
alert("Excel 파일(.xlsx, .xls)만 업로드 가능합니다.");
|
console.log("⏸️ 처리 중이거나 상태 변경 중입니다. 현재 상태:", {
|
||||||
|
isProcessing,
|
||||||
|
isInitializing: UniverseManager.isInitializing(),
|
||||||
|
isDisposing: UniverseManager.isDisposing(),
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 파일 크기 검증 (50MB)
|
if (!file.name.toLowerCase().endsWith(".xlsx")) {
|
||||||
if (file.size > 50 * 1024 * 1024) {
|
throw new Error("XLSX 파일만 지원됩니다.");
|
||||||
alert("파일 크기는 50MB를 초과할 수 없습니다.");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await handleFileProcessing(file);
|
setIsProcessing(true);
|
||||||
|
console.log("📁 파일 처리 시작:", file.name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// LuckyExcel을 사용하여 Excel 파일을 Univer 데이터로 변환
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
LuckyExcel.transformExcelToUniver(
|
||||||
|
file,
|
||||||
|
async (exportJson: any) => {
|
||||||
|
try {
|
||||||
|
console.log("📊 LuckyExcel 변환 완료:", exportJson);
|
||||||
|
|
||||||
|
// 변환된 데이터로 워크북 업데이트
|
||||||
|
if (exportJson && typeof exportJson === "object") {
|
||||||
|
await initializeUniver(exportJson);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"⚠️ 변환된 데이터가 유효하지 않음, 기본 워크북 사용",
|
||||||
|
);
|
||||||
|
await initializeUniver();
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentFile(file);
|
||||||
|
setShowUploadOverlay(false);
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 워크북 업데이트 오류:", error);
|
||||||
|
try {
|
||||||
|
await initializeUniver();
|
||||||
|
setCurrentFile(file);
|
||||||
|
setShowUploadOverlay(false);
|
||||||
|
resolve();
|
||||||
|
} catch (fallbackError) {
|
||||||
|
reject(fallbackError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error: any) => {
|
||||||
|
console.error("❌ LuckyExcel 변환 오류:", error);
|
||||||
|
reject(new Error("파일 변환 중 오류가 발생했습니다."));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 파일 처리 오류:", error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
console.log("✅ 파일 처리 완료");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[handleFileProcessing],
|
[isProcessing, initializeUniver],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 파일 입력 변경 처리
|
||||||
|
const handleFileInputChange = useCallback(
|
||||||
|
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await processFile(file);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 파일 처리 오류:", error);
|
||||||
|
alert(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "파일 처리 중 오류가 발생했습니다.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
// 파일 입력 초기화로 중복 실행 방지
|
||||||
|
if (event.target) {
|
||||||
|
event.target.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[processFile],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 파일 피커 클릭
|
||||||
|
const handleFilePickerClick = useCallback(() => {
|
||||||
|
if (isProcessing || UniverseManager.isInitializing()) return;
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
}, [isProcessing]);
|
||||||
|
|
||||||
|
// 새 업로드 처리
|
||||||
|
const handleNewUpload = useCallback(() => {
|
||||||
|
if (isProcessing || UniverseManager.isInitializing()) return;
|
||||||
|
setShowUploadOverlay(true);
|
||||||
|
setCurrentFile(null);
|
||||||
|
}, [isProcessing]);
|
||||||
|
|
||||||
// 드래그 앤 드롭 이벤트 핸들러
|
// 드래그 앤 드롭 이벤트 핸들러
|
||||||
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
setIsDragOver(true);
|
||||||
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
|
|
||||||
setIsDragOver(true);
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
|
||||||
setIsDragOver(false);
|
setIsDragOver(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
const handleDrop = useCallback(
|
||||||
async (e: React.DragEvent) => {
|
async (e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
|
||||||
setIsDragOver(false);
|
setIsDragOver(false);
|
||||||
|
|
||||||
if (isProcessing) return;
|
|
||||||
|
|
||||||
const files = e.dataTransfer.files;
|
const files = e.dataTransfer.files;
|
||||||
if (files && files.length > 0) {
|
if (files.length === 0) return;
|
||||||
await handleFileSelection(files);
|
|
||||||
|
const file = files[0];
|
||||||
|
|
||||||
|
if (!file.name.toLowerCase().endsWith(".xlsx")) {
|
||||||
|
alert("XLSX 파일만 업로드 가능합니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > 50 * 1024 * 1024) {
|
||||||
|
alert("파일 크기는 50MB를 초과할 수 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await processFile(file);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 파일 처리 오류:", error);
|
||||||
|
alert(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "파일 처리 중 오류가 발생했습니다.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[handleFileSelection, isProcessing],
|
[processFile],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 파일 선택 버튼 클릭
|
// 컴포넌트 마운트 시 초기화
|
||||||
const handleFilePickerClick = useCallback(() => {
|
useEffect(() => {
|
||||||
if (isProcessing || !fileInputRef.current) return;
|
mountedRef.current = true;
|
||||||
fileInputRef.current.click();
|
console.log("🎯 컴포넌트 마운트됨");
|
||||||
}, [isProcessing]);
|
|
||||||
|
|
||||||
// 파일 입력 변경
|
// 기존 전역 인스턴스 확인 및 재사용
|
||||||
const handleFileInputChange = useCallback(
|
const existingUniver = UniverseManager.getInstance();
|
||||||
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
if (existingUniver && !UniverseManager.isInitializing()) {
|
||||||
const files = e.target.files;
|
console.log("♻️ 기존 전역 Univer 인스턴스 재사용");
|
||||||
if (files && files.length > 0) {
|
setIsInitialized(true);
|
||||||
await handleFileSelection(files);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 컨테이너 준비 후 초기화
|
||||||
|
const initTimer = setTimeout(() => {
|
||||||
|
if (containerRef.current && !UniverseManager.isInitializing()) {
|
||||||
|
console.log("🚀 컴포넌트 마운트 시 Univer 초기화");
|
||||||
|
initializeUniver();
|
||||||
}
|
}
|
||||||
e.target.value = "";
|
}, 100); // 짧은 지연으로 DOM 완전 준비 보장
|
||||||
},
|
|
||||||
[handleFileSelection],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 새 파일 업로드 (오버레이 다시 표시)
|
return () => {
|
||||||
const handleNewUpload = useCallback(() => {
|
clearTimeout(initTimer);
|
||||||
setShowUploadOverlay(true);
|
mountedRef.current = false;
|
||||||
setCurrentFile(null);
|
console.log("👋 컴포넌트 언마운트됨");
|
||||||
|
};
|
||||||
|
}, [initializeUniver]);
|
||||||
|
|
||||||
|
// 컴포넌트 언마운트 시 정리 (전역 인스턴스는 유지)
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
// 전역 인스턴스는 앱 종료 시에만 정리
|
||||||
|
// 여기서는 로컬 상태만 초기화
|
||||||
|
setIsInitialized(false);
|
||||||
|
console.log("🧹 로컬 상태 정리 완료");
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-screen flex flex-col relative">
|
<div className="w-full h-screen flex flex-col relative">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="bg-white border-b p-4 flex-shrink-0 relative z-10">
|
<div className="bg-white border-b p-4 flex-shrink-0 relative z-10">
|
||||||
<h1 className="text-xl font-bold">🧪 Univer CE + 파일 업로드</h1>
|
<h1 className="text-xl font-bold">
|
||||||
|
🧪 Univer CE + 파일 업로드 (Window 기반 관리)
|
||||||
|
</h1>
|
||||||
<div className="mt-2 flex items-center gap-4">
|
<div className="mt-2 flex items-center gap-4">
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
||||||
@@ -331,6 +588,12 @@ const TestSheetViewer: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 디버그 정보 */}
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
전역 인스턴스: {UniverseManager.getInstance() ? "✅" : "❌"} |
|
||||||
|
초기화 중: {UniverseManager.isInitializing() ? "⏳" : "❌"}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -352,7 +615,7 @@ const TestSheetViewer: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: "00px", // 헤더 높이만큼 아래로 (헤더는 약 80px)
|
top: "0px", // 헤더 높이만큼 아래로 (헤더는 약 80px)
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
@@ -383,7 +646,13 @@ const TestSheetViewer: React.FC = () => {
|
|||||||
className="max-w-2xl w-full"
|
className="max-w-2xl w-full"
|
||||||
style={{ transform: "scale(0.8)" }}
|
style={{ transform: "scale(0.8)" }}
|
||||||
>
|
>
|
||||||
<div className="bg-white rounded-lg shadow-xl border p-8 md:p-12">
|
<div
|
||||||
|
className="bg-white rounded-lg shadow-xl border p-8 md:p-12"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
{/* 아이콘 및 제목 */}
|
{/* 아이콘 및 제목 */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
@@ -442,7 +711,7 @@ const TestSheetViewer: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span className="font-medium text-gray-900">
|
<span className="font-medium text-gray-900">
|
||||||
.xlsx, .xls
|
.xlsx
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
파일을 드래그 앤 드롭하거나 클릭하여 업로드
|
파일을 드래그 앤 드롭하거나 클릭하여 업로드
|
||||||
</>
|
</>
|
||||||
@@ -490,7 +759,7 @@ const TestSheetViewer: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
accept=".xlsx,.xls"
|
accept=".xlsx"
|
||||||
onChange={handleFileInputChange}
|
onChange={handleFileInputChange}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
@@ -498,8 +767,12 @@ const TestSheetViewer: React.FC = () => {
|
|||||||
|
|
||||||
{/* 지원 형식 안내 */}
|
{/* 지원 형식 안내 */}
|
||||||
<div className="mt-6 text-xs text-gray-500">
|
<div className="mt-6 text-xs text-gray-500">
|
||||||
<p>지원 형식: Excel (.xlsx, .xls)</p>
|
<p>지원 형식: Excel (.xlsx)</p>
|
||||||
<p>최대 파일 크기: 50MB</p>
|
<p>최대 파일 크기: 50MB</p>
|
||||||
|
<p className="mt-2 text-blue-600">
|
||||||
|
💡 브라우저 콘솔에서 window.__UNIVER_DEBUG__ 로 디버깅
|
||||||
|
가능
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1290
src/utils/fileProcessor.ts
Normal file
1290
src/utils/fileProcessor.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -16,13 +16,18 @@ export default defineConfig({
|
|||||||
stream: "stream-browserify",
|
stream: "stream-browserify",
|
||||||
buffer: "buffer",
|
buffer: "buffer",
|
||||||
},
|
},
|
||||||
|
// 중복 모듈 해결을 위한 dedupe 설정
|
||||||
|
dedupe: ["@wendellhu/redi"],
|
||||||
},
|
},
|
||||||
|
|
||||||
// 의존성 최적화 설정
|
// 의존성 최적화 설정
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
exclude: [
|
include: [
|
||||||
// 중복 로딩 방지를 위해 redi와 univer 관련 제외
|
// REDI 중복 로드 방지를 위해 명시적으로 포함
|
||||||
"@wendellhu/redi",
|
"@wendellhu/redi",
|
||||||
|
],
|
||||||
|
exclude: [
|
||||||
|
// Univer 관련 모듈만 제외
|
||||||
"@univerjs/core",
|
"@univerjs/core",
|
||||||
"@univerjs/design",
|
"@univerjs/design",
|
||||||
"@univerjs/ui",
|
"@univerjs/ui",
|
||||||
@@ -46,6 +51,8 @@ export default defineConfig({
|
|||||||
external: [],
|
external: [],
|
||||||
output: {
|
output: {
|
||||||
manualChunks: {
|
manualChunks: {
|
||||||
|
// REDI를 별도 청크로 분리하여 중복 방지
|
||||||
|
redi: ["@wendellhu/redi"],
|
||||||
// Univer 관련 라이브러리를 별도 청크로 분리
|
// Univer 관련 라이브러리를 별도 청크로 분리
|
||||||
"univer-core": [
|
"univer-core": [
|
||||||
"@univerjs/core",
|
"@univerjs/core",
|
||||||
|
|||||||
Reference in New Issue
Block a user