럭키시트 로드 가능, 옵션이 안불러짐
This commit is contained in:
5
.cursor/rules/luckysheet-functionlist-error-fix.mdc
Normal file
5
.cursor/rules/luckysheet-functionlist-error-fix.mdc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
172
.cursor/rules/tailwind-v4-migration.mdc
Normal file
172
.cursor/rules/tailwind-v4-migration.mdc
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Tailwind CSS v4 Migration Guide
|
||||||
|
|
||||||
|
## **Key Changes in Tailwind CSS v4**
|
||||||
|
- **No PostCSS dependency**: v4 has built-in CSS processing
|
||||||
|
- **Simplified import**: Use `@import "tailwindcss"` instead of separate directives
|
||||||
|
- **Vite plugin**: Use `@tailwindcss/vite` for Vite integration
|
||||||
|
- **No config file needed**: Configuration through CSS custom properties
|
||||||
|
|
||||||
|
## **Complete Migration Steps**
|
||||||
|
|
||||||
|
### **1. Remove All v3 Dependencies**
|
||||||
|
```bash
|
||||||
|
# Remove all Tailwind v3 and PostCSS packages
|
||||||
|
npm uninstall tailwindcss @tailwindcss/postcss @tailwindcss/node @tailwindcss/oxide
|
||||||
|
npm uninstall autoprefixer postcss postcss-import
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Install Tailwind CSS v4**
|
||||||
|
```bash
|
||||||
|
# Install v4 packages
|
||||||
|
npm install @tailwindcss/cli@next @tailwindcss/vite@next
|
||||||
|
npm install tailwind-merge # For utility merging
|
||||||
|
```
|
||||||
|
|
||||||
|
### **3. Update CSS Import**
|
||||||
|
```css
|
||||||
|
/* ❌ DON'T: v3 style imports */
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* ✅ DO: v4 single import */
|
||||||
|
@import "tailwindcss";
|
||||||
|
```
|
||||||
|
|
||||||
|
### **4. Update Vite Configuration**
|
||||||
|
```typescript
|
||||||
|
// ❌ DON'T: v3 PostCSS setup
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import tailwindcss from "tailwindcss";
|
||||||
|
import autoprefixer from "autoprefixer";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
css: {
|
||||||
|
postcss: {
|
||||||
|
plugins: [tailwindcss(), autoprefixer()],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ DO: v4 Vite plugin
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### **5. Remove Configuration Files**
|
||||||
|
```bash
|
||||||
|
# Delete these files (v4 doesn't need them)
|
||||||
|
rm postcss.config.js
|
||||||
|
rm tailwind.config.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## **v4 Configuration (Optional)**
|
||||||
|
|
||||||
|
### **CSS-based Configuration**
|
||||||
|
```css
|
||||||
|
/* src/index.css */
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* Custom theme configuration */
|
||||||
|
@theme {
|
||||||
|
--color-primary: #3b82f6;
|
||||||
|
--color-secondary: #64748b;
|
||||||
|
--font-family-custom: "Inter", sans-serif;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Advanced Configuration**
|
||||||
|
```css
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* Custom utilities */
|
||||||
|
@utility {
|
||||||
|
.scroll-smooth {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom components */
|
||||||
|
@component {
|
||||||
|
.btn {
|
||||||
|
@apply px-4 py-2 rounded font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
@apply bg-blue-500 text-white hover:bg-blue-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## **Migration Checklist**
|
||||||
|
|
||||||
|
### **Dependencies**
|
||||||
|
- ✅ Remove: `tailwindcss`, `@tailwindcss/postcss`, `autoprefixer`, `postcss`
|
||||||
|
- ✅ Install: `@tailwindcss/cli@next`, `@tailwindcss/vite@next`
|
||||||
|
- ✅ Keep: `tailwind-merge`, `class-variance-authority`, `clsx`
|
||||||
|
|
||||||
|
### **Files**
|
||||||
|
- ✅ Update: `src/index.css` - Use `@import "tailwindcss"`
|
||||||
|
- ✅ Update: `vite.config.ts` - Use `@tailwindcss/vite` plugin
|
||||||
|
- ✅ Delete: `postcss.config.js`, `tailwind.config.js`
|
||||||
|
|
||||||
|
### **Code Changes**
|
||||||
|
- ✅ All existing Tailwind classes work the same
|
||||||
|
- ✅ `tailwind-merge` still works for utility merging
|
||||||
|
- ✅ Custom CSS can be added alongside Tailwind
|
||||||
|
|
||||||
|
## **Benefits of v4**
|
||||||
|
- **Faster builds**: No PostCSS processing overhead
|
||||||
|
- **Simpler setup**: Fewer configuration files
|
||||||
|
- **Better performance**: Optimized CSS generation
|
||||||
|
- **Modern architecture**: Built for current web standards
|
||||||
|
|
||||||
|
## **Troubleshooting**
|
||||||
|
|
||||||
|
### **Build Errors**
|
||||||
|
```bash
|
||||||
|
# If you see PostCSS errors, ensure all PostCSS packages are removed
|
||||||
|
npm ls | grep postcss # Should return nothing
|
||||||
|
|
||||||
|
# Clean install if needed
|
||||||
|
rm -rf node_modules package-lock.json
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### **CSS Not Loading**
|
||||||
|
```css
|
||||||
|
/* Ensure correct import in src/index.css */
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* NOT these old imports */
|
||||||
|
/* @tailwind base; */
|
||||||
|
/* @tailwind components; */
|
||||||
|
/* @tailwind utilities; */
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Vite Plugin Issues**
|
||||||
|
```typescript
|
||||||
|
// Ensure correct plugin import
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
|
// Add to plugins array
|
||||||
|
plugins: [react(), tailwindcss()]
|
||||||
|
```
|
||||||
|
|
||||||
|
## **Best Practices**
|
||||||
|
- **Test thoroughly**: Verify all Tailwind classes still work
|
||||||
|
- **Update incrementally**: Migrate one component at a time if needed
|
||||||
|
- **Monitor bundle size**: v4 should reduce overall CSS size
|
||||||
|
- **Use CSS-in-CSS**: Leverage v4's CSS-based configuration for themes
|
||||||
BIN
luckysheet-src.zip
Normal file
BIN
luckysheet-src.zip
Normal file
Binary file not shown.
115
package-lock.json
generated
115
package-lock.json
generated
@@ -15,6 +15,7 @@
|
|||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
"luckyexcel": "^1.0.1",
|
"luckyexcel": "^1.0.1",
|
||||||
|
"luckysheet": "^2.1.13",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"sheetjs-style": "^0.15.8",
|
"sheetjs-style": "^0.15.8",
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||||
"@typescript-eslint/parser": "^8.15.0",
|
"@typescript-eslint/parser": "^8.15.0",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"@vitest/ui": "^3.2.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.15.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
@@ -309,7 +311,6 @@
|
|||||||
"version": "7.27.6",
|
"version": "7.27.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
|
||||||
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
|
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -1424,6 +1425,13 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@polka/url": {
|
||||||
|
"version": "1.0.0-next.29",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||||
|
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.11",
|
"version": "1.0.0-beta.11",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz",
|
||||||
@@ -2782,6 +2790,28 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitest/ui": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/utils": "3.2.4",
|
||||||
|
"fflate": "^0.8.2",
|
||||||
|
"flatted": "^3.3.3",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"sirv": "^3.0.1",
|
||||||
|
"tinyglobby": "^0.2.14",
|
||||||
|
"tinyrainbow": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vitest": "3.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitest/utils": {
|
"node_modules/@vitest/utils": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
|
||||||
@@ -3496,6 +3526,12 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.11.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||||
|
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
@@ -4120,6 +4156,13 @@
|
|||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
@@ -4183,6 +4226,12 @@
|
|||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/flatpickr": {
|
||||||
|
"version": "4.6.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz",
|
||||||
|
"integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/flatted": {
|
"node_modules/flatted": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||||
@@ -5151,6 +5200,13 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jquery": {
|
||||||
|
"version": "2.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/jquery/-/jquery-2.2.4.tgz",
|
||||||
|
"integrity": "sha512-lBHj60ezci2u1v2FqnZIraShGgEXq35qCzMv4lITyHGppTnA13rwR0MgwyNJh9TnDs3aXUvd1xjAotfraMHX/Q==",
|
||||||
|
"deprecated": "This version is deprecated. Please upgrade to the latest version or find support at https://www.herodevs.com/support/jquery-nes.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -5633,6 +5689,19 @@
|
|||||||
"jszip": "^3.5.0"
|
"jszip": "^3.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/luckysheet": {
|
||||||
|
"version": "2.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/luckysheet/-/luckysheet-2.1.13.tgz",
|
||||||
|
"integrity": "sha512-ZotItRKh3fxEtYz0GrZxkf97jeQSGsJpFNAu1I0NMDQ6rVrHAWKeggFak5pClGQ3DP62Gi8kd+8rzOpyY/UNZw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.1",
|
||||||
|
"dayjs": "^1.9.6",
|
||||||
|
"flatpickr": "^4.6.6",
|
||||||
|
"jquery": "^2.2.4",
|
||||||
|
"numeral": "^2.0.6",
|
||||||
|
"pako": "^1.0.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lz-string": {
|
"node_modules/lz-string": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||||
@@ -5769,6 +5838,16 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mrmime": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -5840,6 +5919,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/numeral": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nwsapi": {
|
"node_modules/nwsapi": {
|
||||||
"version": "2.2.20",
|
"version": "2.2.20",
|
||||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
|
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
|
||||||
@@ -6672,6 +6760,21 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sirv": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@polka/url": "^1.0.0-next.24",
|
||||||
|
"mrmime": "^2.0.0",
|
||||||
|
"totalist": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/slash": {
|
"node_modules/slash": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||||
@@ -7197,6 +7300,16 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/totalist": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tough-cookie": {
|
"node_modules/tough-cookie": {
|
||||||
"version": "4.1.4",
|
"version": "4.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
"luckyexcel": "^1.0.1",
|
"luckyexcel": "^1.0.1",
|
||||||
|
"luckysheet": "^2.1.13",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"sheetjs-style": "^0.15.8",
|
"sheetjs-style": "^0.15.8",
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||||
"@typescript-eslint/parser": "^8.15.0",
|
"@typescript-eslint/parser": "^8.15.0",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"@vitest/ui": "^3.2.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.15.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
|||||||
1701
public/output.css
Normal file
1701
public/output.css
Normal file
File diff suppressed because it is too large
Load Diff
43
src/App.tsx
43
src/App.tsx
@@ -2,8 +2,14 @@ import { useAppStore } from "./stores/useAppStore";
|
|||||||
import { Card, CardContent } from "./components/ui/card";
|
import { Card, CardContent } from "./components/ui/card";
|
||||||
import { Button } from "./components/ui/button";
|
import { Button } from "./components/ui/button";
|
||||||
import { FileUpload } from "./components/sheet/FileUpload";
|
import { FileUpload } from "./components/sheet/FileUpload";
|
||||||
|
import { SheetViewer } from "./components/sheet/SheetViewer";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const { currentFile, sheets, resetApp } = useAppStore();
|
||||||
|
|
||||||
|
// 파일이 업로드되어 시트 데이터가 있는 경우와 없는 경우 구분
|
||||||
|
const hasSheetData = currentFile && sheets && sheets.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
@@ -14,17 +20,44 @@ function App() {
|
|||||||
<h1 className="text-2xl font-bold text-blue-600">sheetEasy AI</h1>
|
<h1 className="text-2xl font-bold text-blue-600">sheetEasy AI</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<span className="text-sm text-gray-600">
|
{hasSheetData && (
|
||||||
Excel 파일 AI 처리 도구
|
<>
|
||||||
</span>
|
<span className="text-sm text-gray-600">
|
||||||
|
{currentFile.name}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={resetApp}
|
||||||
|
className="text-gray-600 hover:text-gray-800"
|
||||||
|
>
|
||||||
|
새 파일
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!hasSheetData && (
|
||||||
|
<span className="text-sm text-gray-600">
|
||||||
|
Excel 파일 AI 처리 도구
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* 메인 콘텐츠 */}
|
{/* 메인 콘텐츠 */}
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<main className="h-[calc(100vh-4rem)]">
|
||||||
<FileUpload />
|
{hasSheetData ? (
|
||||||
|
// 파일이 업로드된 경우: SheetViewer 표시 (전체화면)
|
||||||
|
<div className="h-full">
|
||||||
|
<SheetViewer className="h-full" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// 파일이 업로드되지 않은 경우: FileUpload 표시 (중앙 정렬)
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<FileUpload />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
516
src/components/sheet/SheetViewer.tsx
Normal file
516
src/components/sheet/SheetViewer.tsx
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
import React, {
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { useAppStore } from "../../stores/useAppStore";
|
||||||
|
import type { SheetData } from "../../types/sheet";
|
||||||
|
|
||||||
|
// Window 타입 확장
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
luckysheet: any;
|
||||||
|
LuckyExcel: any;
|
||||||
|
$: any; // jQuery
|
||||||
|
Store: any; // Luckysheet Store
|
||||||
|
luckysheet_function: any; // Luckysheet function list
|
||||||
|
functionlist: any[]; // 글로벌 functionlist
|
||||||
|
luckysheetConfigsetting: any; // Luckysheet 설정 객체
|
||||||
|
luckysheetPostil: any; // Luckysheet 포스틸 객체
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SheetViewerProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Luckysheet 시트 뷰어 컴포넌트
|
||||||
|
* - 참고 내용 기반: 완전한 라이브러리 로딩 순서 적용
|
||||||
|
* - functionlist 오류 방지를 위한 완전한 초기화
|
||||||
|
* - 필수 플러그인과 CSS 포함
|
||||||
|
*/
|
||||||
|
export function SheetViewer({ className }: SheetViewerProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const luckysheetRef = useRef<any>(null);
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
const [isConverting, setIsConverting] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isContainerReady, setIsContainerReady] = useState(false);
|
||||||
|
const [librariesLoaded, setLibrariesLoaded] = useState(false);
|
||||||
|
|
||||||
|
// 스토어에서 시트 데이터 가져오기
|
||||||
|
const { sheets, activeSheetId, currentFile, setSelectedRange } =
|
||||||
|
useAppStore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CDN 배포판 + functionlist 직접 초기화 방식
|
||||||
|
*/
|
||||||
|
const loadLuckysheetLibrary = useCallback((): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 이미 로드된 경우
|
||||||
|
if (
|
||||||
|
window.luckysheet &&
|
||||||
|
window.LuckyExcel &&
|
||||||
|
window.$ &&
|
||||||
|
librariesLoaded
|
||||||
|
) {
|
||||||
|
console.log("📦 모든 라이브러리가 이미 로드됨");
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log("📦 CDN 배포판 + functionlist 직접 초기화 방식...");
|
||||||
|
|
||||||
|
const loadResource = (
|
||||||
|
type: "css" | "js",
|
||||||
|
src: string,
|
||||||
|
id: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
return new Promise((resourceResolve, resourceReject) => {
|
||||||
|
// 이미 로드된 리소스 체크
|
||||||
|
if (document.querySelector(`[data-luckysheet-id="${id}"]`)) {
|
||||||
|
// console.log(`📦 ${id} 이미 로드됨`);
|
||||||
|
resourceResolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "css") {
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.href = src;
|
||||||
|
link.setAttribute("data-luckysheet-id", id);
|
||||||
|
link.onload = () => {
|
||||||
|
// console.log(`✅ ${id} CSS 로드 완료`);
|
||||||
|
resourceResolve();
|
||||||
|
};
|
||||||
|
link.onerror = (error) => {
|
||||||
|
// console.error(`❌ ${id} CSS 로드 실패:`, error);
|
||||||
|
resourceReject(new Error(`${id} CSS 로드 실패`));
|
||||||
|
};
|
||||||
|
document.head.appendChild(link);
|
||||||
|
} else {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = src;
|
||||||
|
script.setAttribute("data-luckysheet-id", id);
|
||||||
|
script.onload = () => {
|
||||||
|
// console.log(`✅ ${id} JS 로드 완료`);
|
||||||
|
resourceResolve();
|
||||||
|
};
|
||||||
|
script.onerror = (error) => {
|
||||||
|
// console.error(`❌ ${id} JS 로드 실패:`, error);
|
||||||
|
resourceReject(new Error(`${id} JS 로드 실패`));
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// CDN 배포판 로딩 + functionlist 직접 초기화
|
||||||
|
const loadSequence = async () => {
|
||||||
|
try {
|
||||||
|
// 1. jQuery (Luckysheet 의존성)
|
||||||
|
if (!window.$) {
|
||||||
|
await loadResource(
|
||||||
|
"js",
|
||||||
|
"https://code.jquery.com/jquery-3.6.0.min.js",
|
||||||
|
"jquery",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. CSS 로드 (CDN 방식 - 공식 문서 순서 준수)
|
||||||
|
await loadResource(
|
||||||
|
"css",
|
||||||
|
"https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/css/pluginsCss.css",
|
||||||
|
"plugins-css",
|
||||||
|
);
|
||||||
|
await loadResource(
|
||||||
|
"css",
|
||||||
|
"https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/plugins.css",
|
||||||
|
"plugins-main-css",
|
||||||
|
);
|
||||||
|
await loadResource(
|
||||||
|
"css",
|
||||||
|
"https://cdn.jsdelivr.net/npm/luckysheet/dist/css/luckysheet.css",
|
||||||
|
"luckysheet-css",
|
||||||
|
);
|
||||||
|
await loadResource(
|
||||||
|
"css",
|
||||||
|
"https://cdn.jsdelivr.net/npm/luckysheet/dist/assets/iconfont/iconfont.css",
|
||||||
|
"iconfont-css",
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Plugin JS 먼저 로드 (functionlist 초기화 우선)
|
||||||
|
await loadResource(
|
||||||
|
"js",
|
||||||
|
"https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/js/plugin.js",
|
||||||
|
"plugin-js",
|
||||||
|
);
|
||||||
|
|
||||||
|
// 👉 Plugin.js 로드 완료 후 바로 다음 단계로 진행
|
||||||
|
console.log("✅ Plugin.js 로드 완료");
|
||||||
|
|
||||||
|
// 4. LuckyExcel (Excel 파일 처리용 - 공식 문서 방식)
|
||||||
|
if (!window.LuckyExcel) {
|
||||||
|
await loadResource(
|
||||||
|
"js",
|
||||||
|
"https://cdn.jsdelivr.net/npm/luckyexcel/dist/luckyexcel.umd.js",
|
||||||
|
"luckyexcel",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Luckysheet 메인 (functionlist 준비 후 - 공식 문서 방식)
|
||||||
|
if (!window.luckysheet) {
|
||||||
|
await loadResource(
|
||||||
|
"js",
|
||||||
|
"https://cdn.jsdelivr.net/npm/luckysheet/dist/luckysheet.umd.js",
|
||||||
|
"luckysheet",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 라이브러리 로드 후 검증
|
||||||
|
// console.log("🔍 라이브러리 로드 후 검증 중...");
|
||||||
|
|
||||||
|
// NOTE: plugin.js 가 실제 functionlist 를 채웠으므로 별도 지연 대기 불필요
|
||||||
|
// 필수 객체 검증
|
||||||
|
const validationResults = {
|
||||||
|
jquery: !!window.$,
|
||||||
|
luckyExcel: !!window.LuckyExcel,
|
||||||
|
luckysheet: !!window.luckysheet,
|
||||||
|
luckysheetCreate: !!(
|
||||||
|
window.luckysheet &&
|
||||||
|
typeof window.luckysheet.create === "function"
|
||||||
|
),
|
||||||
|
luckysheetDestroy: !!(
|
||||||
|
window.luckysheet &&
|
||||||
|
typeof window.luckysheet.destroy === "function"
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// console.log("🔍 라이브러리 검증 결과:", validationResults);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!validationResults.luckysheet ||
|
||||||
|
!validationResults.luckysheetCreate
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Luckysheet 객체가 올바르게 초기화되지 않았습니다.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🚀 초기화 없이 바로 라이브러리 검증
|
||||||
|
console.log("🚀 라이브러리 로드 완료, 검증 중...");
|
||||||
|
|
||||||
|
setLibrariesLoaded(true);
|
||||||
|
// console.log("✅ CDN 배포판 + functionlist 초기화 완료");
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
// console.error("❌ 라이브러리 로딩 실패:", error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadSequence();
|
||||||
|
});
|
||||||
|
}, [librariesLoaded]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 참고 내용 기반: 올바른 데이터 구조로 Luckysheet 초기화
|
||||||
|
*/
|
||||||
|
const convertXLSXToLuckysheet = useCallback(
|
||||||
|
async (xlsxBuffer: ArrayBuffer, fileName: string) => {
|
||||||
|
if (!containerRef.current) {
|
||||||
|
console.warn("⚠️ 컨테이너가 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsConverting(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// console.log(
|
||||||
|
// "🔄 참고 내용 기반: XLSX → LuckyExcel → Luckysheet 변환 시작...",
|
||||||
|
// );
|
||||||
|
|
||||||
|
// 라이브러리 로드 확인
|
||||||
|
await loadLuckysheetLibrary();
|
||||||
|
|
||||||
|
// 기존 인스턴스 정리 (참고 내용 권장사항)
|
||||||
|
// console.log("🧹 기존 Luckysheet 인스턴스 정리...");
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
window.luckysheet &&
|
||||||
|
typeof window.luckysheet.destroy === "function"
|
||||||
|
) {
|
||||||
|
window.luckysheet.destroy();
|
||||||
|
// console.log("✅ 기존 인스턴스 destroy 완료");
|
||||||
|
}
|
||||||
|
} catch (destroyError) {
|
||||||
|
// console.warn("⚠️ destroy 중 오류 (무시됨):", destroyError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 컨테이너 초기화
|
||||||
|
if (containerRef.current) {
|
||||||
|
containerRef.current.innerHTML = "";
|
||||||
|
// console.log("✅ 컨테이너 초기화 완료");
|
||||||
|
}
|
||||||
|
|
||||||
|
luckysheetRef.current = null;
|
||||||
|
|
||||||
|
// console.log("🍀 LuckyExcel.transformExcelToLucky 호출...");
|
||||||
|
|
||||||
|
// fileProcessor에서 이미 변환된 데이터를 사용하여 직접 생성
|
||||||
|
try {
|
||||||
|
console.log("🍀 이미 변환된 시트 데이터 사용:", currentFile?.name);
|
||||||
|
|
||||||
|
// 기존 인스턴스 정리
|
||||||
|
if (
|
||||||
|
window.luckysheet &&
|
||||||
|
typeof window.luckysheet.destroy === "function"
|
||||||
|
) {
|
||||||
|
window.luckysheet.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// store에서 sheets 데이터를 가져와서 luckysheet 형식으로 변환
|
||||||
|
const sheets = useAppStore.getState().sheets;
|
||||||
|
const luckysheetData = sheets.map((sheet, index) => ({
|
||||||
|
name: sheet.name,
|
||||||
|
index: index.toString(),
|
||||||
|
status: 1,
|
||||||
|
order: index,
|
||||||
|
celldata: sheet.config?.data?.[0]?.celldata || [],
|
||||||
|
row: sheet.config?.data?.[0]?.row || 50,
|
||||||
|
column: sheet.config?.data?.[0]?.column || 26,
|
||||||
|
}));
|
||||||
|
|
||||||
|
window.luckysheet.create({
|
||||||
|
container: containerRef.current?.id || "luckysheet-container",
|
||||||
|
showinfobar: false,
|
||||||
|
showtoolbar: false,
|
||||||
|
data: luckysheetData,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("🎉 Luckysheet 생성 완료!");
|
||||||
|
setIsInitialized(true);
|
||||||
|
setIsConverting(false);
|
||||||
|
setError(null);
|
||||||
|
luckysheetRef.current = window.luckysheet;
|
||||||
|
} catch (createError) {
|
||||||
|
console.error("❌ Luckysheet 생성 실패:", createError);
|
||||||
|
setError(
|
||||||
|
`Luckysheet 생성 실패: ${createError instanceof Error ? createError.message : "알 수 없는 오류"}`,
|
||||||
|
);
|
||||||
|
setIsInitialized(false);
|
||||||
|
setIsConverting(false);
|
||||||
|
}
|
||||||
|
} catch (conversionError) {
|
||||||
|
console.error("❌ 변환 프로세스 실패:", conversionError);
|
||||||
|
setError(
|
||||||
|
`변환 프로세스에 실패했습니다: ${
|
||||||
|
conversionError instanceof Error
|
||||||
|
? conversionError.message
|
||||||
|
: String(conversionError)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
setIsConverting(false);
|
||||||
|
setIsInitialized(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[loadLuckysheetLibrary, setSelectedRange],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DOM 컨테이너 준비 상태 체크 - useLayoutEffect로 동기적 체크
|
||||||
|
*/
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
// console.log("🔍 useLayoutEffect: DOM 컨테이너 체크 시작...");
|
||||||
|
if (containerRef.current) {
|
||||||
|
// console.log("✅ DOM 컨테이너 준비 완료:", containerRef.current.id);
|
||||||
|
setIsContainerReady(true);
|
||||||
|
} else {
|
||||||
|
// console.warn("⚠️ useLayoutEffect: DOM 컨테이너가 아직 준비되지 않음");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DOM 컨테이너 준비 상태 재체크 (fallback)
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isContainerReady) {
|
||||||
|
// console.log("🔄 useEffect: DOM 컨테이너 재체크...");
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (containerRef.current && !isContainerReady) {
|
||||||
|
// console.log("✅ useEffect: DOM 컨테이너 지연 준비 완료");
|
||||||
|
setIsContainerReady(true);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isContainerReady]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컴포넌트 마운트 시 초기화 - 중복 실행 방지
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
currentFile?.xlsxBuffer &&
|
||||||
|
isContainerReady &&
|
||||||
|
containerRef.current &&
|
||||||
|
!isInitialized &&
|
||||||
|
!isConverting
|
||||||
|
) {
|
||||||
|
console.log("🔄 변환된 XLSX 감지, Luckysheet 초기화 시작...", {
|
||||||
|
fileName: currentFile.name,
|
||||||
|
bufferSize: currentFile.xlsxBuffer.byteLength,
|
||||||
|
containerId: containerRef.current.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 중복 실행 방지를 위해 즉시 상태 변경
|
||||||
|
setIsConverting(true);
|
||||||
|
|
||||||
|
// 변환된 XLSX ArrayBuffer를 사용하여 직접 변환
|
||||||
|
convertXLSXToLuckysheet(currentFile.xlsxBuffer, currentFile.name);
|
||||||
|
} else if (currentFile && !currentFile.xlsxBuffer) {
|
||||||
|
setError("파일 변환 데이터가 없습니다. 파일을 다시 업로드해주세요.");
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
currentFile?.xlsxBuffer,
|
||||||
|
currentFile?.name,
|
||||||
|
isContainerReady,
|
||||||
|
isInitialized,
|
||||||
|
isConverting,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컴포넌트 언마운트 시 정리
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (luckysheetRef.current && window.luckysheet) {
|
||||||
|
// console.log("🧹 컴포넌트 언마운트: Luckysheet 정리 중...");
|
||||||
|
try {
|
||||||
|
window.luckysheet.destroy();
|
||||||
|
} catch (error) {
|
||||||
|
// console.warn("⚠️ Luckysheet 정리 중 오류:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 윈도우 리사이즈 처리
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
if (luckysheetRef.current && window.luckysheet) {
|
||||||
|
try {
|
||||||
|
window.luckysheet.resize();
|
||||||
|
} catch (error) {
|
||||||
|
// console.warn("⚠️ Luckysheet 리사이즈 중 오류:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
return () => window.removeEventListener("resize", handleResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`w-full h-full min-h-[70vh] ${className || ""}`}
|
||||||
|
style={{ position: "relative" }}
|
||||||
|
>
|
||||||
|
{/* Luckysheet 컨테이너 - 항상 렌더링 */}
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
id="luckysheet-container"
|
||||||
|
className="w-full h-full"
|
||||||
|
style={{
|
||||||
|
minHeight: "70vh",
|
||||||
|
border: "1px solid #e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 에러 상태 오버레이 */}
|
||||||
|
{error && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<div className="text-center p-6">
|
||||||
|
<div className="text-red-600 text-lg font-semibold mb-2">
|
||||||
|
시트 로드 오류
|
||||||
|
</div>
|
||||||
|
<div className="text-red-500 text-sm mb-4">{error}</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setError(null);
|
||||||
|
setIsInitialized(false);
|
||||||
|
setIsConverting(false);
|
||||||
|
if (currentFile?.xlsxBuffer) {
|
||||||
|
convertXLSXToLuckysheet(
|
||||||
|
currentFile.xlsxBuffer,
|
||||||
|
currentFile.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="mt-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
|
||||||
|
>
|
||||||
|
다시 시도
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 로딩 상태 오버레이 */}
|
||||||
|
{!error &&
|
||||||
|
(isConverting || !isInitialized) &&
|
||||||
|
currentFile?.xlsxBuffer && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<div className="text-center p-6">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||||
|
<div className="text-blue-600 text-lg font-semibold mb-2">
|
||||||
|
{isConverting ? "XLSX 변환 중..." : "시트 초기화 중..."}
|
||||||
|
</div>
|
||||||
|
<div className="text-blue-500 text-sm">
|
||||||
|
{isConverting
|
||||||
|
? "변환된 XLSX를 Luckysheet로 처리하고 있습니다."
|
||||||
|
: "잠시만 기다려주세요."}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 데이터 없음 상태 오버레이 */}
|
||||||
|
{!error && !currentFile?.xlsxBuffer && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-gray-50 border border-gray-200 rounded-lg">
|
||||||
|
<div className="text-center p-6">
|
||||||
|
<div className="text-gray-500 text-lg font-semibold mb-2">
|
||||||
|
표시할 시트가 없습니다
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-400 text-sm">
|
||||||
|
Excel 파일을 업로드해주세요.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 시트 정보 표시 (개발용) */}
|
||||||
|
{process.env.NODE_ENV === "development" && (
|
||||||
|
<div className="absolute top-2 right-2 bg-black bg-opacity-75 text-white text-xs p-2 rounded z-10">
|
||||||
|
<div>파일: {currentFile?.name}</div>
|
||||||
|
<div>
|
||||||
|
XLSX 버퍼:{" "}
|
||||||
|
{currentFile?.xlsxBuffer
|
||||||
|
? `${currentFile.xlsxBuffer.byteLength} bytes`
|
||||||
|
: "없음"}
|
||||||
|
</div>
|
||||||
|
<div>변환 중: {isConverting ? "예" : "아니오"}</div>
|
||||||
|
<div>초기화: {isInitialized ? "완료" : "대기"}</div>
|
||||||
|
<div>컨테이너 준비: {isContainerReady ? "완료" : "대기"}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
838
src/components/sheet/SheetViewer.tsx.bak
Normal file
838
src/components/sheet/SheetViewer.tsx.bak
Normal file
@@ -0,0 +1,838 @@
|
|||||||
|
import React, {
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { useAppStore } from "../../stores/useAppStore";
|
||||||
|
import type { SheetData } from "../../types/sheet";
|
||||||
|
|
||||||
|
// Window 타입 확장
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
luckysheet: any;
|
||||||
|
LuckyExcel: any;
|
||||||
|
$: any; // jQuery
|
||||||
|
Store: any; // Luckysheet Store
|
||||||
|
luckysheet_function: any; // Luckysheet function list
|
||||||
|
functionlist: any[]; // 글로벌 functionlist
|
||||||
|
luckysheetConfigsetting: any; // Luckysheet 설정 객체
|
||||||
|
luckysheetPostil: any; // Luckysheet 포스틸 객체
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SheetViewerProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Luckysheet 시트 뷰어 컴포넌트
|
||||||
|
* - 참고 내용 기반: 완전한 라이브러리 로딩 순서 적용
|
||||||
|
* - functionlist 오류 방지를 위한 완전한 초기화
|
||||||
|
* - 필수 플러그인과 CSS 포함
|
||||||
|
*/
|
||||||
|
export function SheetViewer({ className }: SheetViewerProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const luckysheetRef = useRef<any>(null);
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
const [isConverting, setIsConverting] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isContainerReady, setIsContainerReady] = useState(false);
|
||||||
|
const [librariesLoaded, setLibrariesLoaded] = useState(false);
|
||||||
|
|
||||||
|
// 스토어에서 시트 데이터 가져오기
|
||||||
|
const { sheets, activeSheetId, currentFile, setSelectedRange } =
|
||||||
|
useAppStore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CDN 배포판 + functionlist 직접 초기화 방식
|
||||||
|
*/
|
||||||
|
const loadLuckysheetLibrary = useCallback((): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 이미 로드된 경우
|
||||||
|
if (
|
||||||
|
window.luckysheet &&
|
||||||
|
window.LuckyExcel &&
|
||||||
|
window.$ &&
|
||||||
|
librariesLoaded
|
||||||
|
) {
|
||||||
|
console.log("📦 모든 라이브러리가 이미 로드됨");
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log("📦 CDN 배포판 + functionlist 직접 초기화 방식...");
|
||||||
|
|
||||||
|
const loadResource = (
|
||||||
|
type: "css" | "js",
|
||||||
|
src: string,
|
||||||
|
id: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
return new Promise((resourceResolve, resourceReject) => {
|
||||||
|
// 이미 로드된 리소스 체크
|
||||||
|
if (document.querySelector(`[data-luckysheet-id="${id}"]`)) {
|
||||||
|
// console.log(`📦 ${id} 이미 로드됨`);
|
||||||
|
resourceResolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "css") {
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.href = src;
|
||||||
|
link.setAttribute("data-luckysheet-id", id);
|
||||||
|
link.onload = () => {
|
||||||
|
// console.log(`✅ ${id} CSS 로드 완료`);
|
||||||
|
resourceResolve();
|
||||||
|
};
|
||||||
|
link.onerror = (error) => {
|
||||||
|
// console.error(`❌ ${id} CSS 로드 실패:`, error);
|
||||||
|
resourceReject(new Error(`${id} CSS 로드 실패`));
|
||||||
|
};
|
||||||
|
document.head.appendChild(link);
|
||||||
|
} else {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = src;
|
||||||
|
script.setAttribute("data-luckysheet-id", id);
|
||||||
|
script.onload = () => {
|
||||||
|
// console.log(`✅ ${id} JS 로드 완료`);
|
||||||
|
resourceResolve();
|
||||||
|
};
|
||||||
|
script.onerror = (error) => {
|
||||||
|
// console.error(`❌ ${id} JS 로드 실패:`, error);
|
||||||
|
resourceReject(new Error(`${id} JS 로드 실패`));
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// CDN 배포판 로딩 + functionlist 직접 초기화
|
||||||
|
const loadSequence = async () => {
|
||||||
|
try {
|
||||||
|
// 1. jQuery (Luckysheet 의존성)
|
||||||
|
if (!window.$) {
|
||||||
|
await loadResource(
|
||||||
|
"js",
|
||||||
|
"https://code.jquery.com/jquery-3.6.0.min.js",
|
||||||
|
"jquery",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. CSS 로드 (빌드된 파일들)
|
||||||
|
await loadResource(
|
||||||
|
"css",
|
||||||
|
"/luckysheet/dist/plugins/css/pluginsCss.css",
|
||||||
|
"plugins-css",
|
||||||
|
);
|
||||||
|
await loadResource(
|
||||||
|
"css",
|
||||||
|
"/luckysheet/dist/plugins/plugins.css",
|
||||||
|
"plugins-main-css",
|
||||||
|
);
|
||||||
|
await loadResource(
|
||||||
|
"css",
|
||||||
|
"/luckysheet/dist/css/luckysheet.css",
|
||||||
|
"luckysheet-css",
|
||||||
|
);
|
||||||
|
await loadResource(
|
||||||
|
"css",
|
||||||
|
"/luckysheet/dist/assets/iconfont/iconfont.css",
|
||||||
|
"iconfont-css",
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Plugin JS 먼저 로드 (functionlist 초기화 우선)
|
||||||
|
await loadResource(
|
||||||
|
"js",
|
||||||
|
"/luckysheet/dist/plugins/js/plugin.js",
|
||||||
|
"plugin-js",
|
||||||
|
);
|
||||||
|
|
||||||
|
// 👉 plugin.js 로드 후 실제 functionlist 가 채워졌는지 polling 으로 확인 (최대 3초)
|
||||||
|
const waitForFunctionlistReady = (
|
||||||
|
timeout = 3000,
|
||||||
|
interval = 50,
|
||||||
|
): Promise<void> => {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
let waited = 0;
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
if (window.Store?.functionlist?.length) {
|
||||||
|
clearInterval(timer);
|
||||||
|
res();
|
||||||
|
} else if ((waited += interval) >= timeout) {
|
||||||
|
clearInterval(timer);
|
||||||
|
rej(new Error("functionlist 초기화 시간 초과"));
|
||||||
|
}
|
||||||
|
}, interval);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
await waitForFunctionlistReady();
|
||||||
|
|
||||||
|
// 4. LuckyExcel (Excel 파일 처리용)
|
||||||
|
if (!window.LuckyExcel) {
|
||||||
|
await loadResource(
|
||||||
|
"js",
|
||||||
|
"/luckysheet/dist/luckyexcel.umd.js",
|
||||||
|
"luckyexcel",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Luckysheet 메인 (functionlist 준비 후)
|
||||||
|
if (!window.luckysheet) {
|
||||||
|
await loadResource(
|
||||||
|
"js",
|
||||||
|
"/luckysheet/dist/luckysheet.umd.js",
|
||||||
|
"luckysheet",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 라이브러리 로드 후 검증
|
||||||
|
// console.log("🔍 라이브러리 로드 후 검증 중...");
|
||||||
|
|
||||||
|
// NOTE: plugin.js 가 실제 functionlist 를 채웠으므로 별도 지연 대기 불필요
|
||||||
|
// 필수 객체 검증
|
||||||
|
const validationResults = {
|
||||||
|
jquery: !!window.$,
|
||||||
|
luckyExcel: !!window.LuckyExcel,
|
||||||
|
luckysheet: !!window.luckysheet,
|
||||||
|
luckysheetCreate: !!(
|
||||||
|
window.luckysheet &&
|
||||||
|
typeof window.luckysheet.create === "function"
|
||||||
|
),
|
||||||
|
luckysheetDestroy: !!(
|
||||||
|
window.luckysheet &&
|
||||||
|
typeof window.luckysheet.destroy === "function"
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// console.log("🔍 라이브러리 검증 결과:", validationResults);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!validationResults.luckysheet ||
|
||||||
|
!validationResults.luckysheetCreate
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Luckysheet 객체가 올바르게 초기화되지 않았습니다.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔧 강력한 functionlist 초기화 (메모리 해결책 적용)
|
||||||
|
// console.log("🔧 강력한 functionlist 및 모든 필수 객체 초기화 중...");
|
||||||
|
try {
|
||||||
|
// 1. Store 객체 강제 생성
|
||||||
|
if (!window.Store) {
|
||||||
|
window.Store = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. functionlist 다중 레벨 초기화
|
||||||
|
if (!window.Store.functionlist) {
|
||||||
|
window.Store.functionlist = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. luckysheet_function 다중 레벨 초기화
|
||||||
|
if (!window.luckysheet_function) {
|
||||||
|
window.luckysheet_function = {};
|
||||||
|
}
|
||||||
|
if (!window.Store.luckysheet_function) {
|
||||||
|
window.Store.luckysheet_function = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Luckysheet 내부에서 사용하는 추가 functionlist 객체들 초기화
|
||||||
|
if (window.luckysheet && !window.luckysheet.functionlist) {
|
||||||
|
window.luckysheet.functionlist = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 글로벌 functionlist 초기화 (다양한 참조 경로 대응)
|
||||||
|
if (!window.functionlist) {
|
||||||
|
window.functionlist = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Store 내부 구조 완전 초기화
|
||||||
|
if (!window.Store.config) {
|
||||||
|
window.Store.config = {};
|
||||||
|
}
|
||||||
|
if (!window.Store.luckysheetfile) {
|
||||||
|
window.Store.luckysheetfile = [];
|
||||||
|
}
|
||||||
|
if (!window.Store.currentSheetIndex) {
|
||||||
|
window.Store.currentSheetIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Luckysheet 모듈별 초기화 확인
|
||||||
|
if (window.luckysheet) {
|
||||||
|
// 함수 관련 모듈 초기화
|
||||||
|
if (!window.luckysheet.formula) {
|
||||||
|
window.luckysheet.formula = {};
|
||||||
|
}
|
||||||
|
if (!window.luckysheet.formulaCache) {
|
||||||
|
window.luckysheet.formulaCache = {};
|
||||||
|
}
|
||||||
|
if (!window.luckysheet.formulaObjects) {
|
||||||
|
window.luckysheet.formulaObjects = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log("✅ 강력한 functionlist 및 모든 필수 객체 초기화 완료");
|
||||||
|
} catch (functionError) {
|
||||||
|
// console.warn(
|
||||||
|
// "⚠️ 강력한 functionlist 초기화 중 오류 (무시됨):",
|
||||||
|
// functionError,
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
setLibrariesLoaded(true);
|
||||||
|
// console.log("✅ CDN 배포판 + functionlist 초기화 완료");
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
// console.error("❌ 라이브러리 로딩 실패:", error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadSequence();
|
||||||
|
});
|
||||||
|
}, [librariesLoaded]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 참고 내용 기반: 올바른 데이터 구조로 Luckysheet 초기화
|
||||||
|
*/
|
||||||
|
const convertXLSXToLuckysheet = useCallback(
|
||||||
|
async (xlsxBuffer: ArrayBuffer, fileName: string) => {
|
||||||
|
if (!containerRef.current) {
|
||||||
|
console.warn("⚠️ 컨테이너가 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsConverting(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// console.log(
|
||||||
|
// "🔄 참고 내용 기반: XLSX → LuckyExcel → Luckysheet 변환 시작...",
|
||||||
|
// );
|
||||||
|
|
||||||
|
// 라이브러리 로드 확인
|
||||||
|
await loadLuckysheetLibrary();
|
||||||
|
|
||||||
|
// 기존 인스턴스 정리 (참고 내용 권장사항)
|
||||||
|
// console.log("🧹 기존 Luckysheet 인스턴스 정리...");
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
window.luckysheet &&
|
||||||
|
typeof window.luckysheet.destroy === "function"
|
||||||
|
) {
|
||||||
|
window.luckysheet.destroy();
|
||||||
|
// console.log("✅ 기존 인스턴스 destroy 완료");
|
||||||
|
}
|
||||||
|
} catch (destroyError) {
|
||||||
|
// console.warn("⚠️ destroy 중 오류 (무시됨):", destroyError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 컨테이너 초기화
|
||||||
|
if (containerRef.current) {
|
||||||
|
containerRef.current.innerHTML = "";
|
||||||
|
// console.log("✅ 컨테이너 초기화 완료");
|
||||||
|
}
|
||||||
|
|
||||||
|
luckysheetRef.current = null;
|
||||||
|
|
||||||
|
// console.log("🍀 LuckyExcel.transformExcelToLucky 호출...");
|
||||||
|
|
||||||
|
// LuckyExcel 변환 (참고 내용의 블로그 포스트 방식)
|
||||||
|
window.LuckyExcel.transformExcelToLucky(
|
||||||
|
xlsxBuffer,
|
||||||
|
// 성공 콜백 - 변환 완료 후에만 Luckysheet 초기화
|
||||||
|
(exportJson: any, luckysheetfile: any) => {
|
||||||
|
try {
|
||||||
|
// console.log("✅ LuckyExcel 변환 완료:", {
|
||||||
|
// hasExportJson: !!exportJson,
|
||||||
|
// hasSheets: !!exportJson?.sheets,
|
||||||
|
// sheetsCount: exportJson?.sheets?.length || 0,
|
||||||
|
// sheetsStructure:
|
||||||
|
// exportJson?.sheets?.map((sheet: any, index: number) => ({
|
||||||
|
// index,
|
||||||
|
// name: sheet?.name,
|
||||||
|
// hasData: !!sheet?.data,
|
||||||
|
// dataLength: Array.isArray(sheet?.data)
|
||||||
|
// ? sheet.data.length
|
||||||
|
// : 0,
|
||||||
|
// })) || [],
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 공식 LuckyExcel 방식: 기본 검증만 수행 (과도한 변환 방지)
|
||||||
|
if (
|
||||||
|
!exportJson ||
|
||||||
|
!exportJson.sheets ||
|
||||||
|
!Array.isArray(exportJson.sheets)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"LuckyExcel 변환 결과가 유효하지 않습니다: sheets 배열이 없음",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportJson.sheets.length === 0) {
|
||||||
|
throw new Error("변환된 시트가 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log("✅ LuckyExcel 변환 결과 검증 완료:", {
|
||||||
|
// sheetsCount: exportJson.sheets.length,
|
||||||
|
// hasInfo: !!exportJson.info,
|
||||||
|
// infoName: exportJson.info?.name,
|
||||||
|
// infoCreator: exportJson.info?.creator,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// console.log(
|
||||||
|
// "🎯 functionlist 초기화 완료: Luckysheet 초기화 시작...",
|
||||||
|
// );
|
||||||
|
|
||||||
|
// 메모리 해결책: 최종 완전한 functionlist 및 모든 Luckysheet 내부 객체 초기화
|
||||||
|
try {
|
||||||
|
// Level 1: Store 객체 완전 초기화
|
||||||
|
if (!window.Store) window.Store = {};
|
||||||
|
if (!window.Store.functionlist) window.Store.functionlist = [];
|
||||||
|
if (!window.Store.luckysheet_function)
|
||||||
|
window.Store.luckysheet_function = {};
|
||||||
|
if (!window.Store.config) window.Store.config = {};
|
||||||
|
if (!window.Store.luckysheetfile)
|
||||||
|
window.Store.luckysheetfile = [];
|
||||||
|
|
||||||
|
// Level 2: 글로벌 function 객체들 완전 초기화
|
||||||
|
if (!window.luckysheet_function)
|
||||||
|
window.luckysheet_function = {};
|
||||||
|
if (!window.functionlist) window.functionlist = [];
|
||||||
|
|
||||||
|
// Level 3: Luckysheet 내부 깊은 레벨 초기화
|
||||||
|
if (window.luckysheet) {
|
||||||
|
// 함수 관련 깊은 객체들
|
||||||
|
if (!window.luckysheet.functionlist)
|
||||||
|
window.luckysheet.functionlist = [];
|
||||||
|
if (!window.luckysheet.formula)
|
||||||
|
window.luckysheet.formula = {};
|
||||||
|
if (!window.luckysheet.formulaCache)
|
||||||
|
window.luckysheet.formulaCache = {};
|
||||||
|
if (!window.luckysheet.formulaObjects)
|
||||||
|
window.luckysheet.formulaObjects = {};
|
||||||
|
|
||||||
|
// Store 레퍼런스 초기화
|
||||||
|
if (!window.luckysheet.Store)
|
||||||
|
window.luckysheet.Store = window.Store;
|
||||||
|
if (!window.luckysheet.luckysheetfile)
|
||||||
|
window.luckysheet.luckysheetfile = [];
|
||||||
|
|
||||||
|
// 내부 모듈들 초기화
|
||||||
|
if (!window.luckysheet.menuButton)
|
||||||
|
window.luckysheet.menuButton = {};
|
||||||
|
if (!window.luckysheet.server) window.luckysheet.server = {};
|
||||||
|
if (!window.luckysheet.selection)
|
||||||
|
window.luckysheet.selection = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level 4: 추가적인 깊은 레벨 객체들 (Luckysheet 내부에서 사용할 수 있는)
|
||||||
|
if (!window.luckysheetConfigsetting)
|
||||||
|
window.luckysheetConfigsetting = {};
|
||||||
|
if (!window.luckysheetPostil) window.luckysheetPostil = {};
|
||||||
|
if (!window.Store.visibledatarow)
|
||||||
|
window.Store.visibledatarow = [];
|
||||||
|
if (!window.Store.visibledatacolumn)
|
||||||
|
window.Store.visibledatacolumn = [];
|
||||||
|
if (!window.Store.defaultcollen)
|
||||||
|
window.Store.defaultcollen = 73;
|
||||||
|
if (!window.Store.defaultrowlen)
|
||||||
|
window.Store.defaultrowlen = 19;
|
||||||
|
|
||||||
|
console.log("✅ 완전한 Luckysheet 내부 객체 초기화 완료");
|
||||||
|
|
||||||
|
// 극한의 방법: Luckysheet 내부 코드 직접 패치 (임시)
|
||||||
|
if (
|
||||||
|
window.luckysheet &&
|
||||||
|
typeof window.luckysheet.create === "function"
|
||||||
|
) {
|
||||||
|
// Luckysheet 내부에서 사용하는 모든 가능한 functionlist 경로 강제 생성
|
||||||
|
const originalCreate = window.luckysheet.create;
|
||||||
|
window.luckysheet.create = function (options: any) {
|
||||||
|
try {
|
||||||
|
// 생성 직전 모든 functionlist 경로 재검증
|
||||||
|
if (!window.Store) window.Store = {};
|
||||||
|
if (!window.Store.functionlist)
|
||||||
|
window.Store.functionlist = [];
|
||||||
|
if (!this.functionlist) this.functionlist = [];
|
||||||
|
if (!this.Store) this.Store = window.Store;
|
||||||
|
if (!this.Store.functionlist)
|
||||||
|
this.Store.functionlist = [];
|
||||||
|
|
||||||
|
// 원본 함수 호출
|
||||||
|
return originalCreate.call(this, options);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Luckysheet create 패치된 함수에서 오류:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
console.log("🔧 Luckysheet.create 함수 패치 완료");
|
||||||
|
}
|
||||||
|
} catch (finalInitError) {
|
||||||
|
console.warn(
|
||||||
|
"⚠️ 완전한 functionlist 초기화 중 오류:",
|
||||||
|
finalInitError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 공식 LuckyExcel 방식: exportJson.sheets를 직접 사용
|
||||||
|
const luckysheetOptions = {
|
||||||
|
container: containerRef.current?.id || "luckysheet-container",
|
||||||
|
title: exportJson.info?.name || fileName || "Sheet Easy AI",
|
||||||
|
lang: "ko",
|
||||||
|
data: exportJson.sheets, // 공식 방식: LuckyExcel 결과를 직접 사용
|
||||||
|
|
||||||
|
// userInfo도 공식 예시대로 설정
|
||||||
|
userInfo: exportJson.info?.creator || "Sheet Easy AI User",
|
||||||
|
|
||||||
|
// UI 설정 (모든 기능 활성화)
|
||||||
|
showinfobar: false,
|
||||||
|
showtoolbar: true, // 툴바 활성화
|
||||||
|
showsheetbar: true,
|
||||||
|
showstatisticBar: false,
|
||||||
|
showConfigWindowResize: true,
|
||||||
|
|
||||||
|
// 편집 기능 활성화
|
||||||
|
allowCopy: true,
|
||||||
|
allowEdit: true,
|
||||||
|
enableAddRow: true, // 행/열 추가 활성화
|
||||||
|
enableAddCol: true,
|
||||||
|
|
||||||
|
// 함수 기능 활성화
|
||||||
|
allowUpdate: true,
|
||||||
|
enableAddBackTop: true,
|
||||||
|
showFormulaBar: true, // 수식바 활성화
|
||||||
|
|
||||||
|
// 이벤트 핸들러 (모든 기능)
|
||||||
|
hook: {
|
||||||
|
cellClick: (cell: any, position: any, sheetFile: any) => {
|
||||||
|
// console.log("🖱️ 셀 클릭:", { cell, position, sheetFile });
|
||||||
|
if (
|
||||||
|
position &&
|
||||||
|
typeof position.r === "number" &&
|
||||||
|
typeof position.c === "number"
|
||||||
|
) {
|
||||||
|
setSelectedRange({
|
||||||
|
range: {
|
||||||
|
startRow: position.r,
|
||||||
|
startCol: position.c,
|
||||||
|
endRow: position.r,
|
||||||
|
endCol: position.c,
|
||||||
|
},
|
||||||
|
sheetId: sheetFile?.index || "0",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sheetActivate: (
|
||||||
|
index: number,
|
||||||
|
isPivotInitial: boolean,
|
||||||
|
isInitialLoad: boolean,
|
||||||
|
) => {
|
||||||
|
// console.log("📋 시트 활성화:", {
|
||||||
|
// index,
|
||||||
|
// isPivotInitial,
|
||||||
|
// isInitialLoad,
|
||||||
|
// });
|
||||||
|
if (exportJson.sheets[index]) {
|
||||||
|
useAppStore.getState().setActiveSheetId(`sheet_${index}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updated: (operate: any) => {
|
||||||
|
// console.log("🔄 시트 업데이트:", operate);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// === 상세 디버깅 정보 ===
|
||||||
|
console.log("=== LuckyExcel → Luckysheet 디버깅 정보 ===");
|
||||||
|
console.log("📋 exportJson 구조:", {
|
||||||
|
hasExportJson: !!exportJson,
|
||||||
|
hasInfo: !!exportJson?.info,
|
||||||
|
hasSheets: !!exportJson?.sheets,
|
||||||
|
sheetsCount: exportJson?.sheets?.length,
|
||||||
|
infoKeys: Object.keys(exportJson?.info || {}),
|
||||||
|
firstSheetKeys: Object.keys(exportJson?.sheets?.[0] || {}),
|
||||||
|
});
|
||||||
|
console.log("🔧 Functionlist 상태 상세 검사:", {
|
||||||
|
windowStore: !!window.Store,
|
||||||
|
storeFunctionlist: !!window.Store?.functionlist,
|
||||||
|
storeFunctionlistLength: window.Store?.functionlist?.length,
|
||||||
|
luckysheetFunction: !!window.luckysheet_function,
|
||||||
|
globalFunctionlist: !!window.functionlist,
|
||||||
|
globalFunctionlistLength: window.functionlist?.length,
|
||||||
|
luckysheetStoreFunctionlist:
|
||||||
|
!!window.luckysheet?.Store?.functionlist,
|
||||||
|
luckysheetOwnFunctionlist: !!window.luckysheet?.functionlist,
|
||||||
|
allWindowKeys: Object.keys(window).filter((key) =>
|
||||||
|
key.includes("function"),
|
||||||
|
),
|
||||||
|
allStoreKeys: Object.keys(window.Store || {}),
|
||||||
|
allLuckysheetKeys: Object.keys(window.luckysheet || {}).slice(
|
||||||
|
0,
|
||||||
|
10,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
console.log("🎯 Luckysheet 객체:", {
|
||||||
|
hasLuckysheet: !!window.luckysheet,
|
||||||
|
hasCreate: typeof window.luckysheet?.create === "function",
|
||||||
|
methodsCount: Object.keys(window.luckysheet || {}).length,
|
||||||
|
});
|
||||||
|
console.log("=== 디버깅 정보 끝 ===");
|
||||||
|
|
||||||
|
// Luckysheet 생성
|
||||||
|
window.luckysheet.create(luckysheetOptions);
|
||||||
|
|
||||||
|
luckysheetRef.current = window.luckysheet;
|
||||||
|
setIsInitialized(true);
|
||||||
|
setIsConverting(false);
|
||||||
|
|
||||||
|
// console.log(
|
||||||
|
// "✅ functionlist 초기화 완료: Luckysheet 초기화 완료!",
|
||||||
|
// );
|
||||||
|
} catch (initError) {
|
||||||
|
console.error("❌ Luckysheet 초기화 실패:", initError);
|
||||||
|
setError(
|
||||||
|
`시트 초기화에 실패했습니다: ${
|
||||||
|
initError instanceof Error
|
||||||
|
? initError.message
|
||||||
|
: String(initError)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
setIsInitialized(false);
|
||||||
|
setIsConverting(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 오류 콜백
|
||||||
|
(error: any) => {
|
||||||
|
console.error("❌ LuckyExcel 변환 실패:", error);
|
||||||
|
setError(
|
||||||
|
`XLSX 변환에 실패했습니다: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
setIsConverting(false);
|
||||||
|
setIsInitialized(false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (conversionError) {
|
||||||
|
console.error("❌ 변환 프로세스 실패:", conversionError);
|
||||||
|
setError(
|
||||||
|
`변환 프로세스에 실패했습니다: ${
|
||||||
|
conversionError instanceof Error
|
||||||
|
? conversionError.message
|
||||||
|
: String(conversionError)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
setIsConverting(false);
|
||||||
|
setIsInitialized(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[loadLuckysheetLibrary, setSelectedRange],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DOM 컨테이너 준비 상태 체크 - useLayoutEffect로 동기적 체크
|
||||||
|
*/
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
// console.log("🔍 useLayoutEffect: DOM 컨테이너 체크 시작...");
|
||||||
|
if (containerRef.current) {
|
||||||
|
// console.log("✅ DOM 컨테이너 준비 완료:", containerRef.current.id);
|
||||||
|
setIsContainerReady(true);
|
||||||
|
} else {
|
||||||
|
// console.warn("⚠️ useLayoutEffect: DOM 컨테이너가 아직 준비되지 않음");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DOM 컨테이너 준비 상태 재체크 (fallback)
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isContainerReady) {
|
||||||
|
// console.log("🔄 useEffect: DOM 컨테이너 재체크...");
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (containerRef.current && !isContainerReady) {
|
||||||
|
// console.log("✅ useEffect: DOM 컨테이너 지연 준비 완료");
|
||||||
|
setIsContainerReady(true);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isContainerReady]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컴포넌트 마운트 시 초기화 - 블로그 포스트 방식 적용
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
// console.log("🔍 useEffect 실행 조건 체크:", {
|
||||||
|
// hasCurrentFile: !!currentFile,
|
||||||
|
// hasXlsxBuffer: !!currentFile?.xlsxBuffer,
|
||||||
|
// hasContainer: !!containerRef.current,
|
||||||
|
// isContainerReady,
|
||||||
|
// currentFileName: currentFile?.name,
|
||||||
|
// bufferSize: currentFile?.xlsxBuffer?.byteLength,
|
||||||
|
// });
|
||||||
|
|
||||||
|
if (currentFile?.xlsxBuffer && isContainerReady && containerRef.current) {
|
||||||
|
// console.log("🔄 변환된 XLSX 감지, LuckyExcel → Luckysheet 시작...", {
|
||||||
|
// fileName: currentFile.name,
|
||||||
|
// bufferSize: currentFile.xlsxBuffer.byteLength,
|
||||||
|
// containerId: containerRef.current.id,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 변환된 XLSX ArrayBuffer를 사용하여 직접 변환 (블로그 포스트 방식)
|
||||||
|
convertXLSXToLuckysheet(currentFile.xlsxBuffer, currentFile.name);
|
||||||
|
} else if (currentFile && !currentFile.xlsxBuffer) {
|
||||||
|
// console.warn(
|
||||||
|
// "⚠️ currentFile은 있지만 xlsxBuffer가 없습니다:",
|
||||||
|
// currentFile,
|
||||||
|
// );
|
||||||
|
setError("파일 변환 데이터가 없습니다. 파일을 다시 업로드해주세요.");
|
||||||
|
} else if (!currentFile) {
|
||||||
|
// console.log("ℹ️ currentFile이 없습니다. 파일을 업로드해주세요.");
|
||||||
|
} else if (currentFile?.xlsxBuffer && !isContainerReady) {
|
||||||
|
// console.log("⏳ DOM 컨테이너 준비 대기 중...");
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
currentFile?.xlsxBuffer,
|
||||||
|
currentFile?.name,
|
||||||
|
isContainerReady,
|
||||||
|
convertXLSXToLuckysheet,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컴포넌트 언마운트 시 정리
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (luckysheetRef.current && window.luckysheet) {
|
||||||
|
// console.log("🧹 컴포넌트 언마운트: Luckysheet 정리 중...");
|
||||||
|
try {
|
||||||
|
window.luckysheet.destroy();
|
||||||
|
} catch (error) {
|
||||||
|
// console.warn("⚠️ Luckysheet 정리 중 오류:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 윈도우 리사이즈 처리
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
if (luckysheetRef.current && window.luckysheet) {
|
||||||
|
try {
|
||||||
|
window.luckysheet.resize();
|
||||||
|
} catch (error) {
|
||||||
|
// console.warn("⚠️ Luckysheet 리사이즈 중 오류:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
return () => window.removeEventListener("resize", handleResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`w-full h-full min-h-[70vh] ${className || ""}`}
|
||||||
|
style={{ position: "relative" }}
|
||||||
|
>
|
||||||
|
{/* Luckysheet 컨테이너 - 항상 렌더링 */}
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
id="luckysheet-container"
|
||||||
|
className="w-full h-full"
|
||||||
|
style={{
|
||||||
|
minHeight: "70vh",
|
||||||
|
border: "1px solid #e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 에러 상태 오버레이 */}
|
||||||
|
{error && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<div className="text-center p-6">
|
||||||
|
<div className="text-red-600 text-lg font-semibold mb-2">
|
||||||
|
시트 로드 오류
|
||||||
|
</div>
|
||||||
|
<div className="text-red-500 text-sm mb-4">{error}</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setError(null);
|
||||||
|
setIsInitialized(false);
|
||||||
|
setIsConverting(false);
|
||||||
|
if (currentFile?.xlsxBuffer) {
|
||||||
|
convertXLSXToLuckysheet(
|
||||||
|
currentFile.xlsxBuffer,
|
||||||
|
currentFile.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="mt-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
|
||||||
|
>
|
||||||
|
다시 시도
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 로딩 상태 오버레이 */}
|
||||||
|
{!error &&
|
||||||
|
(isConverting || !isInitialized) &&
|
||||||
|
currentFile?.xlsxBuffer && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<div className="text-center p-6">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||||
|
<div className="text-blue-600 text-lg font-semibold mb-2">
|
||||||
|
{isConverting ? "XLSX 변환 중..." : "시트 초기화 중..."}
|
||||||
|
</div>
|
||||||
|
<div className="text-blue-500 text-sm">
|
||||||
|
{isConverting
|
||||||
|
? "변환된 XLSX를 Luckysheet로 처리하고 있습니다."
|
||||||
|
: "잠시만 기다려주세요."}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 데이터 없음 상태 오버레이 */}
|
||||||
|
{!error && !currentFile?.xlsxBuffer && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-gray-50 border border-gray-200 rounded-lg">
|
||||||
|
<div className="text-center p-6">
|
||||||
|
<div className="text-gray-500 text-lg font-semibold mb-2">
|
||||||
|
표시할 시트가 없습니다
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-400 text-sm">
|
||||||
|
Excel 파일을 업로드해주세요.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 시트 정보 표시 (개발용) */}
|
||||||
|
{process.env.NODE_ENV === "development" && (
|
||||||
|
<div className="absolute top-2 right-2 bg-black bg-opacity-75 text-white text-xs p-2 rounded z-10">
|
||||||
|
<div>파일: {currentFile?.name}</div>
|
||||||
|
<div>
|
||||||
|
XLSX 버퍼:{" "}
|
||||||
|
{currentFile?.xlsxBuffer
|
||||||
|
? `${currentFile.xlsxBuffer.byteLength} bytes`
|
||||||
|
: "없음"}
|
||||||
|
</div>
|
||||||
|
<div>변환 중: {isConverting ? "예" : "아니오"}</div>
|
||||||
|
<div>초기화: {isInitialized ? "완료" : "대기"}</div>
|
||||||
|
<div>컨테이너 준비: {isContainerReady ? "완료" : "대기"}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
402
src/components/sheet/__tests__/SheetViewer.test.tsx
Normal file
402
src/components/sheet/__tests__/SheetViewer.test.tsx
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import "@testing-library/jest-dom";
|
||||||
|
import { vi } from "vitest";
|
||||||
|
import { SheetViewer } from "../SheetViewer";
|
||||||
|
import { useAppStore } from "../../../stores/useAppStore";
|
||||||
|
import type { SheetData } from "../../../types/sheet";
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock("../../../stores/useAppStore");
|
||||||
|
|
||||||
|
// Luckysheet 모킹
|
||||||
|
const mockLuckysheet = {
|
||||||
|
create: vi.fn(),
|
||||||
|
destroy: vi.fn(),
|
||||||
|
resize: vi.fn(),
|
||||||
|
getSheet: vi.fn(),
|
||||||
|
getAllSheets: vi.fn(),
|
||||||
|
setActiveSheet: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Window.luckysheet 모킹
|
||||||
|
Object.defineProperty(window, "luckysheet", {
|
||||||
|
value: mockLuckysheet,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// useAppStore 모킹 타입
|
||||||
|
const mockUseAppStore = vi.mocked(useAppStore);
|
||||||
|
|
||||||
|
// 기본 스토어 상태
|
||||||
|
const defaultStoreState = {
|
||||||
|
sheets: [],
|
||||||
|
activeSheetId: null,
|
||||||
|
currentFile: null,
|
||||||
|
setSelectedRange: vi.fn(),
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
setLoading: vi.fn(),
|
||||||
|
setError: vi.fn(),
|
||||||
|
uploadFile: vi.fn(),
|
||||||
|
clearFileUploadErrors: vi.fn(),
|
||||||
|
resetApp: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 테스트용 시트 데이터
|
||||||
|
const mockSheetData: SheetData[] = [
|
||||||
|
{
|
||||||
|
id: "sheet_0",
|
||||||
|
name: "Sheet1",
|
||||||
|
data: [
|
||||||
|
["A1", "B1", "C1"],
|
||||||
|
["A2", "B2", "C2"],
|
||||||
|
],
|
||||||
|
config: {
|
||||||
|
container: "luckysheet_0",
|
||||||
|
title: "Sheet1",
|
||||||
|
lang: "ko",
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: "Sheet1",
|
||||||
|
index: "0",
|
||||||
|
celldata: [
|
||||||
|
{
|
||||||
|
r: 0,
|
||||||
|
c: 0,
|
||||||
|
v: { v: "A1", m: "A1", ct: { fa: "General", t: "g" } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
r: 0,
|
||||||
|
c: 1,
|
||||||
|
v: { v: "B1", m: "B1", ct: { fa: "General", t: "g" } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
status: 1,
|
||||||
|
order: 0,
|
||||||
|
row: 2,
|
||||||
|
column: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
showtoolbar: true,
|
||||||
|
showinfobar: false,
|
||||||
|
showsheetbar: true,
|
||||||
|
showstatisticBar: false,
|
||||||
|
allowCopy: true,
|
||||||
|
allowEdit: true,
|
||||||
|
enableAddRow: true,
|
||||||
|
enableAddCol: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("SheetViewer", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockUseAppStore.mockReturnValue(defaultStoreState);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// DOM 정리
|
||||||
|
document.head.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("초기 렌더링", () => {
|
||||||
|
it("시트 데이터가 없을 때 적절한 메시지를 표시한다", () => {
|
||||||
|
mockUseAppStore.mockReturnValue({
|
||||||
|
...defaultStoreState,
|
||||||
|
sheets: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<SheetViewer />);
|
||||||
|
|
||||||
|
expect(screen.getByText("표시할 시트가 없습니다")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("Excel 파일을 업로드해주세요."),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("시트 데이터가 있을 때 로딩 상태를 표시한다", () => {
|
||||||
|
mockUseAppStore.mockReturnValue({
|
||||||
|
...defaultStoreState,
|
||||||
|
sheets: mockSheetData,
|
||||||
|
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<SheetViewer />);
|
||||||
|
|
||||||
|
expect(screen.getByText("시트 로딩 중...")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("잠시만 기다려주세요.")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Luckysheet 컨테이너가 올바르게 렌더링된다", () => {
|
||||||
|
mockUseAppStore.mockReturnValue({
|
||||||
|
...defaultStoreState,
|
||||||
|
sheets: mockSheetData,
|
||||||
|
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<SheetViewer />);
|
||||||
|
|
||||||
|
const container = document.getElementById("luckysheet-container");
|
||||||
|
expect(container).toBeInTheDocument();
|
||||||
|
expect(container).toHaveClass("w-full", "h-full");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Luckysheet 초기화", () => {
|
||||||
|
it("시트 데이터가 변경되면 Luckysheet를 초기화한다", async () => {
|
||||||
|
mockUseAppStore.mockReturnValue({
|
||||||
|
...defaultStoreState,
|
||||||
|
sheets: mockSheetData,
|
||||||
|
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<SheetViewer />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockLuckysheet.create).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// create 호출 시 전달된 설정 확인
|
||||||
|
const createCall = mockLuckysheet.create.mock.calls[0];
|
||||||
|
expect(createCall).toBeDefined();
|
||||||
|
|
||||||
|
const config = createCall[0];
|
||||||
|
expect(config.container).toBe("luckysheet-container");
|
||||||
|
expect(config.title).toBe("test.xlsx");
|
||||||
|
expect(config.lang).toBe("ko");
|
||||||
|
expect(config.data).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("기존 Luckysheet 인스턴스가 있으면 제거한다", async () => {
|
||||||
|
mockUseAppStore.mockReturnValue({
|
||||||
|
...defaultStoreState,
|
||||||
|
sheets: mockSheetData,
|
||||||
|
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { rerender } = render(<SheetViewer />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockLuckysheet.create).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 시트 데이터 변경
|
||||||
|
const newSheetData: SheetData[] = [
|
||||||
|
{
|
||||||
|
...mockSheetData[0],
|
||||||
|
name: "NewSheet",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
mockUseAppStore.mockReturnValue({
|
||||||
|
...defaultStoreState,
|
||||||
|
sheets: newSheetData,
|
||||||
|
currentFile: { name: "new.xlsx", size: 1000, uploadedAt: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
rerender(<SheetViewer />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockLuckysheet.destroy).toHaveBeenCalled();
|
||||||
|
expect(mockLuckysheet.create).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("에러 처리", () => {
|
||||||
|
it("Luckysheet 초기화 실패 시 에러 메시지를 표시한다", async () => {
|
||||||
|
mockUseAppStore.mockReturnValue({
|
||||||
|
...defaultStoreState,
|
||||||
|
sheets: mockSheetData,
|
||||||
|
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Luckysheet.create에서 에러 발생 시뮬레이션
|
||||||
|
mockLuckysheet.create.mockImplementation(() => {
|
||||||
|
throw new Error("Luckysheet 초기화 실패");
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<SheetViewer />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("시트 로드 오류")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(/시트 초기화에 실패했습니다/),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 다시 시도 버튼 확인
|
||||||
|
const retryButton = screen.getByRole("button", { name: "다시 시도" });
|
||||||
|
expect(retryButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("다시 시도 버튼을 클릭하면 초기화를 재시도한다", async () => {
|
||||||
|
mockUseAppStore.mockReturnValue({
|
||||||
|
...defaultStoreState,
|
||||||
|
sheets: mockSheetData,
|
||||||
|
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 첫 번째 시도에서 실패
|
||||||
|
mockLuckysheet.create.mockImplementationOnce(() => {
|
||||||
|
throw new Error("첫 번째 실패");
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<SheetViewer />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("시트 로드 오류")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 두 번째 시도에서 성공하도록 설정
|
||||||
|
mockLuckysheet.create.mockImplementationOnce(() => {
|
||||||
|
// 성공
|
||||||
|
});
|
||||||
|
|
||||||
|
const retryButton = screen.getByRole("button", { name: "다시 시도" });
|
||||||
|
retryButton.click();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockLuckysheet.create).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("이벤트 핸들링", () => {
|
||||||
|
it("셀 클릭 시 선택된 범위를 스토어에 저장한다", async () => {
|
||||||
|
const mockSetSelectedRange = vi.fn();
|
||||||
|
mockUseAppStore.mockReturnValue({
|
||||||
|
...defaultStoreState,
|
||||||
|
sheets: mockSheetData,
|
||||||
|
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
|
||||||
|
setSelectedRange: mockSetSelectedRange,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<SheetViewer />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockLuckysheet.create).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// create 호출 시 전달된 hook 확인
|
||||||
|
const createCall = mockLuckysheet.create.mock.calls[0];
|
||||||
|
const config = createCall[0];
|
||||||
|
|
||||||
|
// cellClick 핸들러 시뮬레이션
|
||||||
|
const cellClickHandler = config.hook.cellClick;
|
||||||
|
expect(cellClickHandler).toBeDefined();
|
||||||
|
|
||||||
|
const mockCell = {};
|
||||||
|
const mockPosition = { r: 1, c: 2 };
|
||||||
|
const mockSheetFile = { index: "0" };
|
||||||
|
|
||||||
|
cellClickHandler(mockCell, mockPosition, mockSheetFile);
|
||||||
|
|
||||||
|
expect(mockSetSelectedRange).toHaveBeenCalledWith({
|
||||||
|
range: {
|
||||||
|
startRow: 1,
|
||||||
|
startCol: 2,
|
||||||
|
endRow: 1,
|
||||||
|
endCol: 2,
|
||||||
|
},
|
||||||
|
sheetId: "0",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("시트 활성화 시 활성 시트 ID를 업데이트한다", async () => {
|
||||||
|
mockUseAppStore.mockReturnValue({
|
||||||
|
...defaultStoreState,
|
||||||
|
sheets: mockSheetData,
|
||||||
|
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
// setActiveSheetId를 spy로 설정
|
||||||
|
const setActiveSheetIdSpy = vi.fn();
|
||||||
|
useAppStore.getState = vi.fn().mockReturnValue({
|
||||||
|
setActiveSheetId: setActiveSheetIdSpy,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<SheetViewer />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockLuckysheet.create).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// create 호출 시 전달된 hook 확인
|
||||||
|
const createCall = mockLuckysheet.create.mock.calls[0];
|
||||||
|
const config = createCall[0];
|
||||||
|
|
||||||
|
// sheetActivate 핸들러 시뮬레이션
|
||||||
|
const sheetActivateHandler = config.hook.sheetActivate;
|
||||||
|
expect(sheetActivateHandler).toBeDefined();
|
||||||
|
|
||||||
|
sheetActivateHandler(0, false, false);
|
||||||
|
|
||||||
|
expect(setActiveSheetIdSpy).toHaveBeenCalledWith("sheet_0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("컴포넌트 생명주기", () => {
|
||||||
|
it("컴포넌트 언마운트 시 Luckysheet를 정리한다", async () => {
|
||||||
|
mockUseAppStore.mockReturnValue({
|
||||||
|
...defaultStoreState,
|
||||||
|
sheets: mockSheetData,
|
||||||
|
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { unmount } = render(<SheetViewer />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockLuckysheet.create).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
|
||||||
|
expect(mockLuckysheet.destroy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("윈도우 리사이즈 시 Luckysheet 리사이즈를 호출한다", async () => {
|
||||||
|
mockUseAppStore.mockReturnValue({
|
||||||
|
...defaultStoreState,
|
||||||
|
sheets: mockSheetData,
|
||||||
|
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<SheetViewer />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockLuckysheet.create).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 윈도우 리사이즈 이벤트 시뮬레이션
|
||||||
|
window.dispatchEvent(new Event("resize"));
|
||||||
|
|
||||||
|
expect(mockLuckysheet.resize).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("개발 모드 정보", () => {
|
||||||
|
it("개발 모드에서 시트 정보를 표시한다", () => {
|
||||||
|
const originalEnv = process.env.NODE_ENV;
|
||||||
|
process.env.NODE_ENV = "development";
|
||||||
|
|
||||||
|
mockUseAppStore.mockReturnValue({
|
||||||
|
...defaultStoreState,
|
||||||
|
sheets: mockSheetData,
|
||||||
|
activeSheetId: "sheet_0",
|
||||||
|
currentFile: { name: "test.xlsx", size: 1000, uploadedAt: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<SheetViewer />);
|
||||||
|
|
||||||
|
expect(screen.getByText("시트 개수: 1")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("활성 시트: sheet_0")).toBeInTheDocument();
|
||||||
|
|
||||||
|
process.env.NODE_ENV = originalEnv;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -18,6 +18,7 @@ interface AppState {
|
|||||||
name: string;
|
name: string;
|
||||||
size: number;
|
size: number;
|
||||||
uploadedAt: Date;
|
uploadedAt: Date;
|
||||||
|
xlsxBuffer?: ArrayBuffer; // 변환된 XLSX ArrayBuffer
|
||||||
} | null;
|
} | null;
|
||||||
sheets: SheetData[];
|
sheets: SheetData[];
|
||||||
activeSheetId: string | null;
|
activeSheetId: string | null;
|
||||||
@@ -41,7 +42,12 @@ interface AppState {
|
|||||||
setAuthenticated: (authenticated: boolean) => void;
|
setAuthenticated: (authenticated: boolean) => void;
|
||||||
|
|
||||||
setCurrentFile: (
|
setCurrentFile: (
|
||||||
file: { name: string; size: number; uploadedAt: Date } | null,
|
file: {
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
uploadedAt: Date;
|
||||||
|
xlsxBuffer?: ArrayBuffer;
|
||||||
|
} | null,
|
||||||
) => void;
|
) => void;
|
||||||
setSheets: (sheets: SheetData[]) => void;
|
setSheets: (sheets: SheetData[]) => void;
|
||||||
setActiveSheetId: (sheetId: string | null) => void;
|
setActiveSheetId: (sheetId: string | null) => void;
|
||||||
@@ -126,6 +132,7 @@ export const useAppStore = create<AppState>()(
|
|||||||
name: result.fileName || "Unknown",
|
name: result.fileName || "Unknown",
|
||||||
size: result.fileSize || 0,
|
size: result.fileSize || 0,
|
||||||
uploadedAt: new Date(),
|
uploadedAt: new Date(),
|
||||||
|
xlsxBuffer: result.xlsxBuffer,
|
||||||
},
|
},
|
||||||
sheets: result.data,
|
sheets: result.data,
|
||||||
activeSheetId: result.data[0]?.id || null,
|
activeSheetId: result.data[0]?.id || null,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface SheetData {
|
|||||||
name: string;
|
name: string;
|
||||||
data: any[][]; // Luckysheet 데이터 형식
|
data: any[][]; // Luckysheet 데이터 형식
|
||||||
config?: LuckysheetConfig;
|
config?: LuckysheetConfig;
|
||||||
|
xlsxBuffer?: ArrayBuffer; // 변환된 XLSX ArrayBuffer
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LuckysheetConfig {
|
export interface LuckysheetConfig {
|
||||||
@@ -30,6 +31,8 @@ export interface FileUploadResult {
|
|||||||
error?: string;
|
error?: string;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
fileSize?: number;
|
fileSize?: number;
|
||||||
|
file?: File;
|
||||||
|
xlsxBuffer?: ArrayBuffer; // 변환된 XLSX ArrayBuffer
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExportOptions {
|
export interface ExportOptions {
|
||||||
|
|||||||
@@ -419,7 +419,9 @@ function convertSheetJSToLuckyExcel(workbook: any): SheetData[] {
|
|||||||
/**
|
/**
|
||||||
* SheetJS로 파일을 읽고 XLSX로 변환한 뒤 LuckyExcel로 처리
|
* SheetJS로 파일을 읽고 XLSX로 변환한 뒤 LuckyExcel로 처리
|
||||||
*/
|
*/
|
||||||
async function processFileWithSheetJSToXLSX(file: File): Promise<SheetData[]> {
|
async function processFileWithSheetJSToXLSX(
|
||||||
|
file: File,
|
||||||
|
): Promise<{ sheets: SheetData[]; xlsxBuffer: ArrayBuffer }> {
|
||||||
console.log("📊 SheetJS → XLSX → LuckyExcel 파이프라인 시작...");
|
console.log("📊 SheetJS → XLSX → LuckyExcel 파이프라인 시작...");
|
||||||
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
@@ -632,194 +634,206 @@ async function processFileWithSheetJSToXLSX(file: File): Promise<SheetData[]> {
|
|||||||
console.log("🚀 LuckyExcel 호출 시작...");
|
console.log("🚀 LuckyExcel 호출 시작...");
|
||||||
|
|
||||||
// Promise를 사용한 LuckyExcel 처리
|
// Promise를 사용한 LuckyExcel 처리
|
||||||
return new Promise<SheetData[]>((resolve, reject) => {
|
return new Promise<{ sheets: SheetData[]; xlsxBuffer: ArrayBuffer }>(
|
||||||
try {
|
(resolve, reject) => {
|
||||||
// LuckyExcel API는 (arrayBuffer, successCallback, errorCallback) 형태로 호출
|
try {
|
||||||
// 공식 문서: LuckyExcel.transformExcelToLucky(file, successCallback, errorCallback)
|
// LuckyExcel API는 (arrayBuffer, successCallback, errorCallback) 형태로 호출
|
||||||
(LuckyExcel as any).transformExcelToLucky(
|
// 공식 문서: LuckyExcel.transformExcelToLucky(file, successCallback, errorCallback)
|
||||||
xlsxArrayBuffer,
|
(LuckyExcel as any).transformExcelToLucky(
|
||||||
// 성공 콜백 함수 (두 번째 매개변수)
|
xlsxArrayBuffer,
|
||||||
(exportJson: any, luckysheetfile: any) => {
|
// 성공 콜백 함수 (두 번째 매개변수)
|
||||||
try {
|
(exportJson: any, luckysheetfile: any) => {
|
||||||
console.log("🍀 =================================");
|
try {
|
||||||
console.log("🍀 LuckyExcel 변환 결과 상세 정보:");
|
console.log("🍀 =================================");
|
||||||
console.log("🍀 =================================");
|
console.log("🍀 LuckyExcel 변환 결과 상세 정보:");
|
||||||
console.log("🍀 원본 파일명:", xlsxFileName);
|
console.log("🍀 =================================");
|
||||||
console.log("🍀 exportJson 존재:", !!exportJson);
|
console.log("🍀 원본 파일명:", xlsxFileName);
|
||||||
console.log("🍀 exportJson 타입:", typeof exportJson);
|
console.log("🍀 exportJson 존재:", !!exportJson);
|
||||||
|
console.log("🍀 exportJson 타입:", typeof exportJson);
|
||||||
|
|
||||||
if (exportJson) {
|
if (exportJson) {
|
||||||
console.log("🍀 exportJson 전체 구조:", exportJson);
|
console.log("🍀 exportJson 전체 구조:", exportJson);
|
||||||
console.log("🍀 exportJson.sheets 존재:", !!exportJson.sheets);
|
console.log("🍀 exportJson.sheets 존재:", !!exportJson.sheets);
|
||||||
console.log(
|
console.log(
|
||||||
"🍀 exportJson.sheets 타입:",
|
"🍀 exportJson.sheets 타입:",
|
||||||
typeof exportJson.sheets,
|
typeof exportJson.sheets,
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
"🍀 exportJson.sheets 배열 여부:",
|
"🍀 exportJson.sheets 배열 여부:",
|
||||||
Array.isArray(exportJson.sheets),
|
Array.isArray(exportJson.sheets),
|
||||||
);
|
);
|
||||||
console.log("🍀 시트 개수:", exportJson?.sheets?.length || 0);
|
console.log("🍀 시트 개수:", exportJson?.sheets?.length || 0);
|
||||||
|
|
||||||
if (exportJson.sheets && Array.isArray(exportJson.sheets)) {
|
if (exportJson.sheets && Array.isArray(exportJson.sheets)) {
|
||||||
exportJson.sheets.forEach((sheet: any, index: number) => {
|
exportJson.sheets.forEach((sheet: any, index: number) => {
|
||||||
console.log(`🍀 시트 ${index + 1}:`, {
|
console.log(`🍀 시트 ${index + 1}:`, {
|
||||||
name: sheet.name,
|
name: sheet.name,
|
||||||
row: sheet.row,
|
row: sheet.row,
|
||||||
column: sheet.column,
|
column: sheet.column,
|
||||||
celldata길이: sheet.celldata?.length || 0,
|
celldata길이: sheet.celldata?.length || 0,
|
||||||
키목록: Object.keys(sheet),
|
키목록: Object.keys(sheet),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🍀 luckysheetfile 존재:", !!luckysheetfile);
|
||||||
|
console.log("🍀 luckysheetfile 타입:", typeof luckysheetfile);
|
||||||
|
if (luckysheetfile) {
|
||||||
|
console.log("🍀 luckysheetfile 구조:", luckysheetfile);
|
||||||
|
}
|
||||||
|
console.log("🍀 =================================");
|
||||||
|
|
||||||
|
console.log("🔍 LuckyExcel 변환 결과:", {
|
||||||
|
hasExportJson: !!exportJson,
|
||||||
|
hasSheets: !!exportJson?.sheets,
|
||||||
|
sheetsCount: 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: false,
|
||||||
|
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);
|
||||||
|
|
||||||
console.log("🍀 luckysheetfile 존재:", !!luckysheetfile);
|
// LuckyExcel 오류 시 SheetJS 방식으로 대체
|
||||||
console.log("🍀 luckysheetfile 타입:", typeof luckysheetfile);
|
|
||||||
if (luckysheetfile) {
|
|
||||||
console.log("🍀 luckysheetfile 구조:", luckysheetfile);
|
|
||||||
}
|
|
||||||
console.log("🍀 =================================");
|
|
||||||
|
|
||||||
console.log("🔍 LuckyExcel 변환 결과:", {
|
|
||||||
hasExportJson: !!exportJson,
|
|
||||||
hasSheets: !!exportJson?.sheets,
|
|
||||||
sheetsCount: 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(fallbackSheets);
|
|
||||||
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,
|
|
||||||
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: false,
|
|
||||||
allowCopy: true,
|
|
||||||
allowEdit: true,
|
|
||||||
enableAddRow: true,
|
|
||||||
enableAddCol: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("✅ LuckyExcel 처리 성공:", sheets.length, "개 시트");
|
|
||||||
resolve(sheets);
|
|
||||||
} catch (processError) {
|
|
||||||
console.error("❌ LuckyExcel 후처리 중 오류:", processError);
|
|
||||||
|
|
||||||
// LuckyExcel 후처리 실패 시 SheetJS 방식으로 대체
|
|
||||||
try {
|
try {
|
||||||
console.log("🔄 SheetJS 방식으로 대체 처리...");
|
console.log("🔄 SheetJS 방식으로 대체 처리...");
|
||||||
const fallbackSheets = convertSheetJSToLuckyExcel(workbook);
|
const fallbackSheets = convertSheetJSToLuckyExcel(workbook);
|
||||||
resolve(fallbackSheets);
|
resolve({ sheets: fallbackSheets, xlsxBuffer: xlsxArrayBuffer });
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
console.error("❌ SheetJS 대체 처리도 실패:", fallbackError);
|
console.error("❌ SheetJS 대체 처리도 실패:", fallbackError);
|
||||||
reject(fallbackError);
|
reject(fallbackError);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
);
|
||||||
// 오류 콜백 함수 (세 번째 매개변수)
|
} catch (luckyError) {
|
||||||
(error: any) => {
|
console.error("❌ LuckyExcel 호출 중 오류:", luckyError);
|
||||||
console.error("❌ LuckyExcel 변환 오류:", error);
|
|
||||||
|
|
||||||
// LuckyExcel 오류 시 SheetJS 방식으로 대체
|
// LuckyExcel 호출 실패 시 SheetJS 방식으로 대체
|
||||||
try {
|
try {
|
||||||
console.log("🔄 SheetJS 방식으로 대체 처리...");
|
console.log("🔄 SheetJS 방식으로 대체 처리...");
|
||||||
const fallbackSheets = convertSheetJSToLuckyExcel(workbook);
|
const fallbackSheets = convertSheetJSToLuckyExcel(workbook);
|
||||||
resolve(fallbackSheets);
|
resolve({ sheets: fallbackSheets, xlsxBuffer: xlsxArrayBuffer });
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
console.error("❌ SheetJS 대체 처리도 실패:", fallbackError);
|
console.error("❌ SheetJS 대체 처리도 실패:", fallbackError);
|
||||||
reject(fallbackError);
|
reject(fallbackError);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (luckyError) {
|
|
||||||
console.error("❌ LuckyExcel 호출 중 오류:", luckyError);
|
|
||||||
|
|
||||||
// LuckyExcel 호출 실패 시 SheetJS 방식으로 대체
|
|
||||||
try {
|
|
||||||
console.log("🔄 SheetJS 방식으로 대체 처리...");
|
|
||||||
const fallbackSheets = convertSheetJSToLuckyExcel(workbook);
|
|
||||||
resolve(fallbackSheets);
|
|
||||||
} catch (fallbackError) {
|
|
||||||
console.error("❌ SheetJS 대체 처리도 실패:", fallbackError);
|
|
||||||
reject(fallbackError);
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -837,6 +851,7 @@ export async function processExcelFile(file: File): Promise<FileUploadResult> {
|
|||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
fileSize: file.size,
|
fileSize: file.size,
|
||||||
|
file: file,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -853,6 +868,7 @@ export async function processExcelFile(file: File): Promise<FileUploadResult> {
|
|||||||
"지원되지 않는 파일 형식입니다. .csv, .xls, .xlsx 파일을 사용해주세요.",
|
"지원되지 않는 파일 형식입니다. .csv, .xls, .xlsx 파일을 사용해주세요.",
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
fileSize: file.size,
|
fileSize: file.size,
|
||||||
|
file: file,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -861,7 +877,7 @@ export async function processExcelFile(file: File): Promise<FileUploadResult> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 통합된 처리 방식: SheetJS → XLSX → LuckyExcel
|
// 통합된 처리 방식: SheetJS → XLSX → LuckyExcel
|
||||||
const sheets = await processFileWithSheetJSToXLSX(file);
|
const { sheets, xlsxBuffer } = await processFileWithSheetJSToXLSX(file);
|
||||||
|
|
||||||
if (!sheets || sheets.length === 0) {
|
if (!sheets || sheets.length === 0) {
|
||||||
return {
|
return {
|
||||||
@@ -869,6 +885,7 @@ export async function processExcelFile(file: File): Promise<FileUploadResult> {
|
|||||||
error: "파일에서 유효한 시트를 찾을 수 없습니다.",
|
error: "파일에서 유효한 시트를 찾을 수 없습니다.",
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
fileSize: file.size,
|
fileSize: file.size,
|
||||||
|
file: file,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -879,6 +896,8 @@ export async function processExcelFile(file: File): Promise<FileUploadResult> {
|
|||||||
data: sheets,
|
data: sheets,
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
fileSize: file.size,
|
fileSize: file.size,
|
||||||
|
file: file,
|
||||||
|
xlsxBuffer,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ 파일 처리 중 오류 발생:", error);
|
console.error("❌ 파일 처리 중 오류 발생:", error);
|
||||||
@@ -912,6 +931,7 @@ export async function processExcelFile(file: File): Promise<FileUploadResult> {
|
|||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
fileSize: file.size,
|
fileSize: file.size,
|
||||||
|
file: file,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/utils/luckysheetApi.ts
Normal file
1
src/utils/luckysheetApi.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
120
src/vite-env.d.ts
vendored
120
src/vite-env.d.ts
vendored
@@ -61,3 +61,123 @@ declare module "luckyexcel" {
|
|||||||
LuckyExcelResult,
|
LuckyExcelResult,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Luckysheet 타입 선언
|
||||||
|
*/
|
||||||
|
declare module "luckysheet" {
|
||||||
|
interface LuckysheetConfig {
|
||||||
|
container?: string;
|
||||||
|
title?: string;
|
||||||
|
lang?: string;
|
||||||
|
data?: any[];
|
||||||
|
myFolderUrl?: string;
|
||||||
|
plugins?: string[];
|
||||||
|
fontList?: Array<{
|
||||||
|
fontFamily: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
options?: {
|
||||||
|
showtoolbar?: boolean;
|
||||||
|
showinfobar?: boolean;
|
||||||
|
showsheetbar?: boolean;
|
||||||
|
showstatisticBar?: boolean;
|
||||||
|
allowCopy?: boolean;
|
||||||
|
allowEdit?: boolean;
|
||||||
|
enableAddRow?: boolean;
|
||||||
|
enableAddCol?: boolean;
|
||||||
|
sheetRightClickConfig?: {
|
||||||
|
delete?: boolean;
|
||||||
|
copy?: boolean;
|
||||||
|
rename?: boolean;
|
||||||
|
color?: boolean;
|
||||||
|
hide?: boolean;
|
||||||
|
move?: boolean;
|
||||||
|
};
|
||||||
|
cellRightClickConfig?: {
|
||||||
|
copy?: boolean;
|
||||||
|
copyAs?: boolean;
|
||||||
|
paste?: boolean;
|
||||||
|
insertRow?: boolean;
|
||||||
|
insertColumn?: boolean;
|
||||||
|
deleteRow?: boolean;
|
||||||
|
deleteColumn?: boolean;
|
||||||
|
deleteCell?: boolean;
|
||||||
|
hideRow?: boolean;
|
||||||
|
hideColumn?: boolean;
|
||||||
|
rowHeight?: boolean;
|
||||||
|
columnWidth?: boolean;
|
||||||
|
clear?: boolean;
|
||||||
|
matrix?: boolean;
|
||||||
|
sort?: boolean;
|
||||||
|
filter?: boolean;
|
||||||
|
chart?: boolean;
|
||||||
|
image?: boolean;
|
||||||
|
link?: boolean;
|
||||||
|
data?: boolean;
|
||||||
|
cellFormat?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
hook?: {
|
||||||
|
cellMousedown?: (cell: any, postion: any, sheetFile: any) => void;
|
||||||
|
cellClick?: (cell: any, postion: any, sheetFile: any) => void;
|
||||||
|
sheetActivate?: (
|
||||||
|
index: number,
|
||||||
|
isPivotInitial: boolean,
|
||||||
|
isInitialLoad: boolean,
|
||||||
|
) => void;
|
||||||
|
updated?: (operate: any) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LuckysheetAPI {
|
||||||
|
create: (config: LuckysheetConfig) => void;
|
||||||
|
destroy: () => void;
|
||||||
|
refreshFormula: () => void;
|
||||||
|
setSheetData: (data: any[]) => void;
|
||||||
|
getAllSheets: () => any[];
|
||||||
|
getSheet: (index?: number) => any;
|
||||||
|
setActiveSheet: (index: number) => void;
|
||||||
|
getCellValue: (r: number, c: number, data?: any) => any;
|
||||||
|
setCellValue: (r: number, c: number, d: any, isRefresh?: boolean) => void;
|
||||||
|
getRange: () => any[];
|
||||||
|
setRange: (range: any[]) => void;
|
||||||
|
scroll: (settings: { scrollLeft?: number; scrollTop?: number }) => void;
|
||||||
|
resize: () => void;
|
||||||
|
undo: () => void;
|
||||||
|
redo: () => void;
|
||||||
|
copy: () => void;
|
||||||
|
paste: () => void;
|
||||||
|
cut: () => void;
|
||||||
|
insertRow: (index?: number) => void;
|
||||||
|
insertColumn: (index?: number) => void;
|
||||||
|
deleteRow: (start: number, end?: number) => void;
|
||||||
|
deleteColumn: (start: number, end?: number) => void;
|
||||||
|
hideRow: (rowIndexes: number[]) => void;
|
||||||
|
showRow: (rowIndexes: number[]) => void;
|
||||||
|
hideColumn: (columnIndexes: number[]) => void;
|
||||||
|
showColumn: (columnIndexes: number[]) => void;
|
||||||
|
setRowHeight: (rowInfo: { [key: number]: number }) => void;
|
||||||
|
setColumnWidth: (columnInfo: { [key: number]: number }) => void;
|
||||||
|
getRowHeight: (rowIndexes: number[]) => { [key: number]: number };
|
||||||
|
getColumnWidth: (columnIndexes: number[]) => { [key: number]: number };
|
||||||
|
setWorkbookName: (name: string) => void;
|
||||||
|
getWorkbookName: () => string;
|
||||||
|
exitEditMode: () => void;
|
||||||
|
enterEditMode: (cell?: any) => void;
|
||||||
|
updateCell: (r: number, c: number, value: any) => void;
|
||||||
|
refreshCanvas: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const luckysheet: LuckysheetAPI;
|
||||||
|
export = luckysheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Luckysheet 전역 변수 타입 선언
|
||||||
|
*/
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
luckysheet: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/// <reference types="vitest" />
|
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
// @ts-ignore - vitest config
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
|
|||||||
Reference in New Issue
Block a user