From 105265a384b732f5970a3e89108184f17d465513 Mon Sep 17 00:00:00 2001 From: sheetEasy AI Team Date: Tue, 24 Jun 2025 16:38:17 +0900 Subject: [PATCH] =?UTF-8?q?xlsx=20=ED=8C=8C=EC=9D=BC=20=EC=A3=BC=EC=9E=85?= =?UTF-8?q?=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/rules/univer-initialization.mdc | 326 ++++++ package-lock.json | 915 ++++++++++++++- package.json | 1 + src/components/sheet/TestSheetViewer.tsx | 643 +++++++---- src/utils/fileProcessor.ts | 1290 ++++++++++++++++++++++ src/utils/luckysheetApi.ts | 1 - vite.config.ts | 11 +- 7 files changed, 2991 insertions(+), 196 deletions(-) create mode 100644 .cursor/rules/univer-initialization.mdc create mode 100644 src/utils/fileProcessor.ts delete mode 100644 src/utils/luckysheetApi.ts diff --git a/.cursor/rules/univer-initialization.mdc b/.cursor/rules/univer-initialization.mdc new file mode 100644 index 0000000..2e106ba --- /dev/null +++ b/.cursor/rules/univer-initialization.mdc @@ -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 { + 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(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 아키텍처 특성상 필수적임 +- 전역 인스턴스 관리로 브라우저 재로드 없이 안정적 운용 가능 +- 개발 환경에서 오류 발생 시 반드시 캐시 삭제 후 재시작 diff --git a/package-lock.json b/package-lock.json index 3d74f73..3cd6841 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@univerjs/sheets-ui": "^0.8.2", "@univerjs/ui": "^0.8.2", "@univerjs/uniscript": "^0.8.2", + "@zwight/luckyexcel": "^1.1.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "file-saver": "^2.0.5", @@ -994,6 +995,47 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, "node_modules/@flatten-js/interval-tree": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@flatten-js/interval-tree/-/interval-tree-1.1.3.tgz", @@ -1548,6 +1590,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@progress/jszip-esm": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@progress/jszip-esm/-/jszip-esm-1.0.4.tgz", + "integrity": "sha512-A5i26JcTosFKeHCrklarNsByW3RUJd8osRq69eskZgIaq05weTCXdpztlFMwrHpgOGods1D0WFoSQcMNE0eI8Q==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@progress/pako-esm": "^1.0.1" + } + }, + "node_modules/@progress/pako-esm": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@progress/pako-esm/-/pako-esm-1.0.1.tgz", + "integrity": "sha512-O4A3b1EuE9Xe1pC3Xz9Tcn1M/CYrL71f4y/5TXeytOVTkmkzBgYW97fYP2f+54H0e0erWRaqV/kUUB/a8Uxfbw==", + "license": "SEE LICENSE IN LICENSE.md" + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -6665,6 +6722,138 @@ "integrity": "sha512-0o57fGpzid62p4UsXv/vAWnkKW+vqkUjsjFDkFt68yZrIVCFPmcR2761YIfrzUqmUjkrySURd8Qu1CQ2NPDkBw==", "license": "MIT" }, + "node_modules/@zwight/exceljs": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@zwight/exceljs/-/exceljs-4.4.2.tgz", + "integrity": "sha512-WA+u2zRE9Mf1rt3UiQesERv9bgv6DSjLjL9AEMPZgBgTRDOjS09Vi1MVzzhuehsaSaKaScgdK+MSInfOgoxxpg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@zwight/exceljs/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@zwight/exceljs/node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@zwight/luckyexcel": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@zwight/luckyexcel/-/luckyexcel-1.1.6.tgz", + "integrity": "sha512-SPU16JL9WkfIqLJto7PTBjC2mzmswnibnhAJKU5Zih1ioLSfHQnGSNUA57g29v0kGD1RBL7uh1gHNw1tyiz+Dw==", + "license": "MIT", + "dependencies": { + "@progress/jszip-esm": "^1.0.3", + "@univerjs/core": "^0.6.0", + "@zwight/exceljs": "4.4.2", + "dayjs": "^1.10.6", + "nanoid": "^3.3.7", + "papaparse": "^5.5.2", + "xml2js": "^0.6.2" + } + }, + "node_modules/@zwight/luckyexcel/node_modules/@univerjs/core": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/@univerjs/core/-/core-0.6.10.tgz", + "integrity": "sha512-vgSlJbqYncKou3mA79nyIec8HNztuYp8r6Q7+7wE7s4A18xZzcSk882IQQJ445tG0ZfzLbXSRXf/RvE5Gu6/Vg==", + "license": "Apache-2.0", + "dependencies": { + "@univerjs/protocol": "0.1.45", + "@wendellhu/redi": "0.17.1", + "async-lock": "^1.4.1", + "dayjs": "^1.11.13", + "fast-diff": "1.3.0", + "kdbush": "^4.0.2", + "lodash-es": "^4.17.21", + "nanoid": "5.1.5", + "numeral": "^2.0.6", + "numfmt": "^2.5.2", + "ot-json1": "^1.0.2", + "rbush": "^4.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/univer" + }, + "peerDependencies": { + "@wendellhu/redi": "0.17.1", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@zwight/luckyexcel/node_modules/@univerjs/core/node_modules/nanoid": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", + "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@zwight/luckyexcel/node_modules/@univerjs/protocol": { + "version": "0.1.45", + "resolved": "https://registry.npmjs.org/@univerjs/protocol/-/protocol-0.1.45.tgz", + "integrity": "sha512-pdQR6Im6gbmge7UgbyQCSMuX3mn3fCNafk4MmCkn5NbEluOURVxhshn6jfEHLraUyw+vz/ghwBiouVHR+n1C2g==", + "engines": { + "node": ">=18.0.0", + "pnpm": ">=10.0.0" + }, + "peerDependencies": { + "@grpc/grpc-js": ">=1", + "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", + "integrity": "sha512-VXrB2bpU9Xa0oCHq8IsqE2CcUx5OLupLC3oryFT4DB9e/xe+OnUzBndhXfNHUzxFE4DYI3Sx4OtzS1Sdaf7tEw==", + "license": "MIT" + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -6799,6 +6988,80 @@ "node": ">= 8" } }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -6845,6 +7108,12 @@ "node": ">=12" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/async-lock": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", @@ -6900,14 +7169,12 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -6924,6 +7191,28 @@ ], "license": "MIT" }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -6937,11 +7226,65 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -7019,6 +7362,32 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -7101,6 +7470,18 @@ "node": ">=12" } }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -7350,11 +7731,39 @@ "node": ">= 0.8" } }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/convert-source-map": { @@ -7370,6 +7779,45 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7596,6 +8044,15 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -7617,6 +8074,15 @@ "dev": true, "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -8027,6 +8493,19 @@ "node": ">=12.0.0" } }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -8244,6 +8723,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -8259,6 +8750,34 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -8547,7 +9066,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -8617,6 +9135,17 @@ "node": ">=8" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -9334,6 +9863,18 @@ "json-buffer": "3.0.1" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -9605,6 +10146,12 @@ "dev": true, "license": "MIT" }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, "node_modules/localforage": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", @@ -9659,6 +10206,73 @@ "license": "MIT", "peer": true }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9666,6 +10280,18 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -9824,7 +10450,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -9833,6 +10458,15 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -9950,7 +10584,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10007,6 +10640,15 @@ "node": ">= 6" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/opentype.js": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-1.3.4.tgz", @@ -10104,6 +10746,12 @@ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, + "node_modules/papaparse": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -10140,6 +10788,15 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -11056,6 +11713,36 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -11157,6 +11844,40 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.44.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz", @@ -11244,6 +11965,12 @@ "dev": true, "license": "MIT" }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -11758,6 +12485,36 @@ "node": ">=18" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -11885,6 +12642,15 @@ "node": ">=14.0.0" } }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -11937,6 +12703,15 @@ "node": ">=12" } }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/trigram-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/trigram-utils/-/trigram-utils-2.0.1.tgz", @@ -12048,6 +12823,24 @@ "node": ">= 4.0.0" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -12149,6 +12942,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vite": { "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", @@ -12581,6 +13383,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.18.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", @@ -12613,11 +13421,32 @@ "node": ">=12" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, "license": "MIT" }, "node_modules/y18n": { @@ -12737,6 +13566,76 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/zustand": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.5.tgz", diff --git a/package.json b/package.json index 76d76e1..12c476c 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@univerjs/sheets-ui": "^0.8.2", "@univerjs/ui": "^0.8.2", "@univerjs/uniscript": "^0.8.2", + "@zwight/luckyexcel": "^1.1.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "file-saver": "^2.0.5", diff --git a/src/components/sheet/TestSheetViewer.tsx b/src/components/sheet/TestSheetViewer.tsx index 16ddf42..21a0df1 100644 --- a/src/components/sheet/TestSheetViewer.tsx +++ b/src/components/sheet/TestSheetViewer.tsx @@ -13,6 +13,7 @@ import { UniverSheetsNumfmtPlugin } from "@univerjs/sheets-numfmt"; import { UniverSheetsNumfmtUIPlugin } from "@univerjs/sheets-numfmt-ui"; import { UniverUIPlugin } from "@univerjs/ui"; import { cn } from "../../lib/utils"; +import LuckyExcel from "@zwight/luckyexcel"; // 언어팩 import 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-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 | 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 + 파일 업로드 오버레이 - * - Univer CE는 항상 렌더링 (기본 레이어) - * - 오버레이로 파일 업로드 UI 표시 - * - disposeUnit()으로 메모리 관리 + * Window 객체 기반 강화된 전역 Univer 관리자 + * 모듈 재로드와 HMR에도 안전하게 작동 */ -const TestSheetViewer: React.FC = () => { - const containerRef = useRef(null); - const univerRef = useRef(null); - const initializingRef = useRef(false); - const fileInputRef = useRef(null); +const UniverseManager = { + // 전역 인스턴스 생성 (완전 단일 인스턴스 보장) + async createInstance(container: HTMLElement): Promise { + const state = getGlobalState(); + const containerId = container.id || `container-${Date.now()}`; - 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); + console.log(`🚀 Univer 인스턴스 생성 요청 - Container: ${containerId}`); - // Univer 초기화 - 공식 문서 패턴 따라서 - useEffect(() => { - if ( - !containerRef.current || - isInitialized || - univerRef.current || - initializingRef.current - ) - return; + // 이미 존재하는 인스턴스가 있고 같은 컨테이너면 재사용 + if (state.instance && state.lastContainerId === containerId) { + console.log("✅ 기존 전역 Univer 인스턴스 재사용"); + return state.instance; + } - const initializeUniver = async () => { + // 초기화가 진행 중이면 대기 + if (state.isInitializing && state.initializationPromise) { + console.log("⏳ 기존 초기화 프로세스 대기 중..."); + return state.initializationPromise; + } + + // 새로운 초기화 시작 + state.isInitializing = true; + console.log("🔄 새로운 Univer 인스턴스 생성 시작"); + + // 기존 인스턴스 정리 + if (state.instance) { try { - initializingRef.current = true; - console.log("🚀 Univer CE 초기화 시작"); + console.log("🗑️ 기존 인스턴스 정리 중..."); + 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({ theme: defaultTheme, 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(UniverFormulaEnginePlugin); - - univer.registerPlugin(UniverUIPlugin, { - container: containerRef.current!, - }); - - univer.registerPlugin(UniverDocsPlugin); - univer.registerPlugin(UniverDocsUIPlugin); - + univer.registerPlugin(UniverUIPlugin, { container }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); + univer.registerPlugin(UniverDocsPlugin); + univer.registerPlugin(UniverDocsUIPlugin); univer.registerPlugin(UniverSheetsFormulaPlugin); univer.registerPlugin(UniverSheetsFormulaUIPlugin); univer.registerPlugin(UniverSheetsNumfmtPlugin); univer.registerPlugin(UniverSheetsNumfmtUIPlugin); - // 3. 기본 워크북 생성 - univer.createUnit(UniverInstanceType.UNIVER_SHEET, { - id: "default-workbook", - name: "New Workbook", - sheetOrder: ["sheet1"], - sheets: { - sheet1: { - id: "sheet1", - name: "Sheet1", - cellData: { - 0: { - 0: { v: "Ready for Excel Import" }, - 1: { v: "파일을 업로드하세요" }, - }, - }, - rowCount: 100, - columnCount: 26, - }, - }, - }); + // 전역 상태 업데이트 + state.instance = univer; + state.lastContainerId = containerId; + window[GLOBAL_UNIVER_KEY] = univer; - univerRef.current = univer; - setIsInitialized(true); - - console.log("✅ Univer CE 초기화 완료"); + console.log("✅ Univer 인스턴스 생성 완료"); + return univer; } catch (error) { - console.error("❌ Univer CE 초기화 실패:", error); - initializingRef.current = false; + console.error("❌ Univer 인스턴스 생성 실패:", error); + throw error; + } finally { + state.isInitializing = false; + state.initializationPromise = null; } - }; + })(); - initializeUniver(); - }, []); + return state.initializationPromise; + }, - // 컴포넌트 언마운트 시 정리 - useEffect(() => { - return () => { - try { - if (univerRef.current) { - univerRef.current.dispose(); - univerRef.current = null; - } - initializingRef.current = false; - } catch (error) { - console.error("❌ Univer dispose 오류:", error); - } - }; - }, []); + // 전역 인스턴스 정리 + async disposeInstance(): Promise { + const state = getGlobalState(); - // 파일 처리 로직 - const handleFileProcessing = useCallback(async (file: File) => { - if (!univerRef.current) return; + if (state.isDisposing) { + console.log("🔄 이미 dispose 진행 중..."); + return; + } - setIsProcessing(true); - console.log("📁 파일 처리 시작:", file.name); + if (!state.instance) { + console.log("ℹ️ 정리할 인스턴스가 없음"); + return; + } + + state.isDisposing = true; try { - // 파일을 ArrayBuffer로 읽기 - const arrayBuffer = await file.arrayBuffer(); - const fileSize = (arrayBuffer.byteLength / 1024).toFixed(2); + console.log("🗑️ 전역 Univer 인스턴스 dispose 시작"); + await state.instance.dispose(); + 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 { - // TODO: 실제 disposeUnit API 확인 후 구현 - // univerRef.current.disposeUnit('default-workbook'); - console.log("🗑️ 기존 워크북 정리 완료"); + state.instance.dispose(); } catch (error) { - console.warn("⚠️ 기존 워크북 정리 실패:", error); + console.warn("강제 리셋 중 dispose 오류:", error); } + } - // 새 워크북 생성 (실제 Excel 파싱은 추후 구현) - const newWorkbook = { + state.instance = null; + 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(null); + const fileInputRef = useRef(null); + const mountedRef = useRef(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); + + // 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()}`, - name: file.name, - sheetOrder: ["imported-sheet"], + locale: LocaleType.EN_US, + name: "Sample Workbook", + sheetOrder: ["sheet-01"], sheets: { - "imported-sheet": { - id: "imported-sheet", - name: "Imported Data", - cellData: { - 0: { - 0: { v: "파일명" }, - 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 파싱은 추후 구현" }, - }, - }, + "sheet-01": { + type: 0, + id: "sheet-01", + name: "Sheet1", + tabColor: "", + hidden: 0, 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, - newWorkbook, + workbookToUse, ); - setCurrentFile(file); - setShowUploadOverlay(false); // 오버레이 숨기기 - - console.log("✅ 파일 처리 완료"); + console.log("✅ 워크북 생성 완료:", workbook?.getUnitId()); + setIsInitialized(true); } catch (error) { - console.error("❌ 파일 처리 실패:", error); - } finally { - setIsProcessing(false); + console.error("❌ Univer 초기화 실패:", error); + setIsInitialized(false); } }, []); - // 파일 선택 처리 - const handleFileSelection = useCallback( - async (files: FileList) => { - if (files.length === 0) return; - - const file = files[0]; - - // 파일 타입 검증 - if (!file.name.match(/\.(xlsx|xls)$/i)) { - alert("Excel 파일(.xlsx, .xls)만 업로드 가능합니다."); + // 파일 처리 함수 + const processFile = useCallback( + async (file: File) => { + // 강화된 상태 확인으로 중복 실행 완전 차단 + if ( + isProcessing || + UniverseManager.isInitializing() || + UniverseManager.isDisposing() + ) { + console.log("⏸️ 처리 중이거나 상태 변경 중입니다. 현재 상태:", { + isProcessing, + isInitializing: UniverseManager.isInitializing(), + isDisposing: UniverseManager.isDisposing(), + }); return; } - // 파일 크기 검증 (50MB) - if (file.size > 50 * 1024 * 1024) { - alert("파일 크기는 50MB를 초과할 수 없습니다."); - return; + if (!file.name.toLowerCase().endsWith(".xlsx")) { + throw new Error("XLSX 파일만 지원됩니다."); } - await handleFileProcessing(file); + setIsProcessing(true); + console.log("📁 파일 처리 시작:", file.name); + + try { + // LuckyExcel을 사용하여 Excel 파일을 Univer 데이터로 변환 + await new Promise((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) => { + 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) => { e.preventDefault(); - e.stopPropagation(); - if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { - setIsDragOver(true); - } + setIsDragOver(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); - e.stopPropagation(); setIsDragOver(false); }, []); - const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - }, []); - const handleDrop = useCallback( async (e: React.DragEvent) => { e.preventDefault(); - e.stopPropagation(); setIsDragOver(false); - if (isProcessing) return; - const files = e.dataTransfer.files; - if (files && files.length > 0) { - await handleFileSelection(files); + if (files.length === 0) return; + + 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(() => { - if (isProcessing || !fileInputRef.current) return; - fileInputRef.current.click(); - }, [isProcessing]); + // 컴포넌트 마운트 시 초기화 + useEffect(() => { + mountedRef.current = true; + console.log("🎯 컴포넌트 마운트됨"); - // 파일 입력 변경 - const handleFileInputChange = useCallback( - async (e: React.ChangeEvent) => { - const files = e.target.files; - if (files && files.length > 0) { - await handleFileSelection(files); + // 기존 전역 인스턴스 확인 및 재사용 + const existingUniver = UniverseManager.getInstance(); + if (existingUniver && !UniverseManager.isInitializing()) { + console.log("♻️ 기존 전역 Univer 인스턴스 재사용"); + setIsInitialized(true); + return; + } + + // 컨테이너 준비 후 초기화 + const initTimer = setTimeout(() => { + if (containerRef.current && !UniverseManager.isInitializing()) { + console.log("🚀 컴포넌트 마운트 시 Univer 초기화"); + initializeUniver(); } - e.target.value = ""; - }, - [handleFileSelection], - ); + }, 100); // 짧은 지연으로 DOM 완전 준비 보장 - // 새 파일 업로드 (오버레이 다시 표시) - const handleNewUpload = useCallback(() => { - setShowUploadOverlay(true); - setCurrentFile(null); + return () => { + clearTimeout(initTimer); + mountedRef.current = false; + console.log("👋 컴포넌트 언마운트됨"); + }; + }, [initializeUniver]); + + // 컴포넌트 언마운트 시 정리 (전역 인스턴스는 유지) + useEffect(() => { + return () => { + // 전역 인스턴스는 앱 종료 시에만 정리 + // 여기서는 로컬 상태만 초기화 + setIsInitialized(false); + console.log("🧹 로컬 상태 정리 완료"); + }; }, []); return (
{/* 헤더 */}
-

🧪 Univer CE + 파일 업로드

+

+ 🧪 Univer CE + 파일 업로드 (Window 기반 관리) +

{ )} + + {/* 디버그 정보 */} +
+ 전역 인스턴스: {UniverseManager.getInstance() ? "✅" : "❌"} | + 초기화 중: {UniverseManager.isInitializing() ? "⏳" : "❌"} +
@@ -352,7 +615,7 @@ const TestSheetViewer: React.FC = () => {
{ className="max-w-2xl w-full" style={{ transform: "scale(0.8)" }} > -
+
{/* 아이콘 및 제목 */}
@@ -442,7 +711,7 @@ const TestSheetViewer: React.FC = () => { ) : ( <> - .xlsx, .xls + .xlsx {" "} 파일을 드래그 앤 드롭하거나 클릭하여 업로드 @@ -490,7 +759,7 @@ const TestSheetViewer: React.FC = () => { { {/* 지원 형식 안내 */}
-

지원 형식: Excel (.xlsx, .xls)

+

지원 형식: Excel (.xlsx)

최대 파일 크기: 50MB

+

+ 💡 브라우저 콘솔에서 window.__UNIVER_DEBUG__ 로 디버깅 + 가능 +

diff --git a/src/utils/fileProcessor.ts b/src/utils/fileProcessor.ts new file mode 100644 index 0000000..87578d3 --- /dev/null +++ b/src/utils/fileProcessor.ts @@ -0,0 +1,1290 @@ +// import * as XLSX from "xlsx-js-style"; +import type { SheetData, FileUploadResult } from "../types/sheet"; +import { analyzeSheetStyles } from "./styleTest"; + +/** + * 파일 처리 관련 유틸리티 - xlsx-js-style 공식 API 활용 버전 + * - 모든 파일 형식을 SheetJS를 통해 읽은 후 XLSX로 변환 + * - 변환된 XLSX 파일을 LuckyExcel로 전달 + * - xlsx-js-style의 공식 스타일 구조를 그대로 활용 + */ + +// 지원되는 파일 타입 +export const SUPPORTED_FILE_TYPES = { + XLSX: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + XLS: "application/vnd.ms-excel", + CSV: "text/csv", +} as const; + +export const SUPPORTED_EXTENSIONS = [".xlsx", ".xls", ".csv"] as const; + +// 최대 파일 크기 (50MB) +export const MAX_FILE_SIZE = 50 * 1024 * 1024; + +/** + * xlsx-js-style 색상 객체를 Luckysheet 색상 문자열로 변환 + * 공식 xlsx-js-style COLOR_STYLE 형식을 지원: rgb, theme, indexed + */ +function convertXlsxColorToLuckysheet(colorObj: any): string { + if (!colorObj) return ""; + + // RGB 형태 - 공식 문서: {rgb: "FFCC00"} + if (colorObj.rgb) { + const rgb = colorObj.rgb.toUpperCase(); + // ARGB 형태 (8자리) - 앞의 2자리(Alpha) 제거 + if (rgb.length === 8) { + const r = parseInt(rgb.substring(2, 4), 16); + const g = parseInt(rgb.substring(4, 6), 16); + const b = parseInt(rgb.substring(6, 8), 16); + return `rgb(${r},${g},${b})`; + } + // RGB 형태 (6자리) + else if (rgb.length === 6) { + const r = parseInt(rgb.substring(0, 2), 16); + const g = parseInt(rgb.substring(2, 4), 16); + const b = parseInt(rgb.substring(4, 6), 16); + return `rgb(${r},${g},${b})`; + } + } + + // Theme 색상 - 공식 문서: {theme: 4} 또는 {theme: 1, tint: 0.4} + if (typeof colorObj.theme === "number") { + // Excel 테마 색상 매핑 (공식 문서 예시 기반) + const themeColors: { [key: number]: string } = { + 0: "rgb(255,255,255)", // 배경 1 (흰색) + 1: "rgb(0,0,0)", // 텍스트 1 (검정) + 2: "rgb(238,236,225)", // 배경 2 (연회색) + 3: "rgb(31,73,125)", // 텍스트 2 (어두운 파랑) + 4: "rgb(79,129,189)", // 강조 1 (파랑) - 공식 문서 예시 + 5: "rgb(192,80,77)", // 강조 2 (빨강) + 6: "rgb(155,187,89)", // 강조 3 (초록) + 7: "rgb(128,100,162)", // 강조 4 (보라) + 8: "rgb(75,172,198)", // 강조 5 (하늘색) + 9: "rgb(247,150,70)", // 강조 6 (주황) + }; + + let baseColor = themeColors[colorObj.theme] || "rgb(0,0,0)"; + + // Tint 적용 - 공식 문서: {theme: 1, tint: 0.4} ("Blue, Accent 1, Lighter 40%") + if (typeof colorObj.tint === "number") { + baseColor = applyTintToRgbColor(baseColor, colorObj.tint); + } + + return baseColor; + } + + // Indexed 색상 (Excel 기본 색상표) + if (typeof colorObj.indexed === "number") { + const indexedColors: { [key: number]: string } = { + 0: "rgb(0,0,0)", // 검정 + 1: "rgb(255,255,255)", // 흰색 + 2: "rgb(255,0,0)", // 빨강 + 3: "rgb(0,255,0)", // 초록 + 4: "rgb(0,0,255)", // 파랑 + 5: "rgb(255,255,0)", // 노랑 + 6: "rgb(255,0,255)", // 마젠타 + 7: "rgb(0,255,255)", // 시안 + 8: "rgb(128,0,0)", // 어두운 빨강 + 9: "rgb(0,128,0)", // 어두운 초록 + 10: "rgb(0,0,128)", // 어두운 파랑 + 17: "rgb(128,128,128)", // 회색 + }; + + return indexedColors[colorObj.indexed] || "rgb(0,0,0)"; + } + + return ""; +} + +/** + * RGB 색상에 Excel tint 적용 + */ +function applyTintToRgbColor(rgbColor: string, tint: number): string { + const match = rgbColor.match(/rgb\((\d+),(\d+),(\d+)\)/); + if (!match) return rgbColor; + + const r = parseInt(match[1]); + const g = parseInt(match[2]); + const b = parseInt(match[3]); + + // Excel tint 공식 적용 + const applyTint = (color: number, tint: number): number => { + if (tint < 0) { + return Math.round(color * (1 + tint)); + } else { + return Math.round(color * (1 - tint) + (255 - 255 * (1 - tint))); + } + }; + + const newR = Math.max(0, Math.min(255, applyTint(r, tint))); + const newG = Math.max(0, Math.min(255, applyTint(g, tint))); + const newB = Math.max(0, Math.min(255, applyTint(b, tint))); + + return `rgb(${newR},${newG},${newB})`; +} + +/** + * xlsx-js-style 테두리 스타일을 Luckysheet 번호로 변환 + * 공식 문서의 BORDER_STYLE 값들을 지원 + */ +function convertBorderStyleToLuckysheet(borderStyle: string): number { + const styleMap: { [key: string]: number } = { + thin: 1, + medium: 2, + thick: 3, + dotted: 4, + dashed: 5, + dashDot: 6, + dashDotDot: 7, + double: 8, + hair: 1, + mediumDashed: 5, + mediumDashDot: 6, + mediumDashDotDot: 7, + slantDashDot: 6, + }; + + return styleMap[borderStyle] || 1; +} + +/** + * xlsx-js-style 스타일 객체를 Luckysheet 스타일로 변환 + * 공식 xlsx-js-style API 구조를 완전히 활용 + */ +function convertXlsxStyleToLuckysheet(xlsxStyle: any): any { + if (!xlsxStyle) return {}; + + const luckyStyle: any = {}; + + // 🎨 폰트 스타일 변환 - 공식 문서 font 속성 + if (xlsxStyle.font) { + const font = xlsxStyle.font; + + // 폰트명 - 공식 문서: {name: "Courier"} + if (font.name) { + luckyStyle.ff = font.name; + } + + // 폰트 크기 - 공식 문서: {sz: 24} + if (font.sz) { + luckyStyle.fs = font.sz; + } + + // 굵게 - 공식 문서: {bold: true} + if (font.bold) { + luckyStyle.bl = 1; + } + + // 기울임 - 공식 문서: {italic: true} + if (font.italic) { + luckyStyle.it = 1; + } + + // 밑줄 - 공식 문서: {underline: true} + if (font.underline) { + luckyStyle.un = 1; + } + + // 취소선 - 공식 문서: {strike: true} + if (font.strike) { + luckyStyle.st = 1; + } + + // 폰트 색상 - 공식 문서: {color: {rgb: "FF0000"}} + if (font.color) { + const fontColor = convertXlsxColorToLuckysheet(font.color); + if (fontColor) { + luckyStyle.fc = fontColor; + } + } + } + + // 🎨 배경 스타일 변환 - 공식 문서 fill 속성 + if (xlsxStyle.fill) { + const fill = xlsxStyle.fill; + + // 배경색 - 공식 문서: {fgColor: {rgb: "E9E9E9"}} + if (fill.fgColor) { + const bgColor = convertXlsxColorToLuckysheet(fill.fgColor); + if (bgColor) { + luckyStyle.bg = bgColor; + } + } + // bgColor도 확인 (패턴 배경의 경우) + else if (fill.bgColor) { + const bgColor = convertXlsxColorToLuckysheet(fill.bgColor); + if (bgColor) { + luckyStyle.bg = bgColor; + } + } + } + + // 🎨 정렬 스타일 변환 - 공식 문서 alignment 속성 + if (xlsxStyle.alignment) { + const alignment = xlsxStyle.alignment; + + // 수평 정렬 - 공식 문서: {horizontal: "center"} + if (alignment.horizontal) { + luckyStyle.ht = + alignment.horizontal === "left" + ? 1 + : alignment.horizontal === "center" + ? 2 + : alignment.horizontal === "right" + ? 3 + : 1; + } + + // 수직 정렬 - 공식 문서: {vertical: "center"} + if (alignment.vertical) { + luckyStyle.vt = + alignment.vertical === "top" + ? 1 + : alignment.vertical === "center" + ? 2 + : alignment.vertical === "bottom" + ? 3 + : 2; + } + + // 텍스트 줄바꿈 - 공식 문서: {wrapText: true} + if (alignment.wrapText) { + luckyStyle.tb = 1; + } + + // 텍스트 회전 - 공식 문서: {textRotation: 90} + if (alignment.textRotation) { + luckyStyle.tr = alignment.textRotation; + } + } + + // 🎨 테두리 스타일 변환 - 공식 문서 border 속성 + if (xlsxStyle.border) { + const border = xlsxStyle.border; + luckyStyle.bd = {}; + + // 상단 테두리 - 공식 문서: {top: {style: "thin", color: {rgb: "000000"}}} + if (border.top) { + luckyStyle.bd.t = { + style: convertBorderStyleToLuckysheet(border.top.style || "thin"), + color: convertXlsxColorToLuckysheet(border.top.color) || "rgb(0,0,0)", + }; + } + + // 하단 테두리 + if (border.bottom) { + luckyStyle.bd.b = { + style: convertBorderStyleToLuckysheet(border.bottom.style || "thin"), + color: + convertXlsxColorToLuckysheet(border.bottom.color) || "rgb(0,0,0)", + }; + } + + // 좌측 테두리 + if (border.left) { + luckyStyle.bd.l = { + style: convertBorderStyleToLuckysheet(border.left.style || "thin"), + color: convertXlsxColorToLuckysheet(border.left.color) || "rgb(0,0,0)", + }; + } + + // 우측 테두리 + if (border.right) { + luckyStyle.bd.r = { + style: convertBorderStyleToLuckysheet(border.right.style || "thin"), + color: convertXlsxColorToLuckysheet(border.right.color) || "rgb(0,0,0)", + }; + } + } + + // 🎨 숫자 포맷 변환 - 공식 문서 numFmt 속성 + if (xlsxStyle.numFmt) { + // numFmt는 문자열 또는 숫자일 수 있음 + luckyStyle.ct = { + fa: xlsxStyle.numFmt, + t: "n", // 숫자 타입 + }; + } + + return luckyStyle; +} + +/** + * 파일 타입 검증 + */ +export function validateFileType(file: File): boolean { + const fileName = file.name.toLowerCase(); + const extension = fileName.split(".").pop(); + const supportedExtensions = SUPPORTED_EXTENSIONS.map((ext) => ext.slice(1)); + + if (!extension) { + return false; + } + + return supportedExtensions.includes(extension); +} + +/** + * 파일 크기 검증 + */ +export function validateFileSize(file: File): boolean { + return file.size <= MAX_FILE_SIZE; +} + +/** + * 파일 이름에서 확장자 제거 + */ +export function getFileNameWithoutExtension(fileName: string): string { + return fileName.replace(/\.[^/.]+$/, ""); +} + +/** + * 에러 메시지 생성 + */ +export function getFileErrorMessage(file: File): string { + if (!validateFileType(file)) { + const fileName = file.name.toLowerCase(); + const extension = fileName.split(".").pop(); + + if (!extension || extension === fileName) { + return `파일 확장자가 없습니다. ${SUPPORTED_EXTENSIONS.join(", ")} 파일만 업로드 가능합니다.`; + } + return `지원되지 않는 파일 형식입니다. "${extension}" 대신 ${SUPPORTED_EXTENSIONS.join(", ")} 파일을 업로드해주세요.`; + } + + if (!validateFileSize(file)) { + const maxSizeMB = Math.round(MAX_FILE_SIZE / (1024 * 1024)); + const currentSizeMB = (file.size / (1024 * 1024)).toFixed(2); + return `파일 크기가 너무 큽니다. 현재 크기: ${currentSizeMB}MB, 최대 허용: ${maxSizeMB}MB`; + } + + if (file.name.length > 255) { + return "파일명이 너무 깁니다. 255자 이하의 파일명을 사용해주세요."; + } + + const invalidChars = /[<>:"/\\|?*]/; + if (invalidChars.test(file.name)) { + return '파일명에 사용할 수 없는 특수문자가 포함되어 있습니다. (< > : " / \\ | ? *)'; + } + + return ""; +} + +/** + * 한글 시트명을 안전하게 처리하는 함수 + */ +function sanitizeSheetName(sheetName: string): string { + if (!sheetName || typeof sheetName !== "string") { + return "Sheet1"; + } + + const maxLength = 31; + let sanitized = sheetName.trim(); + + if (sanitized.length > maxLength) { + sanitized = sanitized.substring(0, maxLength - 3) + "..."; + } + + sanitized = sanitized.replace(/[\\\/\*\?\[\]]/g, "_"); + + return sanitized || "Sheet"; +} + +/** + * 워크북 구조 검증 함수 + */ +function validateWorkbook(workbook: any): { isValid: boolean; error?: string } { + if (!workbook) { + return { isValid: false, error: "워크북이 null 또는 undefined입니다" }; + } + + if (!workbook.SheetNames) { + return { isValid: false, error: "워크북에 SheetNames 속성이 없습니다" }; + } + + if (!Array.isArray(workbook.SheetNames)) { + return { isValid: false, error: "SheetNames가 배열이 아닙니다" }; + } + + if (workbook.SheetNames.length === 0) { + return { isValid: false, error: "워크북에 시트가 없습니다" }; + } + + return { isValid: true }; +} + +/** + * SheetJS 데이터를 LuckyExcel 형식으로 변환 + */ +function convertSheetJSToLuckyExcel(workbook: any): SheetData[] { + console.log("🔄 SheetJS → LuckyExcel 형식 변환 시작..."); + + const luckySheets: SheetData[] = []; + + // 워크북 구조 검증 + const validation = validateWorkbook(workbook); + if (!validation.isValid) { + console.error("❌ 워크북 검증 실패:", validation.error); + throw new Error(`워크북 구조 오류: ${validation.error}`); + } + + console.log(`📋 발견된 시트: ${workbook.SheetNames.join(", ")}`); + + workbook.SheetNames.forEach((sheetName: string, index: number) => { + console.log( + `📋 시트 ${index + 1}/${workbook.SheetNames.length} "${sheetName}" 변환 중...`, + ); + + const safeSheetName = sanitizeSheetName(sheetName); + const worksheet = workbook.Sheets[sheetName]; + + if (!worksheet) { + console.warn( + `⚠️ 시트 "${sheetName}"를 찾을 수 없습니다. 빈 시트로 생성합니다.`, + ); + + luckySheets.push({ + id: `sheet_${index}`, + name: safeSheetName, + data: [[""]], + config: { + container: `luckysheet_${index}`, + title: safeSheetName, + lang: "ko", + data: [ + { + name: safeSheetName, + index: index.toString(), + celldata: [], + status: 1, + order: index, + row: 100, + column: 26, + }, + ], + options: { + showtoolbar: true, + showinfobar: true, + showsheetbar: true, + showstatisticBar: true, + allowCopy: true, + allowEdit: true, + enableAddRow: true, + enableAddCol: true, + }, + }, + }); + return; + } + + try { + // 시트 범위 확인 + const range = worksheet["!ref"]; + if (!range) { + console.warn( + `⚠️ 시트 "${sheetName}"에 데이터 범위가 없습니다. 빈 시트로 처리합니다.`, + ); + + luckySheets.push({ + id: `sheet_${index}`, + name: safeSheetName, + data: [[""]], + config: { + container: `luckysheet_${index}`, + title: safeSheetName, + lang: "ko", + data: [ + { + name: safeSheetName, + index: index.toString(), + celldata: [], + status: 1, + order: index, + row: 100, + column: 26, + }, + ], + options: { + showtoolbar: true, + showinfobar: true, + showsheetbar: true, + showstatisticBar: true, + allowCopy: true, + allowEdit: true, + enableAddRow: true, + enableAddCol: true, + }, + }, + }); + return; + } + + // 범위 파싱 + const rangeObj = XLSX.utils.decode_range(range); + const maxRow = rangeObj.e.r + 1; + const maxCol = rangeObj.e.c + 1; + + console.log(`📐 시트 "${sheetName}" 크기: ${maxRow}행 x ${maxCol}열`); + + // 2D 배열로 데이터 변환 + const data: any[][] = []; + const cellData: any[] = []; + + // 데이터 배열 초기화 + for (let row = 0; row < maxRow; row++) { + data[row] = new Array(maxCol).fill(""); + } + + // 셀 데이터 변환 + for (let row = rangeObj.s.r; row <= rangeObj.e.r; row++) { + for (let col = rangeObj.s.c; col <= rangeObj.e.c; col++) { + const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }); + const cell = worksheet[cellAddress]; + + if ( + cell && + cell.v !== undefined && + cell.v !== null && + cell.v !== "" + ) { + let cellValue = cell.v; + if (typeof cellValue === "string") { + cellValue = cellValue.trim(); + if (cellValue.length > 1000) { + cellValue = cellValue.substring(0, 997) + "..."; + } + } + + // 2D 배열에 데이터 저장 + data[row][col] = cellValue; + + // LuckyExcel celldata 형식으로 변환 + const luckyCell: any = { + r: row, + c: col, + v: { + v: cellValue, + m: cell.w || String(cellValue), // 포맷팅된 텍스트 우선 사용 + ct: { fa: "General", t: "g" }, + }, + }; + + // 🎨 xlsx-js-style 스타일 정보 처리 + if (cell.s) { + console.log( + `🎨 셀 ${cellAddress}에 스타일 정보 발견:`, + JSON.stringify(cell.s, null, 2), + ); + const convertedStyle = convertXlsxStyleToLuckysheet(cell.s); + console.log( + `🎨 변환된 Luckysheet 스타일:`, + JSON.stringify(convertedStyle, null, 2), + ); + luckyCell.v.s = convertedStyle; + } + + // 셀 타입에 따른 추가 처리 + if (cell.t === "s") { + luckyCell.v.ct.t = "s"; + } else if (cell.t === "n") { + luckyCell.v.ct.t = "n"; + // 숫자 포맷 처리 + if (cell.z) { + luckyCell.v.ct.fa = cell.z; + } + } else if (cell.t === "d") { + luckyCell.v.ct.t = "d"; + // 날짜 포맷 처리 + if (cell.z) { + luckyCell.v.ct.fa = cell.z; + } + } else if (cell.t === "b") { + luckyCell.v.ct.t = "b"; + } + + // 수식 처리 + if (cell.f) { + luckyCell.v.f = cell.f; + } + + cellData.push(luckyCell); + } + } + } + + // 🔗 병합 셀 정보 처리 + const mergeData: any[] = []; + if (worksheet["!merges"]) { + worksheet["!merges"].forEach((merge: any) => { + mergeData.push({ + r: merge.s.r, // 시작 행 + c: merge.s.c, // 시작 열 + rs: merge.e.r - merge.s.r + 1, // 행 병합 수 + cs: merge.e.c - merge.s.c + 1, // 열 병합 수 + }); + }); + console.log( + `🔗 시트 "${sheetName}"에서 ${mergeData.length}개 병합 셀 발견`, + ); + } + + // 📏 열 너비 정보 처리 + const colhidden: { [key: number]: number } = {}; + if (worksheet["!cols"]) { + worksheet["!cols"].forEach((col: any, colIndex: number) => { + if (col && col.hidden) { + colhidden[colIndex] = 0; // 숨겨진 열 + } else if (col && col.wpx) { + // 픽셀 단위 너비가 있으면 기록 (Luckysheet에서 활용 가능) + console.log(`📏 열 ${colIndex}: ${col.wpx}px`); + } + }); + } + + // 📐 행 높이 정보 처리 + const rowhidden: { [key: number]: number } = {}; + if (worksheet["!rows"]) { + worksheet["!rows"].forEach((row: any, rowIndex: number) => { + if (row && row.hidden) { + rowhidden[rowIndex] = 0; // 숨겨진 행 + } else if (row && row.hpx) { + // 픽셀 단위 높이가 있으면 기록 + console.log(`📐 행 ${rowIndex}: ${row.hpx}px`); + } + }); + } + + // SheetData 객체 생성 + const sheetData: SheetData = { + id: `sheet_${index}`, + name: safeSheetName, + data: data, + config: { + container: `luckysheet_${index}`, + title: safeSheetName, + lang: "ko", + data: [ + { + name: safeSheetName, + index: index.toString(), + celldata: cellData, + status: 1, + order: index, + row: maxRow, + column: maxCol, + // 🎨 xlsx-js-style로부터 추가된 스타일 정보들 + ...(mergeData.length > 0 && { merge: mergeData }), // 병합 셀 + ...(Object.keys(colhidden).length > 0 && { colhidden }), // 숨겨진 열 + ...(Object.keys(rowhidden).length > 0 && { rowhidden }), // 숨겨진 행 + }, + ], + options: { + showtoolbar: true, + showinfobar: true, + showsheetbar: true, + showstatisticBar: true, + allowCopy: true, + allowEdit: true, + enableAddRow: true, + enableAddCol: true, + }, + }, + }; + + luckySheets.push(sheetData); + console.log(`✅ 시트 "${sheetName}" 변환 완료: ${cellData.length}개 셀`); + } catch (sheetError) { + console.error(`❌ 시트 "${sheetName}" 변환 중 오류:`, sheetError); + + // 오류 발생 시 빈 시트로 생성 + luckySheets.push({ + id: `sheet_${index}`, + name: safeSheetName, + data: [[""]], + config: { + container: `luckysheet_${index}`, + title: safeSheetName, + lang: "ko", + data: [ + { + name: safeSheetName, + index: index.toString(), + celldata: [], + status: 1, + order: index, + row: 100, + column: 26, + }, + ], + options: { + showtoolbar: true, + showinfobar: true, + showsheetbar: true, + showstatisticBar: true, + allowCopy: true, + allowEdit: true, + enableAddRow: true, + enableAddCol: true, + }, + }, + }); + } + }); + + // 최소 1개 시트는 보장 + if (luckySheets.length === 0) { + console.log("📄 시트가 없어서 기본 시트를 생성합니다."); + luckySheets.push({ + id: "sheet_0", + name: "Sheet1", + data: [[""]], + config: { + container: "luckysheet_0", + title: "Sheet1", + lang: "ko", + data: [ + { + name: "Sheet1", + index: "0", + celldata: [], + status: 1, + order: 0, + row: 100, + column: 26, + }, + ], + options: { + showtoolbar: true, + showinfobar: false, + showsheetbar: true, + showstatisticBar: false, + allowCopy: true, + allowEdit: true, + enableAddRow: true, + enableAddCol: true, + }, + }, + }); + } + + console.log( + `🎉 SheetJS → LuckyExcel 변환 완료: ${luckySheets.length}개 시트`, + ); + return luckySheets; +} + +/** + * SheetJS로 파일을 읽고 XLSX로 변환한 뒤 LuckyExcel로 처리 + */ +async function processFileWithSheetJSToXLSX( + file: File, +): Promise<{ sheets: SheetData[]; xlsxBuffer: ArrayBuffer }> { + console.log("📊 SheetJS → XLSX → LuckyExcel 파이프라인 시작..."); + + const arrayBuffer = await file.arrayBuffer(); + const fileName = file.name.toLowerCase(); + const isCSV = fileName.endsWith(".csv"); + const isXLS = fileName.endsWith(".xls"); + // const isXLSX = fileName.endsWith(".xlsx"); + + // 1단계: SheetJS로 파일 읽기 + let workbook: any; + + try { + if (isCSV) { + // CSV 파일 처리 - UTF-8 디코딩 후 읽기 + console.log("📄 CSV 파일을 SheetJS로 읽는 중..."); + const text = new TextDecoder("utf-8").decode(arrayBuffer); + workbook = XLSX.read(text, { + type: "string", + codepage: 65001, // UTF-8 + raw: false, + }); + } else { + // XLS/XLSX 파일 처리 - 스타일 정보 강제 추출 옵션 + console.log(`📊 ${isXLS ? "XLS" : "XLSX"} 파일을 SheetJS로 읽는 중...`); + workbook = XLSX.read(arrayBuffer, { + cellStyles: true, // 스타일 정보 보존 + cellNF: true, // 숫자 형식 보존 (스타일의 일부) + bookProps: true, // 문서 속성 보존 (스타일 정보 포함 가능) + WTF: true, // 더 관대한 파싱 + }); + } + } catch (readError) { + console.error("❌ SheetJS 파일 읽기 실패:", readError); + throw new Error( + `파일을 읽을 수 없습니다: ${readError instanceof Error ? readError.message : readError}`, + ); + } + + // 파일 버퍼 크기 검증 + if (arrayBuffer.byteLength === 0) { + throw new Error("파일이 비어있습니다."); + } + + // 워크북 null 체크 + if (!workbook) { + throw new Error("워크북을 생성할 수 없습니다."); + } + + // 기본 검증만 수행 + if (!workbook.SheetNames || workbook.SheetNames.length === 0) { + throw new Error("시트 이름 정보가 없습니다."); + } + + // Sheets 객체가 없으면 빈 객체로 초기화 + if (!workbook.Sheets) { + workbook.Sheets = {}; + } + + console.log("✅ SheetJS 워크북 읽기 성공:", { + sheetNames: workbook.SheetNames, + sheetCount: workbook.SheetNames.length, + }); + + // 🎨 스타일 정보 상세 분석 (개발 모드에서만) + if (import.meta.env.DEV) { + analyzeSheetStyles(workbook); + } + + // 2단계: 워크북을 XLSX ArrayBuffer로 변환 + let xlsxArrayBuffer: ArrayBuffer; + try { + console.log("🔄 XLSX 형식으로 변환 중..."); + const xlsxData = XLSX.write(workbook, { + type: "array", + bookType: "xlsx", + cellStyles: true, // 스타일 정보 보존 + }); + + console.log(`✅ XLSX 변환 완료: ${xlsxData.length} bytes`); + + // xlsxData는 Uint8Array이므로 ArrayBuffer로 변환 + if (xlsxData instanceof Uint8Array) { + xlsxArrayBuffer = xlsxData.buffer.slice( + xlsxData.byteOffset, + xlsxData.byteOffset + xlsxData.byteLength, + ); + } else if (xlsxData instanceof ArrayBuffer) { + xlsxArrayBuffer = xlsxData; + } else { + // 다른 타입의 경우 새 ArrayBuffer 생성 + xlsxArrayBuffer = new ArrayBuffer(xlsxData.length); + const view = new Uint8Array(xlsxArrayBuffer); + for (let i = 0; i < xlsxData.length; i++) { + view[i] = xlsxData[i]; + } + } + + console.log(`✅ XLSX 변환 완료: ${xlsxArrayBuffer.byteLength} bytes`); + } catch (writeError) { + console.error("❌ XLSX 변환 실패:", writeError); + throw new Error( + `XLSX 변환 실패: ${writeError instanceof Error ? writeError.message : writeError}`, + ); + } + + // 3단계: ArrayBuffer가 완전히 준비된 후 LuckyExcel로 처리 + console.log("🍀 LuckyExcel로 변환된 XLSX 처리 중..."); + + // ArrayBuffer 최종 검증 + if (!xlsxArrayBuffer) { + throw new Error("ArrayBuffer가 생성되지 않았습니다"); + } + + if (xlsxArrayBuffer.byteLength === 0) { + throw new Error("ArrayBuffer 크기가 0입니다"); + } + + // 원본 파일명에서 확장자를 .xlsx로 변경 + // const xlsxFileName = file.name.replace(/\.(csv|xls|xlsx)$/i, ".xlsx"); + console.log("🍀 LuckyExcel 처리 시작..."); + + // Promise를 사용한 LuckyExcel 처리 + return new Promise<{ sheets: SheetData[]; xlsxBuffer: ArrayBuffer }>( + (resolve, reject) => { + try { + // LuckyExcel API는 (arrayBuffer, successCallback, errorCallback) 형태로 호출 + // 공식 문서: LuckyExcel.transformExcelToLucky(file, successCallback, errorCallback) + (window.LuckyExcel as any).transformExcelToLucky( + xlsxArrayBuffer, + // 성공 콜백 함수 (두 번째 매개변수) + (exportJson: any, _luckysheetfile: any) => { + try { + console.log( + "🍀 LuckyExcel 변환 성공:", + exportJson?.sheets?.length || 0, + "개 시트", + ); + + // 데이터 유효성 검사 + if ( + !exportJson || + !exportJson.sheets || + !Array.isArray(exportJson.sheets) || + exportJson.sheets.length === 0 + ) { + console.warn( + "⚠️ LuckyExcel 결과가 유효하지 않습니다. SheetJS 방식으로 대체 처리합니다.", + ); + + // LuckyExcel 실패 시 SheetJS 데이터를 직접 변환 + const fallbackSheets = convertSheetJSToLuckyExcel(workbook); + resolve({ + sheets: fallbackSheets, + xlsxBuffer: xlsxArrayBuffer, + }); + return; + } + + // LuckyExcel 변환이 성공한 경우 - SheetData 형식으로 변환 + const sheets: SheetData[] = exportJson.sheets.map( + (luckySheet: any, index: number) => { + const sheetName = luckySheet.name || `Sheet${index + 1}`; + const maxRow = luckySheet.row || 0; + const maxCol = luckySheet.column || 0; + + // 2D 배열 초기화 + const data: any[][] = []; + for (let r = 0; r < maxRow; r++) { + data[r] = new Array(maxCol).fill(""); + } + + // celldata에서 데이터 추출 + if ( + luckySheet.celldata && + Array.isArray(luckySheet.celldata) + ) { + luckySheet.celldata.forEach((cell: any) => { + if ( + cell && + typeof cell.r === "number" && + typeof cell.c === "number" + ) { + const row = cell.r; + const col = cell.c; + + if (row < maxRow && col < maxCol && cell.v) { + const cellValue = cell.v.v || cell.v.m || ""; + data[row][col] = String(cellValue).trim(); + } + } + }); + } + + // 빈 데이터 처리 + if (data.length === 0) { + data.push([""]); + } + + return { + id: `sheet_${index}`, + name: sheetName, + data: data, + xlsxBuffer: xlsxArrayBuffer, // 변환된 XLSX ArrayBuffer 포함 + config: { + container: `luckysheet_${index}`, + title: sheetName, + lang: "ko", + data: [ + { + name: sheetName, + index: index.toString(), + celldata: luckySheet.celldata || [], + status: 1, + order: index, + row: maxRow, + column: maxCol, + }, + ], + options: { + showtoolbar: true, + showinfobar: false, + showsheetbar: true, + showstatisticBar: true, + allowCopy: true, + allowEdit: true, + enableAddRow: true, + enableAddCol: true, + }, + }, + }; + }, + ); + + console.log("✅ LuckyExcel 처리 성공:", sheets.length, "개 시트"); + resolve({ sheets, xlsxBuffer: xlsxArrayBuffer }); + } catch (processError) { + console.error("❌ LuckyExcel 후처리 중 오류:", processError); + + // LuckyExcel 후처리 실패 시 SheetJS 방식으로 대체 + try { + console.log("🔄 SheetJS 방식으로 대체 처리..."); + const fallbackSheets = convertSheetJSToLuckyExcel(workbook); + resolve({ + sheets: fallbackSheets, + xlsxBuffer: xlsxArrayBuffer, + }); + } catch (fallbackError) { + console.error("❌ SheetJS 대체 처리도 실패:", fallbackError); + reject(fallbackError); + } + } + }, + // 오류 콜백 함수 (세 번째 매개변수) + (error: any) => { + console.error("❌ LuckyExcel 변환 오류:", error); + + // LuckyExcel 오류 시 SheetJS 방식으로 대체 + try { + console.log("🔄 SheetJS 방식으로 대체 처리..."); + const fallbackSheets = convertSheetJSToLuckyExcel(workbook); + resolve({ sheets: fallbackSheets, xlsxBuffer: xlsxArrayBuffer }); + } catch (fallbackError) { + console.error("❌ SheetJS 대체 처리도 실패:", fallbackError); + reject(fallbackError); + } + }, + ); + } catch (luckyError) { + console.error("❌ LuckyExcel 호출 중 오류:", luckyError); + + // LuckyExcel 호출 실패 시 SheetJS 방식으로 대체 + try { + console.log("🔄 SheetJS 방식으로 대체 처리..."); + const fallbackSheets = convertSheetJSToLuckyExcel(workbook); + resolve({ sheets: fallbackSheets, xlsxBuffer: xlsxArrayBuffer }); + } catch (fallbackError) { + console.error("❌ SheetJS 대체 처리도 실패:", fallbackError); + reject(fallbackError); + } + } + }, + ); +} + +/** + * XLSX 파일을 바로 LuckyExcel로 처리 (공식 예제 순서 준수) + * - 공식 문서 예제를 그대로 따름: LuckyExcel.transformExcelToLucky → luckysheet.create + */ +async function processXLSXWithLuckyExcel( + file: File, +): Promise<{ sheets: SheetData[]; xlsxBuffer: ArrayBuffer }> { + console.log("🍀 XLSX 파일을 LuckyExcel로 직접 처리 시작..."); + console.log(`📊 XLSX 파일: ${file.name}, 크기: ${file.size} bytes`); + + // Promise를 사용한 LuckyExcel 처리 (공식 예제 순서) + return new Promise<{ sheets: SheetData[]; xlsxBuffer: ArrayBuffer }>( + (resolve, reject) => { + // Make sure to get the xlsx file first, and then use the global method window.LuckyExcel to convert + (window.LuckyExcel as any).transformExcelToLucky( + file, + // After obtaining the converted table data, use luckysheet to initialize or update the existing luckysheet workbook + function (exportJson: any, _luckysheetfile: any) { + console.log( + "🍀 LuckyExcel 변환 성공:", + exportJson?.sheets?.length || 0, + "개 시트", + ); + + // ArrayBuffer는 성공 시에만 생성 + file + .arrayBuffer() + .then((arrayBuffer) => { + // exportJson.sheets를 SheetData 형식으로 단순 변환 + const sheets: SheetData[] = exportJson.sheets.map( + (sheet: any, index: number) => ({ + id: `sheet_${index}`, + name: sheet.name || `Sheet${index + 1}`, + data: [[""]], // 실제 데이터는 luckysheet에서 처리 + xlsxBuffer: arrayBuffer, + config: { + container: `luckysheet_${index}`, + title: exportJson.info?.name || file.name, + lang: "ko", + // Note: Luckysheet needs to introduce a dependency package and initialize the table container before it can be used + data: exportJson.sheets, // exportJson.sheets를 그대로 전달 + // 공식 예제에 따른 설정 + ...(exportJson.info?.name && { + title: exportJson.info.name, + }), + ...(exportJson.info?.creator && { + userInfo: exportJson.info.creator, + }), + options: { + showtoolbar: true, + showinfobar: false, + showsheetbar: true, + showstatisticBar: true, + allowCopy: true, + allowEdit: true, + enableAddRow: true, + enableAddCol: true, + }, + }, + }), + ); + + console.log( + "✅ XLSX 파일 LuckyExcel 처리 완료:", + sheets.length, + "개 시트", + ); + resolve({ sheets, xlsxBuffer: arrayBuffer }); + }) + .catch((bufferError) => { + reject(new Error(`ArrayBuffer 생성 실패: ${bufferError}`)); + }); + }, + // Import failed. Is your file a valid xlsx? + function (err: any) { + console.error("❌ LuckyExcel 변환 실패:", err); + reject(new Error(`XLSX 파일 변환 실패: ${err}`)); + }, + ); + }, + ); +} + +/** + * 엑셀 파일을 SheetData 배열로 변환 (파일 형식별 최적화 버전) + * - XLSX: LuckyExcel 직접 처리 (스타일 정보 완전 보존) + * - CSV/XLS: SheetJS → XLSX → LuckyExcel 파이프라인 + */ +export async function processExcelFile(file: File): Promise { + try { + const errorMessage = getFileErrorMessage(file); + if (errorMessage) { + return { + success: false, + error: errorMessage, + fileName: file.name, + fileSize: file.size, + file: file, + }; + } + + // 파일 형식 감지 + const fileName = file.name.toLowerCase(); + const isCSV = fileName.endsWith(".csv"); + const isXLS = fileName.endsWith(".xls"); + const isXLSX = fileName.endsWith(".xlsx"); + + if (!isCSV && !isXLS && !isXLSX) { + return { + success: false, + error: + "지원되지 않는 파일 형식입니다. .csv, .xls, .xlsx 파일을 사용해주세요.", + fileName: file.name, + fileSize: file.size, + file: file, + }; + } + + console.log( + `📁 파일 처리 시작: ${file.name} (${isCSV ? "CSV" : isXLS ? "XLS" : "XLSX"})`, + ); + + let sheets: SheetData[]; + let xlsxBuffer: ArrayBuffer; + + if (isXLSX) { + // 🍀 XLSX 파일: LuckyExcel 직접 처리 (스타일 정보 완전 보존) + console.log("🍀 XLSX 파일 - LuckyExcel 직접 처리 방식 사용"); + const result = await processXLSXWithLuckyExcel(file); + sheets = result.sheets; + xlsxBuffer = result.xlsxBuffer; + } else { + // 📊 CSV/XLS 파일: SheetJS → XLSX → LuckyExcel 파이프라인 + console.log(`📊 ${isCSV ? "CSV" : "XLS"} 파일 - SheetJS 파이프라인 사용`); + const result = await processFileWithSheetJSToXLSX(file); + sheets = result.sheets; + xlsxBuffer = result.xlsxBuffer; + } + + if (!sheets || sheets.length === 0) { + return { + success: false, + error: "파일에서 유효한 시트를 찾을 수 없습니다.", + fileName: file.name, + fileSize: file.size, + file: file, + }; + } + + console.log(`🎉 파일 처리 완료: ${sheets.length}개 시트`); + + return { + success: true, + data: sheets, + fileName: file.name, + fileSize: file.size, + file: file, + xlsxBuffer, + }; + } catch (error) { + console.error("❌ 파일 처리 중 오류 발생:", error); + + let errorMessage = "파일을 읽는 중 오류가 발생했습니다."; + + if (error instanceof Error) { + if ( + error.message.includes("파일에 워크시트가 없습니다") || + error.message.includes("워크북 구조 오류") || + error.message.includes("파일 처리 실패") || + error.message.includes("파일 읽기 실패") || + error.message.includes("XLSX 변환 실패") || + error.message.includes("파일이 비어있습니다") || + error.message.includes("워크북을 생성할 수 없습니다") || + error.message.includes("유효한 시트가 없습니다") || + error.message.includes("시트 이름 정보가 없습니다") || + error.message.includes("파일을 읽을 수 없습니다") || + error.message.includes("XLSX 파일 처리 실패") || + error.message.includes("XLSX 파일 변환 실패") + ) { + errorMessage = error.message; + } else if (error.message.includes("transformExcelToLucky")) { + errorMessage = + "Excel 파일 변환에 실패했습니다. 파일이 손상되었거나 지원되지 않는 형식일 수 있습니다."; + } else { + errorMessage = `파일 처리 중 오류: ${error.message}`; + } + } + + return { + success: false, + error: errorMessage, + fileName: file.name, + fileSize: file.size, + file: file, + }; + } +} + +/** + * 여러 파일 중 유효한 파일만 필터링 + */ +export function filterValidFiles(files: FileList): File[] { + return Array.from(files).filter((file) => { + const errorMessage = getFileErrorMessage(file); + return errorMessage === ""; + }); +} + +/** + * 파일 목록의 에러 정보 수집 + */ +export function getFileErrors( + files: FileList, +): { file: File; error: string }[] { + const errors: { file: File; error: string }[] = []; + + Array.from(files).forEach((file) => { + const errorMessage = getFileErrorMessage(file); + if (errorMessage !== "") { + errors.push({ file, error: errorMessage }); + } + }); + + return errors; +} diff --git a/src/utils/luckysheetApi.ts b/src/utils/luckysheetApi.ts deleted file mode 100644 index 0519ecb..0000000 --- a/src/utils/luckysheetApi.ts +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 3ae7db6..5d566b2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -16,13 +16,18 @@ export default defineConfig({ stream: "stream-browserify", buffer: "buffer", }, + // 중복 모듈 해결을 위한 dedupe 설정 + dedupe: ["@wendellhu/redi"], }, // 의존성 최적화 설정 optimizeDeps: { - exclude: [ - // 중복 로딩 방지를 위해 redi와 univer 관련 제외 + include: [ + // REDI 중복 로드 방지를 위해 명시적으로 포함 "@wendellhu/redi", + ], + exclude: [ + // Univer 관련 모듈만 제외 "@univerjs/core", "@univerjs/design", "@univerjs/ui", @@ -46,6 +51,8 @@ export default defineConfig({ external: [], output: { manualChunks: { + // REDI를 별도 청크로 분리하여 중복 방지 + redi: ["@wendellhu/redi"], // Univer 관련 라이브러리를 별도 청크로 분리 "univer-core": [ "@univerjs/core",