Compare commits
3 Commits
bc5b316f3c
...
ba58aaabf5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba58aaabf5 | ||
|
|
d9a198a157 | ||
|
|
de6b4debac |
194
.cursor/rules/luckysheet-functionlist-error-fix.mdc
Normal file
194
.cursor/rules/luckysheet-functionlist-error-fix.mdc
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Luckysheet Functionlist Error Prevention
|
||||||
|
|
||||||
|
**Root Cause Analysis:**
|
||||||
|
- Luckysheet internally references multiple functionlist objects at different namespaces
|
||||||
|
- Timing issues between library loading and object initialization
|
||||||
|
- Incomplete plugin.js loading which should initialize functionlist objects
|
||||||
|
|
||||||
|
## **Critical Loading Order (MUST Follow)**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ DO: Complete library loading sequence
|
||||||
|
const loadSequence = async () => {
|
||||||
|
// 1. jQuery (Luckysheet dependency)
|
||||||
|
await loadResource("js", "https://code.jquery.com/jquery-3.6.0.min.js", "jquery");
|
||||||
|
|
||||||
|
// 2. CSS files (in specific order)
|
||||||
|
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. LuckyExcel (for Excel file processing)
|
||||||
|
await loadResource("js", "/luckysheet/dist/luckyexcel.umd.js", "luckyexcel");
|
||||||
|
|
||||||
|
// 4. Plugin JS (CRITICAL: initializes functionlist)
|
||||||
|
await loadResource("js", "/luckysheet/dist/plugins/js/plugin.js", "plugin-js");
|
||||||
|
|
||||||
|
// 5. Luckysheet main library
|
||||||
|
await loadResource("js", "/luckysheet/dist/luckysheet.umd.js", "luckysheet");
|
||||||
|
|
||||||
|
// 6. CRITICAL: Wait for internal object initialization
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## **Multi-Level Functionlist Initialization**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ DO: Initialize all possible functionlist reference paths
|
||||||
|
const initializeFunctionlist = () => {
|
||||||
|
try {
|
||||||
|
// Level 1: Core Store objects
|
||||||
|
if (!window.Store) window.Store = {};
|
||||||
|
if (!window.Store.functionlist) window.Store.functionlist = [];
|
||||||
|
if (!window.Store.luckysheet_function) window.Store.luckysheet_function = {};
|
||||||
|
|
||||||
|
// Level 2: Global function objects
|
||||||
|
if (!window.luckysheet_function) window.luckysheet_function = {};
|
||||||
|
if (!window.functionlist) window.functionlist = [];
|
||||||
|
|
||||||
|
// Level 3: Luckysheet internal objects
|
||||||
|
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 = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level 4: Store internal structure
|
||||||
|
if (!window.Store.config) window.Store.config = {};
|
||||||
|
if (!window.Store.luckysheetfile) window.Store.luckysheetfile = [];
|
||||||
|
if (!window.Store.currentSheetIndex) window.Store.currentSheetIndex = 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Functionlist initialization warning:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## **TypeScript Window Interface Extension**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ DO: Extend Window interface for all Luckysheet globals
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
luckysheet: any;
|
||||||
|
LuckyExcel: any;
|
||||||
|
$: any; // jQuery
|
||||||
|
Store: any; // Luckysheet Store
|
||||||
|
luckysheet_function: any; // Luckysheet function list
|
||||||
|
functionlist: any[]; // Global functionlist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## **Critical Timing Requirements**
|
||||||
|
|
||||||
|
- **MUST** call `initializeFunctionlist()` at three points:
|
||||||
|
1. After library loading sequence completion
|
||||||
|
2. After 1000ms wait period for internal initialization
|
||||||
|
3. Immediately before `luckysheet.create()` call
|
||||||
|
|
||||||
|
- **MUST** wait at least 1000ms after all libraries are loaded
|
||||||
|
- **MUST** verify all functionlist objects exist before calling `luckysheet.create()`
|
||||||
|
|
||||||
|
## **Error Recovery Pattern**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ DO: Implement robust error recovery
|
||||||
|
try {
|
||||||
|
// Final verification before luckysheet.create()
|
||||||
|
initializeFunctionlist();
|
||||||
|
|
||||||
|
// Verify critical objects exist
|
||||||
|
const verificationResults = {
|
||||||
|
store: !!window.Store,
|
||||||
|
functionlist: !!window.Store?.functionlist,
|
||||||
|
luckysheet: !!window.luckysheet,
|
||||||
|
createFunction: typeof window.luckysheet?.create === "function"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!verificationResults.luckysheet || !verificationResults.createFunction) {
|
||||||
|
throw new Error("Luckysheet not properly initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
window.luckysheet.create(options);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Luckysheet initialization failed:", error);
|
||||||
|
// Implement retry logic or fallback
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## **Common Anti-Patterns to Avoid**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ DON'T: Skip plugin.js loading
|
||||||
|
// plugin.js is CRITICAL for functionlist initialization
|
||||||
|
|
||||||
|
// ❌ DON'T: Use insufficient wait times
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100)); // TOO SHORT
|
||||||
|
|
||||||
|
// ❌ DON'T: Initialize only Store.functionlist
|
||||||
|
// Multiple objects need initialization
|
||||||
|
|
||||||
|
// ❌ DON'T: Call luckysheet.create() immediately after library load
|
||||||
|
// Internal objects need time to initialize
|
||||||
|
```
|
||||||
|
|
||||||
|
## **Debugging Checklist**
|
||||||
|
|
||||||
|
When functionlist errors occur:
|
||||||
|
1. ✅ Verify all libraries loaded in correct order
|
||||||
|
2. ✅ Check plugin.js is included and loaded
|
||||||
|
3. ✅ Confirm 1000ms wait after library loading
|
||||||
|
4. ✅ Verify all functionlist objects are arrays/objects (not undefined)
|
||||||
|
5. ✅ Check console for library loading errors
|
||||||
|
6. ✅ Ensure complete Luckysheet distribution is used (not partial)
|
||||||
|
|
||||||
|
## **Critical: Use Official LuckyExcel Pattern**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ DO: Follow official LuckyExcel → Luckysheet pattern exactly
|
||||||
|
LuckyExcel.transformExcelToLucky(arrayBuffer, fileName,
|
||||||
|
// Success callback
|
||||||
|
(exportJson: any, luckysheetfile: any) => {
|
||||||
|
// CRITICAL: Use exportJson.sheets directly, no custom validation
|
||||||
|
const luckysheetOptions = {
|
||||||
|
container: 'luckysheet-container',
|
||||||
|
data: exportJson.sheets, // Direct usage - don't modify!
|
||||||
|
title: exportJson.info?.name || fileName,
|
||||||
|
userInfo: exportJson.info?.creator || "User",
|
||||||
|
lang: "ko"
|
||||||
|
};
|
||||||
|
|
||||||
|
window.luckysheet.create(luckysheetOptions);
|
||||||
|
},
|
||||||
|
// Error callback
|
||||||
|
(error: any) => {
|
||||||
|
console.error("LuckyExcel conversion failed:", error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## **Anti-Pattern: Over-Processing Data**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ DON'T: Modify or validate exportJson.sheets structure
|
||||||
|
const validatedSheets = exportJson.sheets.map(sheet => ({
|
||||||
|
name: sheet?.name || `Sheet${index}`,
|
||||||
|
data: Array.isArray(sheet?.data) ? sheet.data : [],
|
||||||
|
// ... other modifications
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ❌ DON'T: Use modified data
|
||||||
|
luckysheet.create({ data: validatedSheets });
|
||||||
|
```
|
||||||
|
|
||||||
|
The root cause of functionlist errors is often data structure mismatch between LuckyExcel output and Luckysheet expectations. Using exportJson.sheets directly maintains the proper internal structure that Luckysheet requires.
|
||||||
|
|
||||||
|
This pattern successfully resolves the "Cannot read properties of undefined (reading 'functionlist')" error by ensuring complete library loading sequence and multi-level functionlist initialization.
|
||||||
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
|
||||||
5
.cursor/rules/xlsx-js-style.mdc
Normal file
5
.cursor/rules/xlsx-js-style.mdc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
BIN
luckysheet-src.zip
Normal file
BIN
luckysheet-src.zip
Normal file
Binary file not shown.
5290
package-lock.json
generated
5290
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -19,16 +19,31 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
|
"@univerjs/core": "^0.8.2",
|
||||||
|
"@univerjs/design": "^0.8.2",
|
||||||
|
"@univerjs/docs": "^0.8.2",
|
||||||
|
"@univerjs/docs-ui": "^0.8.2",
|
||||||
|
"@univerjs/engine-formula": "^0.8.2",
|
||||||
|
"@univerjs/engine-numfmt": "^0.8.2",
|
||||||
|
"@univerjs/engine-render": "^0.8.2",
|
||||||
|
"@univerjs/facade": "^0.5.5",
|
||||||
|
"@univerjs/sheets": "^0.8.2",
|
||||||
|
"@univerjs/sheets-formula": "^0.8.2",
|
||||||
|
"@univerjs/sheets-formula-ui": "^0.8.2",
|
||||||
|
"@univerjs/sheets-numfmt": "^0.8.2",
|
||||||
|
"@univerjs/sheets-numfmt-ui": "^0.8.2",
|
||||||
|
"@univerjs/sheets-ui": "^0.8.2",
|
||||||
|
"@univerjs/ui": "^0.8.2",
|
||||||
|
"@univerjs/uniscript": "^0.8.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"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",
|
|
||||||
"tailwind-merge": "^2.5.4",
|
"tailwind-merge": "^2.5.4",
|
||||||
"xlsx": "^0.18.5",
|
|
||||||
"zustand": "^5.0.2"
|
"zustand": "^5.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -43,7 +58,9 @@
|
|||||||
"@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",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.15.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
@@ -52,6 +69,7 @@
|
|||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.1",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
|
"stream-browserify": "^3.0.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^6.0.1",
|
"vite": "^6.0.1",
|
||||||
|
|||||||
1701
public/output.css
Normal file
1701
public/output.css
Normal file
File diff suppressed because it is too large
Load Diff
53
src/App.tsx
53
src/App.tsx
@@ -1,9 +1,10 @@
|
|||||||
import { useAppStore } from "./stores/useAppStore";
|
import { useState } from "react";
|
||||||
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 TestSheetViewer from "./components/sheet/TestSheetViewer";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [showTestViewer, setShowTestViewer] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
@@ -14,17 +15,53 @@ 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">
|
{/* 테스트 뷰어 토글 버튼 */}
|
||||||
Excel 파일 AI 처리 도구
|
<Button
|
||||||
</span>
|
variant={showTestViewer ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowTestViewer(!showTestViewer)}
|
||||||
|
className="bg-green-500 hover:bg-green-600 text-white border-green-500"
|
||||||
|
>
|
||||||
|
🧪 테스트 뷰어
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{!showTestViewer && (
|
||||||
|
<span className="text-sm text-gray-600">
|
||||||
|
Univer CE 테스트 모드
|
||||||
|
</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 />
|
{showTestViewer ? (
|
||||||
|
// 테스트 뷰어 표시
|
||||||
|
<div className="h-full">
|
||||||
|
<TestSheetViewer />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// 메인 페이지
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 mb-4">
|
||||||
|
🧪 Univer CE 테스트 모드
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 mb-8">
|
||||||
|
현재 Univer CE 전용 테스트 뷰어를 사용해보세요
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowTestViewer(true)}
|
||||||
|
size="lg"
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white px-8 py-3"
|
||||||
|
>
|
||||||
|
테스트 뷰어 시작하기 →
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
491
src/components/sheet/SheetViewer.tsx.bak
Normal file
491
src/components/sheet/SheetViewer.tsx.bak
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
import {
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { useAppStore } from "../../stores/useAppStore";
|
||||||
|
|
||||||
|
interface SheetViewerProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Luckysheet 시트 뷰어 컴포넌트
|
||||||
|
* - 메모리 정보 기반: LuckyExcel 변환 결과를 직접 사용
|
||||||
|
* - 커스텀 검증이나 데이터 구조 변경 금지
|
||||||
|
* - luckysheet.create({ data: exportJson.sheets })로 직접 사용
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 스토어에서 현재 파일 정보만 가져오기 (시트 데이터는 LuckyExcel로 직접 변환)
|
||||||
|
const { currentFile, setSelectedRange } = useAppStore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CDN 배포판 라이브러리 로딩
|
||||||
|
*/
|
||||||
|
const loadLuckysheetLibrary = useCallback((): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 이미 로드된 경우
|
||||||
|
if (
|
||||||
|
window.luckysheet &&
|
||||||
|
window.LuckyExcel &&
|
||||||
|
window.$ &&
|
||||||
|
librariesLoaded
|
||||||
|
) {
|
||||||
|
console.log("📦 모든 라이브러리가 이미 로드됨");
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadResource = (
|
||||||
|
type: "css" | "js",
|
||||||
|
src: string,
|
||||||
|
id: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
return new Promise((resourceResolve, resourceReject) => {
|
||||||
|
// 이미 로드된 리소스 체크
|
||||||
|
if (document.querySelector(`[data-luckysheet-id="${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 = () => resourceResolve();
|
||||||
|
link.onerror = () =>
|
||||||
|
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 = () => resourceResolve();
|
||||||
|
script.onerror = () =>
|
||||||
|
resourceReject(new Error(`${id} JS 로드 실패`));
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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",
|
||||||
|
"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",
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Luckysheet 메인
|
||||||
|
if (!window.luckysheet) {
|
||||||
|
await loadResource(
|
||||||
|
"js",
|
||||||
|
"https://cdn.jsdelivr.net/npm/luckysheet/dist/luckysheet.umd.js",
|
||||||
|
"luckysheet",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. LuckyExcel (Excel 파일 처리용)
|
||||||
|
if (!window.LuckyExcel) {
|
||||||
|
await loadResource(
|
||||||
|
"js",
|
||||||
|
"https://cdn.jsdelivr.net/npm/luckyexcel/dist/luckyexcel.umd.js",
|
||||||
|
"luckyexcel",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 라이브러리 검증
|
||||||
|
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"
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
!validationResults.luckysheet ||
|
||||||
|
!validationResults.luckysheetCreate
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Luckysheet 객체가 올바르게 초기화되지 않았습니다.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLibrariesLoaded(true);
|
||||||
|
console.log("✅ 라이브러리 로드 완료");
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 라이브러리 로딩 실패:", error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadSequence();
|
||||||
|
});
|
||||||
|
}, [librariesLoaded]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메모리 정보 기반: LuckyExcel 변환 결과를 직접 사용하는 방식
|
||||||
|
* - LuckyExcel.transformExcelToLucky()에서 반환된 exportJson.sheets를 그대로 사용
|
||||||
|
* - 커스텀 검증이나 데이터 구조 변경 금지
|
||||||
|
*/
|
||||||
|
const convertXLSXWithLuckyExcel = useCallback(
|
||||||
|
async (xlsxBuffer: ArrayBuffer, fileName: string) => {
|
||||||
|
if (!containerRef.current) {
|
||||||
|
console.warn("⚠️ 컨테이너가 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsConverting(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
console.log("🍀 메모리 정보 기반: LuckyExcel 직접 변환 시작...");
|
||||||
|
|
||||||
|
// 라이브러리 로드 확인
|
||||||
|
await loadLuckysheetLibrary();
|
||||||
|
|
||||||
|
// 기존 인스턴스 정리
|
||||||
|
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 호출...");
|
||||||
|
|
||||||
|
// ArrayBuffer를 File 객체로 변환 (LuckyExcel은 File 객체 필요)
|
||||||
|
const file = new File([xlsxBuffer], fileName, {
|
||||||
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
});
|
||||||
|
|
||||||
|
// LuckyExcel의 직접 변환 사용 (Promise 방식)
|
||||||
|
const luckyExcelResult = await new Promise<any>((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// 🚨 수정: 첫 번째 매개변수는 File 객체여야 함
|
||||||
|
(window.LuckyExcel as any).transformExcelToLucky(
|
||||||
|
file, // ArrayBuffer 대신 File 객체 사용
|
||||||
|
// 성공 콜백
|
||||||
|
(exportJson: any, luckysheetfile: any) => {
|
||||||
|
console.log("🍀 LuckyExcel 변환 성공!");
|
||||||
|
console.log("🍀 exportJson:", exportJson);
|
||||||
|
console.log("🍀 luckysheetfile:", luckysheetfile);
|
||||||
|
resolve(exportJson);
|
||||||
|
},
|
||||||
|
// 에러 콜백
|
||||||
|
(error: any) => {
|
||||||
|
console.error("❌ LuckyExcel 변환 실패:", error);
|
||||||
|
reject(new Error(`LuckyExcel 변환 실패: ${error}`));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (callError) {
|
||||||
|
console.error("❌ LuckyExcel 호출 중 오류:", callError);
|
||||||
|
reject(callError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 결과 검증
|
||||||
|
if (
|
||||||
|
!luckyExcelResult ||
|
||||||
|
!luckyExcelResult.sheets ||
|
||||||
|
!Array.isArray(luckyExcelResult.sheets)
|
||||||
|
) {
|
||||||
|
throw new Error("LuckyExcel 변환 결과가 유효하지 않습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🎉 LuckyExcel 변환 완료, Luckysheet 생성 중...");
|
||||||
|
|
||||||
|
// 메모리 정보 기반: exportJson.sheets를 그대로 사용
|
||||||
|
// luckysheet.create({ data: exportJson.sheets })
|
||||||
|
window.luckysheet.create({
|
||||||
|
container: containerRef.current?.id || "luckysheet-container",
|
||||||
|
showinfobar: true,
|
||||||
|
showtoolbar: true,
|
||||||
|
showsheetbar: true,
|
||||||
|
showstatisticBar: true,
|
||||||
|
allowCopy: true,
|
||||||
|
allowEdit: true,
|
||||||
|
// 🚨 핵심: LuckyExcel의 원본 변환 결과를 직접 사용
|
||||||
|
data: luckyExcelResult.sheets, // 가공하지 않고 그대로 전달
|
||||||
|
title: luckyExcelResult.info?.name || fileName,
|
||||||
|
// 🚨 수정: userInfo 경로 수정
|
||||||
|
userInfo: luckyExcelResult.info?.creator || false,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("🎉 Luckysheet 생성 완료! (원본 데이터 직접 사용)");
|
||||||
|
setIsInitialized(true);
|
||||||
|
setIsConverting(false);
|
||||||
|
setError(null);
|
||||||
|
luckysheetRef.current = window.luckysheet;
|
||||||
|
} catch (conversionError) {
|
||||||
|
console.error("❌ 변환 프로세스 실패:", conversionError);
|
||||||
|
setError(
|
||||||
|
`변환 프로세스에 실패했습니다: ${
|
||||||
|
conversionError instanceof Error
|
||||||
|
? conversionError.message
|
||||||
|
: String(conversionError)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
setIsConverting(false);
|
||||||
|
setIsInitialized(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[loadLuckysheetLibrary, setSelectedRange],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DOM 컨테이너 준비 상태 체크 - useLayoutEffect로 동기적 체크
|
||||||
|
*/
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
console.log("✅ DOM 컨테이너 준비 완료:", containerRef.current.id);
|
||||||
|
setIsContainerReady(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DOM 컨테이너 준비 상태 재체크 (fallback)
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isContainerReady) {
|
||||||
|
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 버퍼 감지, LuckyExcel 직접 변환 시작...", {
|
||||||
|
fileName: currentFile.name,
|
||||||
|
bufferSize: currentFile.xlsxBuffer.byteLength,
|
||||||
|
containerId: containerRef.current.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 중복 실행 방지
|
||||||
|
setIsConverting(true);
|
||||||
|
|
||||||
|
// LuckyExcel로 직접 변환
|
||||||
|
convertXLSXWithLuckyExcel(currentFile.xlsxBuffer, currentFile.name);
|
||||||
|
} else if (currentFile && !currentFile.xlsxBuffer) {
|
||||||
|
setError("파일 변환 데이터가 없습니다. 파일을 다시 업로드해주세요.");
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
currentFile?.xlsxBuffer,
|
||||||
|
currentFile?.name,
|
||||||
|
isContainerReady,
|
||||||
|
isInitialized,
|
||||||
|
isConverting,
|
||||||
|
convertXLSXWithLuckyExcel,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컴포넌트 언마운트 시 정리
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (luckysheetRef.current && window.luckysheet) {
|
||||||
|
try {
|
||||||
|
window.luckysheet.destroy();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("⚠️ Luckysheet 정리 중 오류:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 윈도우 리사이즈 처리
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
if (luckysheetRef.current && window.luckysheet) {
|
||||||
|
try {
|
||||||
|
if (window.luckysheet.resize) {
|
||||||
|
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) {
|
||||||
|
convertXLSXWithLuckyExcel(
|
||||||
|
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 ? "LuckyExcel 변환 중..." : "시트 초기화 중..."}
|
||||||
|
</div>
|
||||||
|
<div className="text-blue-500 text-sm">
|
||||||
|
{isConverting
|
||||||
|
? "원본 Excel 데이터를 완전한 스타일로 변환하고 있습니다."
|
||||||
|
: "잠시만 기다려주세요."}
|
||||||
|
</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>방식: LuckyExcel 직접 변환</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
174
src/components/sheet/TestSheetViewer.tsx
Normal file
174
src/components/sheet/TestSheetViewer.tsx
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import React, { useRef, useEffect, useState } from "react";
|
||||||
|
import { Univer, UniverInstanceType, LocaleType } from "@univerjs/core";
|
||||||
|
import { defaultTheme } from "@univerjs/design";
|
||||||
|
import { UniverDocsPlugin } from "@univerjs/docs";
|
||||||
|
import { UniverDocsUIPlugin } from "@univerjs/docs-ui";
|
||||||
|
import { UniverFormulaEnginePlugin } from "@univerjs/engine-formula";
|
||||||
|
import { UniverRenderEnginePlugin } from "@univerjs/engine-render";
|
||||||
|
import { UniverSheetsPlugin } from "@univerjs/sheets";
|
||||||
|
import { UniverSheetsFormulaPlugin } from "@univerjs/sheets-formula";
|
||||||
|
import { UniverSheetsFormulaUIPlugin } from "@univerjs/sheets-formula-ui";
|
||||||
|
import { UniverSheetsUIPlugin } from "@univerjs/sheets-ui";
|
||||||
|
import { UniverSheetsNumfmtPlugin } from "@univerjs/sheets-numfmt";
|
||||||
|
import { UniverSheetsNumfmtUIPlugin } from "@univerjs/sheets-numfmt-ui";
|
||||||
|
import { UniverUIPlugin } from "@univerjs/ui";
|
||||||
|
|
||||||
|
// 언어팩 import
|
||||||
|
import DesignEnUS from "@univerjs/design/locale/en-US";
|
||||||
|
import UIEnUS from "@univerjs/ui/locale/en-US";
|
||||||
|
import DocsUIEnUS from "@univerjs/docs-ui/locale/en-US";
|
||||||
|
import SheetsEnUS from "@univerjs/sheets/locale/en-US";
|
||||||
|
import SheetsUIEnUS from "@univerjs/sheets-ui/locale/en-US";
|
||||||
|
import SheetsFormulaUIEnUS from "@univerjs/sheets-formula-ui/locale/en-US";
|
||||||
|
import SheetsNumfmtUIEnUS from "@univerjs/sheets-numfmt-ui/locale/en-US";
|
||||||
|
|
||||||
|
// CSS 스타일 import
|
||||||
|
import "@univerjs/design/lib/index.css";
|
||||||
|
import "@univerjs/ui/lib/index.css";
|
||||||
|
import "@univerjs/docs-ui/lib/index.css";
|
||||||
|
import "@univerjs/sheets-ui/lib/index.css";
|
||||||
|
import "@univerjs/sheets-formula-ui/lib/index.css";
|
||||||
|
import "@univerjs/sheets-numfmt-ui/lib/index.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Univer CE 최소 구현 - 공식 문서 기반
|
||||||
|
* 파일 업로드 없이 기본 스프레드시트만 표시
|
||||||
|
*/
|
||||||
|
const TestSheetViewer: React.FC = () => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const univerRef = useRef<Univer | null>(null);
|
||||||
|
const initializingRef = useRef<boolean>(false);
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
|
||||||
|
// Univer 초기화 - 공식 문서 패턴 따라서
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!containerRef.current ||
|
||||||
|
isInitialized ||
|
||||||
|
univerRef.current ||
|
||||||
|
initializingRef.current
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const initializeUniver = async () => {
|
||||||
|
try {
|
||||||
|
initializingRef.current = true;
|
||||||
|
console.log("🚀 Univer CE 초기화 시작");
|
||||||
|
|
||||||
|
// 1. Univer 인스턴스 생성
|
||||||
|
const univer = new Univer({
|
||||||
|
theme: defaultTheme,
|
||||||
|
locale: LocaleType.EN_US,
|
||||||
|
locales: {
|
||||||
|
[LocaleType.EN_US]: {
|
||||||
|
...DesignEnUS,
|
||||||
|
...UIEnUS,
|
||||||
|
...DocsUIEnUS,
|
||||||
|
...SheetsEnUS,
|
||||||
|
...SheetsUIEnUS,
|
||||||
|
...SheetsFormulaUIEnUS,
|
||||||
|
...SheetsNumfmtUIEnUS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 필수 플러그인 등록 (공식 문서 순서)
|
||||||
|
univer.registerPlugin(UniverRenderEnginePlugin);
|
||||||
|
univer.registerPlugin(UniverFormulaEnginePlugin);
|
||||||
|
|
||||||
|
univer.registerPlugin(UniverUIPlugin, {
|
||||||
|
container: containerRef.current!,
|
||||||
|
});
|
||||||
|
|
||||||
|
univer.registerPlugin(UniverDocsPlugin);
|
||||||
|
univer.registerPlugin(UniverDocsUIPlugin);
|
||||||
|
|
||||||
|
univer.registerPlugin(UniverSheetsPlugin);
|
||||||
|
univer.registerPlugin(UniverSheetsUIPlugin);
|
||||||
|
univer.registerPlugin(UniverSheetsFormulaPlugin);
|
||||||
|
univer.registerPlugin(UniverSheetsFormulaUIPlugin);
|
||||||
|
univer.registerPlugin(UniverSheetsNumfmtPlugin);
|
||||||
|
univer.registerPlugin(UniverSheetsNumfmtUIPlugin);
|
||||||
|
|
||||||
|
// 3. 기본 워크북 생성
|
||||||
|
univer.createUnit(UniverInstanceType.UNIVER_SHEET, {
|
||||||
|
id: "test-workbook",
|
||||||
|
name: "Test Workbook",
|
||||||
|
sheetOrder: ["sheet1"],
|
||||||
|
sheets: {
|
||||||
|
sheet1: {
|
||||||
|
id: "sheet1",
|
||||||
|
name: "Sheet1",
|
||||||
|
cellData: {
|
||||||
|
0: {
|
||||||
|
0: { v: "Hello Univer CE!" },
|
||||||
|
1: { v: "환영합니다!" },
|
||||||
|
},
|
||||||
|
1: {
|
||||||
|
0: { v: "Status" },
|
||||||
|
1: { v: "Ready" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowCount: 100,
|
||||||
|
columnCount: 26,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
univerRef.current = univer;
|
||||||
|
setIsInitialized(true);
|
||||||
|
|
||||||
|
console.log("✅ Univer CE 초기화 완료");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Univer CE 초기화 실패:", error);
|
||||||
|
initializingRef.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializeUniver();
|
||||||
|
}, [isInitialized]);
|
||||||
|
|
||||||
|
// 컴포넌트 언마운트 시 정리
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
try {
|
||||||
|
if (univerRef.current) {
|
||||||
|
univerRef.current.dispose();
|
||||||
|
univerRef.current = null;
|
||||||
|
}
|
||||||
|
initializingRef.current = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Univer dispose 오류:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-screen flex flex-col">
|
||||||
|
{/* 간단한 헤더 */}
|
||||||
|
<div className="bg-white border-b p-4 flex-shrink-0">
|
||||||
|
<h1 className="text-xl font-bold">🧪 Univer CE 최소 테스트</h1>
|
||||||
|
<div className="mt-2">
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
||||||
|
isInitialized
|
||||||
|
? "bg-green-100 text-green-800"
|
||||||
|
: "bg-yellow-100 text-yellow-800"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isInitialized ? "✅ 초기화 완료" : "⏳ 초기화 중..."}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Univer 컨테이너 */}
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="flex-1"
|
||||||
|
style={{ minHeight: "500px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TestSheetViewer;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
// import React from "react";
|
||||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
@@ -29,7 +29,7 @@ class MockDragEvent extends Event {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
global.DragEvent = MockDragEvent;
|
global.DragEvent = MockDragEvent;
|
||||||
|
|
||||||
const mockUseAppStore = useAppStore as vi.MockedFunction<typeof useAppStore>;
|
const mockUseAppStore = useAppStore as any;
|
||||||
|
|
||||||
describe("FileUpload", () => {
|
describe("FileUpload", () => {
|
||||||
const mockSetLoading = vi.fn();
|
const mockSetLoading = vi.fn();
|
||||||
|
|||||||
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
165
src/index.css
165
src/index.css
@@ -1,3 +1,12 @@
|
|||||||
|
/* Univer CE 공식 스타일 - @import는 맨 위에 */
|
||||||
|
@import '@univerjs/design/lib/index.css';
|
||||||
|
@import '@univerjs/ui/lib/index.css';
|
||||||
|
@import '@univerjs/docs-ui/lib/index.css';
|
||||||
|
@import '@univerjs/sheets-ui/lib/index.css';
|
||||||
|
@import '@univerjs/sheets-formula-ui/lib/index.css';
|
||||||
|
@import '@univerjs/sheets-numfmt-ui/lib/index.css';
|
||||||
|
|
||||||
|
/* Tailwind CSS */
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
@@ -105,25 +114,29 @@
|
|||||||
.lg\:px-8 { padding-left: 2rem; padding-right: 2rem; }
|
.lg\:px-8 { padding-left: 2rem; padding-right: 2rem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 커스텀 스타일 */
|
/* 전역 스타일 */
|
||||||
body {
|
html, body, #root {
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
height: 100%;
|
||||||
line-height: 1.5;
|
margin: 0;
|
||||||
font-weight: 400;
|
padding: 0;
|
||||||
color-scheme: light;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
color: #1f2937; /* 검은색 계열로 변경 */
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
background-color: #ffffff;
|
sans-serif;
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 스크롤바 스타일링 */
|
/* Univer 컨테이너 스타일 */
|
||||||
|
.univer-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 커스텀 스크롤바 (Univer와 일치) */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 8px;
|
||||||
height: 6px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
@@ -132,20 +145,132 @@ body {
|
|||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #c1c1c1;
|
background: #c1c1c1;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #a1a1a1;
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 파일 업로드 영역 스타일 */
|
||||||
|
.file-upload-area {
|
||||||
|
border: 2px dashed #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload-area:hover {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload-area.dragover {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
background-color: #dbeafe;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 로딩 애니메이션 */
|
/* 로딩 애니메이션 */
|
||||||
|
.loading-spinner {
|
||||||
|
border: 2px solid #f3f3f3;
|
||||||
|
border-top: 2px solid #3b82f6;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to {
|
0% { transform: rotate(0deg); }
|
||||||
transform: rotate(360deg);
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 상태 표시 점 애니메이션 */
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.pulse {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-spin {
|
/* 에러 메시지 스타일 */
|
||||||
animation: spin 1s linear infinite;
|
.error-message {
|
||||||
|
background-color: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
color: #dc2626;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 성공 메시지 스타일 */
|
||||||
|
.success-message {
|
||||||
|
background-color: #f0fdf4;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
color: #16a34a;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 정보 메시지 스타일 */
|
||||||
|
.info-message {
|
||||||
|
background-color: #eff6ff;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
color: #2563eb;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 반응형 디자인 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.file-upload-area {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.univer-container {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 다크 모드 지원 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.file-upload-area {
|
||||||
|
border-color: #374151;
|
||||||
|
background-color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload-area:hover {
|
||||||
|
border-color: #60a5fa;
|
||||||
|
background-color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload-area.dragover {
|
||||||
|
border-color: #60a5fa;
|
||||||
|
background-color: #1e3a8a;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/main.tsx
13
src/main.tsx
@@ -1,10 +1,5 @@
|
|||||||
import { StrictMode } from 'react'
|
import { createRoot } from "react-dom/client";
|
||||||
import { createRoot } from 'react-dom/client'
|
import "./index.css";
|
||||||
import './index.css'
|
import App from "./App.tsx";
|
||||||
import App from './App.tsx'
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById("root")!).render(<App />);
|
||||||
<StrictMode>
|
|
||||||
<App />
|
|
||||||
</StrictMode>,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
import * as XLSX from "xlsx";
|
import * as XLSX from "xlsx-js-style";
|
||||||
import {
|
import {
|
||||||
validateFileType,
|
validateFileType,
|
||||||
validateFileSize,
|
validateFileSize,
|
||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
SUPPORTED_EXTENSIONS,
|
SUPPORTED_EXTENSIONS,
|
||||||
} from "../fileProcessor";
|
} from "../fileProcessor";
|
||||||
|
|
||||||
// SheetJS 모킹 (통합 처리)
|
// xlsx-js-style 모킹 (통합 처리)
|
||||||
vi.mock("xlsx", () => ({
|
vi.mock("xlsx-js-style", () => ({
|
||||||
read: vi.fn(() => ({
|
read: vi.fn(() => ({
|
||||||
SheetNames: ["Sheet1"],
|
SheetNames: ["Sheet1"],
|
||||||
Sheets: {
|
Sheets: {
|
||||||
@@ -30,61 +30,80 @@ vi.mock("xlsx", () => ({
|
|||||||
["테스트", "한글", "데이터"],
|
["테스트", "한글", "데이터"],
|
||||||
["값1", "값2", "값3"],
|
["값1", "값2", "값3"],
|
||||||
]),
|
]),
|
||||||
|
decode_range: vi.fn((_ref) => ({
|
||||||
|
s: { r: 0, c: 0 },
|
||||||
|
e: { r: 1, c: 2 },
|
||||||
|
})),
|
||||||
|
encode_cell: vi.fn(
|
||||||
|
(cell) => `${String.fromCharCode(65 + cell.c)}${cell.r + 1}`,
|
||||||
|
),
|
||||||
|
aoa_to_sheet: vi.fn(() => ({
|
||||||
|
A1: { v: "테스트" },
|
||||||
|
B1: { v: "한글" },
|
||||||
|
C1: { v: "데이터" },
|
||||||
|
"!ref": "A1:C1",
|
||||||
|
})),
|
||||||
|
book_new: vi.fn(() => ({ SheetNames: [], Sheets: {} })),
|
||||||
|
book_append_sheet: vi.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// LuckyExcel 모킹
|
// LuckyExcel 모킹
|
||||||
vi.mock("luckyexcel", () => ({
|
vi.mock("luckyexcel", () => ({
|
||||||
transformExcelToLucky: vi.fn((arrayBuffer, fileName, callback) => {
|
transformExcelToLucky: vi.fn(
|
||||||
// 성공적인 변환 결과 모킹
|
(_arrayBuffer, successCallback, _errorCallback) => {
|
||||||
const mockResult = {
|
// 성공적인 변환 결과 모킹
|
||||||
sheets: [
|
const mockResult = {
|
||||||
{
|
sheets: [
|
||||||
name: "Sheet1",
|
{
|
||||||
index: "0",
|
name: "Sheet1",
|
||||||
status: 1,
|
index: "0",
|
||||||
order: 0,
|
status: 1,
|
||||||
row: 2,
|
order: 0,
|
||||||
column: 3,
|
row: 2,
|
||||||
celldata: [
|
column: 3,
|
||||||
{
|
celldata: [
|
||||||
r: 0,
|
{
|
||||||
c: 0,
|
r: 0,
|
||||||
v: { v: "테스트", m: "테스트", ct: { fa: "General", t: "g" } },
|
c: 0,
|
||||||
},
|
v: { v: "테스트", m: "테스트", ct: { fa: "General", t: "g" } },
|
||||||
{
|
},
|
||||||
r: 0,
|
{
|
||||||
c: 1,
|
r: 0,
|
||||||
v: { v: "한글", m: "한글", ct: { fa: "General", t: "g" } },
|
c: 1,
|
||||||
},
|
v: { v: "한글", m: "한글", ct: { fa: "General", t: "g" } },
|
||||||
{
|
},
|
||||||
r: 0,
|
{
|
||||||
c: 2,
|
r: 0,
|
||||||
v: { v: "데이터", m: "데이터", ct: { fa: "General", t: "g" } },
|
c: 2,
|
||||||
},
|
v: { v: "데이터", m: "데이터", ct: { fa: "General", t: "g" } },
|
||||||
{
|
},
|
||||||
r: 1,
|
{
|
||||||
c: 0,
|
r: 1,
|
||||||
v: { v: "값1", m: "값1", ct: { fa: "General", t: "g" } },
|
c: 0,
|
||||||
},
|
v: { v: "값1", m: "값1", ct: { fa: "General", t: "g" } },
|
||||||
{
|
},
|
||||||
r: 1,
|
{
|
||||||
c: 1,
|
r: 1,
|
||||||
v: { v: "값2", m: "값2", ct: { fa: "General", t: "g" } },
|
c: 1,
|
||||||
},
|
v: { v: "값2", m: "값2", ct: { fa: "General", t: "g" } },
|
||||||
{
|
},
|
||||||
r: 1,
|
{
|
||||||
c: 2,
|
r: 1,
|
||||||
v: { v: "값3", m: "값3", ct: { fa: "General", t: "g" } },
|
c: 2,
|
||||||
},
|
v: { v: "값3", m: "값3", ct: { fa: "General", t: "g" } },
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
};
|
],
|
||||||
|
};
|
||||||
|
|
||||||
// 비동기 콜백 호출
|
// 성공 콜백 비동기 호출 (ArrayBuffer 매개변수 대응)
|
||||||
setTimeout(() => callback(mockResult, null), 0);
|
if (typeof successCallback === "function") {
|
||||||
}),
|
setTimeout(() => successCallback(mockResult, null), 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 파일 생성 도우미 함수
|
// 파일 생성 도우미 함수
|
||||||
|
|||||||
@@ -1,945 +0,0 @@
|
|||||||
import * as XLSX from "xlsx";
|
|
||||||
import * as LuckyExcel from "luckyexcel";
|
|
||||||
import type { SheetData, FileUploadResult } from "../types/sheet";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 파일 처리 관련 유틸리티 - 개선된 버전
|
|
||||||
* - 모든 파일 형식을 SheetJS를 통해 읽은 후 XLSX로 변환
|
|
||||||
* - 변환된 XLSX 파일을 LuckyExcel로 전달
|
|
||||||
* - 안정적인 한글 지원 및 에러 처리
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 지원되는 파일 타입
|
|
||||||
export const SUPPORTED_FILE_TYPES = {
|
|
||||||
XLSX: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
||||||
XLS: "application/vnd.ms-excel",
|
|
||||||
CSV: "text/csv",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const SUPPORTED_EXTENSIONS = [".xlsx", ".xls", ".csv"] as const;
|
|
||||||
|
|
||||||
// 최대 파일 크기 (50MB)
|
|
||||||
export const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 파일 타입 검증
|
|
||||||
*/
|
|
||||||
export function validateFileType(file: File): boolean {
|
|
||||||
const fileName = file.name.toLowerCase();
|
|
||||||
const extension = fileName.split(".").pop();
|
|
||||||
const supportedExtensions = SUPPORTED_EXTENSIONS.map((ext) => ext.slice(1));
|
|
||||||
|
|
||||||
if (!extension) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return supportedExtensions.includes(extension);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 파일 크기 검증
|
|
||||||
*/
|
|
||||||
export function validateFileSize(file: File): boolean {
|
|
||||||
return file.size <= MAX_FILE_SIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 파일 이름에서 확장자 제거
|
|
||||||
*/
|
|
||||||
export function getFileNameWithoutExtension(fileName: string): string {
|
|
||||||
return fileName.replace(/\.[^/.]+$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 에러 메시지 생성
|
|
||||||
*/
|
|
||||||
export function getFileErrorMessage(file: File): string {
|
|
||||||
if (!validateFileType(file)) {
|
|
||||||
const fileName = file.name.toLowerCase();
|
|
||||||
const extension = fileName.split(".").pop();
|
|
||||||
|
|
||||||
if (!extension || extension === fileName) {
|
|
||||||
return `파일 확장자가 없습니다. ${SUPPORTED_EXTENSIONS.join(", ")} 파일만 업로드 가능합니다.`;
|
|
||||||
}
|
|
||||||
return `지원되지 않는 파일 형식입니다. "${extension}" 대신 ${SUPPORTED_EXTENSIONS.join(", ")} 파일을 업로드해주세요.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validateFileSize(file)) {
|
|
||||||
const maxSizeMB = Math.round(MAX_FILE_SIZE / (1024 * 1024));
|
|
||||||
const currentSizeMB = (file.size / (1024 * 1024)).toFixed(2);
|
|
||||||
return `파일 크기가 너무 큽니다. 현재 크기: ${currentSizeMB}MB, 최대 허용: ${maxSizeMB}MB`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.name.length > 255) {
|
|
||||||
return "파일명이 너무 깁니다. 255자 이하의 파일명을 사용해주세요.";
|
|
||||||
}
|
|
||||||
|
|
||||||
const invalidChars = /[<>:"/\\|?*]/;
|
|
||||||
if (invalidChars.test(file.name)) {
|
|
||||||
return '파일명에 사용할 수 없는 특수문자가 포함되어 있습니다. (< > : " / \\ | ? *)';
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 한글 시트명을 안전하게 처리하는 함수
|
|
||||||
*/
|
|
||||||
function sanitizeSheetName(sheetName: string): string {
|
|
||||||
if (!sheetName || typeof sheetName !== "string") {
|
|
||||||
return "Sheet1";
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxLength = 31;
|
|
||||||
let sanitized = sheetName.trim();
|
|
||||||
|
|
||||||
if (sanitized.length > maxLength) {
|
|
||||||
sanitized = sanitized.substring(0, maxLength - 3) + "...";
|
|
||||||
}
|
|
||||||
|
|
||||||
sanitized = sanitized.replace(/[\\\/\*\?\[\]]/g, "_");
|
|
||||||
|
|
||||||
return sanitized || "Sheet1";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 워크북 구조 검증 함수
|
|
||||||
*/
|
|
||||||
function validateWorkbook(workbook: any): { isValid: boolean; error?: string } {
|
|
||||||
if (!workbook) {
|
|
||||||
return { isValid: false, error: "워크북이 null 또는 undefined입니다" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!workbook.SheetNames) {
|
|
||||||
return { isValid: false, error: "워크북에 SheetNames 속성이 없습니다" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(workbook.SheetNames)) {
|
|
||||||
return { isValid: false, error: "SheetNames가 배열이 아닙니다" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (workbook.SheetNames.length === 0) {
|
|
||||||
return { isValid: false, error: "워크북에 시트가 없습니다" };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { isValid: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SheetJS 데이터를 LuckyExcel 형식으로 변환
|
|
||||||
*/
|
|
||||||
function convertSheetJSToLuckyExcel(workbook: any): SheetData[] {
|
|
||||||
console.log("🔄 SheetJS → LuckyExcel 형식 변환 시작...");
|
|
||||||
|
|
||||||
const luckySheets: SheetData[] = [];
|
|
||||||
|
|
||||||
// 워크북 구조 검증
|
|
||||||
const validation = validateWorkbook(workbook);
|
|
||||||
if (!validation.isValid) {
|
|
||||||
console.error("❌ 워크북 검증 실패:", validation.error);
|
|
||||||
throw new Error(`워크북 구조 오류: ${validation.error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`📋 발견된 시트: ${workbook.SheetNames.join(", ")}`);
|
|
||||||
|
|
||||||
workbook.SheetNames.forEach((sheetName: string, index: number) => {
|
|
||||||
console.log(
|
|
||||||
`📋 시트 ${index + 1}/${workbook.SheetNames.length} "${sheetName}" 변환 중...`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const safeSheetName = sanitizeSheetName(sheetName);
|
|
||||||
const worksheet = workbook.Sheets[sheetName];
|
|
||||||
|
|
||||||
if (!worksheet) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ 시트 "${sheetName}"를 찾을 수 없습니다. 빈 시트로 생성합니다.`,
|
|
||||||
);
|
|
||||||
|
|
||||||
luckySheets.push({
|
|
||||||
id: `sheet_${index}`,
|
|
||||||
name: safeSheetName,
|
|
||||||
data: [[""]],
|
|
||||||
config: {
|
|
||||||
container: `luckysheet_${index}`,
|
|
||||||
title: safeSheetName,
|
|
||||||
lang: "ko",
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
name: safeSheetName,
|
|
||||||
index: index.toString(),
|
|
||||||
celldata: [],
|
|
||||||
status: 1,
|
|
||||||
order: index,
|
|
||||||
row: 100,
|
|
||||||
column: 26,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
options: {
|
|
||||||
showtoolbar: true,
|
|
||||||
showinfobar: false,
|
|
||||||
showsheetbar: true,
|
|
||||||
showstatisticBar: false,
|
|
||||||
allowCopy: true,
|
|
||||||
allowEdit: true,
|
|
||||||
enableAddRow: true,
|
|
||||||
enableAddCol: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 시트 범위 확인
|
|
||||||
const range = worksheet["!ref"];
|
|
||||||
if (!range) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ 시트 "${sheetName}"에 데이터 범위가 없습니다. 빈 시트로 처리합니다.`,
|
|
||||||
);
|
|
||||||
|
|
||||||
luckySheets.push({
|
|
||||||
id: `sheet_${index}`,
|
|
||||||
name: safeSheetName,
|
|
||||||
data: [[""]],
|
|
||||||
config: {
|
|
||||||
container: `luckysheet_${index}`,
|
|
||||||
title: safeSheetName,
|
|
||||||
lang: "ko",
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
name: safeSheetName,
|
|
||||||
index: index.toString(),
|
|
||||||
celldata: [],
|
|
||||||
status: 1,
|
|
||||||
order: index,
|
|
||||||
row: 100,
|
|
||||||
column: 26,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
options: {
|
|
||||||
showtoolbar: true,
|
|
||||||
showinfobar: false,
|
|
||||||
showsheetbar: true,
|
|
||||||
showstatisticBar: false,
|
|
||||||
allowCopy: true,
|
|
||||||
allowEdit: true,
|
|
||||||
enableAddRow: true,
|
|
||||||
enableAddCol: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 범위 파싱
|
|
||||||
const rangeObj = XLSX.utils.decode_range(range);
|
|
||||||
const maxRow = rangeObj.e.r + 1;
|
|
||||||
const maxCol = rangeObj.e.c + 1;
|
|
||||||
|
|
||||||
console.log(`📐 시트 "${sheetName}" 크기: ${maxRow}행 x ${maxCol}열`);
|
|
||||||
|
|
||||||
// 2D 배열로 데이터 변환
|
|
||||||
const data: any[][] = [];
|
|
||||||
const cellData: any[] = [];
|
|
||||||
|
|
||||||
// 데이터 배열 초기화
|
|
||||||
for (let row = 0; row < maxRow; row++) {
|
|
||||||
data[row] = new Array(maxCol).fill("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 셀 데이터 변환
|
|
||||||
for (let row = rangeObj.s.r; row <= rangeObj.e.r; row++) {
|
|
||||||
for (let col = rangeObj.s.c; col <= rangeObj.e.c; col++) {
|
|
||||||
const cellAddress = XLSX.utils.encode_cell({ r: row, c: col });
|
|
||||||
const cell = worksheet[cellAddress];
|
|
||||||
|
|
||||||
if (
|
|
||||||
cell &&
|
|
||||||
cell.v !== undefined &&
|
|
||||||
cell.v !== null &&
|
|
||||||
cell.v !== ""
|
|
||||||
) {
|
|
||||||
let cellValue = cell.v;
|
|
||||||
if (typeof cellValue === "string") {
|
|
||||||
cellValue = cellValue.trim();
|
|
||||||
if (cellValue.length > 1000) {
|
|
||||||
cellValue = cellValue.substring(0, 997) + "...";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2D 배열에 데이터 저장
|
|
||||||
data[row][col] = cellValue;
|
|
||||||
|
|
||||||
// LuckyExcel celldata 형식으로 변환
|
|
||||||
const luckyCell: any = {
|
|
||||||
r: row,
|
|
||||||
c: col,
|
|
||||||
v: {
|
|
||||||
v: cellValue,
|
|
||||||
m: String(cellValue),
|
|
||||||
ct: { fa: "General", t: "g" },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 셀 타입에 따른 추가 처리
|
|
||||||
if (cell.t === "s") {
|
|
||||||
luckyCell.v.ct.t = "s";
|
|
||||||
} else if (cell.t === "n") {
|
|
||||||
luckyCell.v.ct.t = "n";
|
|
||||||
} else if (cell.t === "d") {
|
|
||||||
luckyCell.v.ct.t = "d";
|
|
||||||
} else if (cell.t === "b") {
|
|
||||||
luckyCell.v.ct.t = "b";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cell.f) {
|
|
||||||
luckyCell.v.f = cell.f;
|
|
||||||
}
|
|
||||||
|
|
||||||
cellData.push(luckyCell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SheetData 객체 생성
|
|
||||||
const sheetData: SheetData = {
|
|
||||||
id: `sheet_${index}`,
|
|
||||||
name: safeSheetName,
|
|
||||||
data: data,
|
|
||||||
config: {
|
|
||||||
container: `luckysheet_${index}`,
|
|
||||||
title: safeSheetName,
|
|
||||||
lang: "ko",
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
name: safeSheetName,
|
|
||||||
index: index.toString(),
|
|
||||||
celldata: cellData,
|
|
||||||
status: 1,
|
|
||||||
order: index,
|
|
||||||
row: maxRow,
|
|
||||||
column: maxCol,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
options: {
|
|
||||||
showtoolbar: true,
|
|
||||||
showinfobar: false,
|
|
||||||
showsheetbar: true,
|
|
||||||
showstatisticBar: false,
|
|
||||||
allowCopy: true,
|
|
||||||
allowEdit: true,
|
|
||||||
enableAddRow: true,
|
|
||||||
enableAddCol: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
luckySheets.push(sheetData);
|
|
||||||
console.log(`✅ 시트 "${sheetName}" 변환 완료: ${cellData.length}개 셀`);
|
|
||||||
} catch (sheetError) {
|
|
||||||
console.error(`❌ 시트 "${sheetName}" 변환 중 오류:`, sheetError);
|
|
||||||
|
|
||||||
// 오류 발생 시 빈 시트로 생성
|
|
||||||
luckySheets.push({
|
|
||||||
id: `sheet_${index}`,
|
|
||||||
name: safeSheetName,
|
|
||||||
data: [[""]],
|
|
||||||
config: {
|
|
||||||
container: `luckysheet_${index}`,
|
|
||||||
title: safeSheetName,
|
|
||||||
lang: "ko",
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
name: safeSheetName,
|
|
||||||
index: index.toString(),
|
|
||||||
celldata: [],
|
|
||||||
status: 1,
|
|
||||||
order: index,
|
|
||||||
row: 100,
|
|
||||||
column: 26,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
options: {
|
|
||||||
showtoolbar: true,
|
|
||||||
showinfobar: false,
|
|
||||||
showsheetbar: true,
|
|
||||||
showstatisticBar: false,
|
|
||||||
allowCopy: true,
|
|
||||||
allowEdit: true,
|
|
||||||
enableAddRow: true,
|
|
||||||
enableAddCol: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 최소 1개 시트는 보장
|
|
||||||
if (luckySheets.length === 0) {
|
|
||||||
console.log("📄 시트가 없어서 기본 시트를 생성합니다.");
|
|
||||||
luckySheets.push({
|
|
||||||
id: "sheet_0",
|
|
||||||
name: "Sheet1",
|
|
||||||
data: [[""]],
|
|
||||||
config: {
|
|
||||||
container: "luckysheet_0",
|
|
||||||
title: "Sheet1",
|
|
||||||
lang: "ko",
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
name: "Sheet1",
|
|
||||||
index: "0",
|
|
||||||
celldata: [],
|
|
||||||
status: 1,
|
|
||||||
order: 0,
|
|
||||||
row: 100,
|
|
||||||
column: 26,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
options: {
|
|
||||||
showtoolbar: true,
|
|
||||||
showinfobar: false,
|
|
||||||
showsheetbar: true,
|
|
||||||
showstatisticBar: false,
|
|
||||||
allowCopy: true,
|
|
||||||
allowEdit: true,
|
|
||||||
enableAddRow: true,
|
|
||||||
enableAddCol: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`🎉 SheetJS → LuckyExcel 변환 완료: ${luckySheets.length}개 시트`,
|
|
||||||
);
|
|
||||||
return luckySheets;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SheetJS로 파일을 읽고 XLSX로 변환한 뒤 LuckyExcel로 처리
|
|
||||||
*/
|
|
||||||
async function processFileWithSheetJSToXLSX(file: File): Promise<SheetData[]> {
|
|
||||||
console.log("📊 SheetJS → XLSX → LuckyExcel 파이프라인 시작...");
|
|
||||||
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
|
||||||
const fileName = file.name.toLowerCase();
|
|
||||||
const isCSV = fileName.endsWith(".csv");
|
|
||||||
const isXLS = fileName.endsWith(".xls");
|
|
||||||
const isXLSX = fileName.endsWith(".xlsx");
|
|
||||||
|
|
||||||
// 1단계: SheetJS로 파일 읽기
|
|
||||||
let workbook: any;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (isCSV) {
|
|
||||||
// CSV 파일 처리 - UTF-8 디코딩 후 읽기
|
|
||||||
console.log("📄 CSV 파일을 SheetJS로 읽는 중...");
|
|
||||||
const text = new TextDecoder("utf-8").decode(arrayBuffer);
|
|
||||||
workbook = XLSX.read(text, {
|
|
||||||
type: "string",
|
|
||||||
codepage: 65001, // UTF-8
|
|
||||||
raw: false,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// XLS/XLSX 파일 처리 - 관대한 옵션으로 읽기
|
|
||||||
console.log(`📊 ${isXLS ? "XLS" : "XLSX"} 파일을 SheetJS로 읽는 중...`);
|
|
||||||
workbook = XLSX.read(arrayBuffer, {
|
|
||||||
type: "array",
|
|
||||||
cellText: true,
|
|
||||||
sheetStubs: true,
|
|
||||||
WTF: true,
|
|
||||||
bookSheets: false,
|
|
||||||
codepage: 65001,
|
|
||||||
raw: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sheets가 없고 SheetNames만 있는 경우 재시도
|
|
||||||
if (workbook.SheetNames?.length > 0 && !workbook.Sheets) {
|
|
||||||
console.log("⚠️ Sheets 속성이 없어서 재읽기 시도...");
|
|
||||||
workbook = XLSX.read(arrayBuffer, {
|
|
||||||
type: "array",
|
|
||||||
cellText: true,
|
|
||||||
sheetStubs: true,
|
|
||||||
WTF: true,
|
|
||||||
bookSheets: true, // 강제로 시트 읽기
|
|
||||||
codepage: 65001,
|
|
||||||
raw: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 여전히 실패하면 수동으로 빈 시트 생성
|
|
||||||
if (!workbook.Sheets && workbook.SheetNames?.length > 0) {
|
|
||||||
console.log("⚠️ 수동으로 빈 시트 생성...");
|
|
||||||
workbook.Sheets = {};
|
|
||||||
workbook.SheetNames.forEach((sheetName: string) => {
|
|
||||||
workbook.Sheets[sheetName] = {
|
|
||||||
"!ref": "A1:A1",
|
|
||||||
A1: { v: "", t: "s" },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (readError) {
|
|
||||||
console.error("❌ SheetJS 파일 읽기 실패:", readError);
|
|
||||||
throw new Error(
|
|
||||||
`파일을 읽을 수 없습니다: ${readError instanceof Error ? readError.message : readError}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 파일 버퍼 크기 검증
|
|
||||||
if (arrayBuffer.byteLength === 0) {
|
|
||||||
throw new Error("파일이 비어있습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 워크북 null 체크
|
|
||||||
if (!workbook) {
|
|
||||||
throw new Error("워크북을 생성할 수 없습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// workbook.Sheets 존재 및 타입 검증
|
|
||||||
if (!workbook.Sheets || typeof workbook.Sheets !== "object") {
|
|
||||||
throw new Error("유효한 시트가 없습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// workbook.SheetNames 배열 검증
|
|
||||||
if (!Array.isArray(workbook.SheetNames) || workbook.SheetNames.length === 0) {
|
|
||||||
throw new Error("시트 이름 정보가 없습니다.");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ SheetJS 워크북 읽기 성공:", {
|
|
||||||
sheetNames: workbook.SheetNames,
|
|
||||||
sheetCount: workbook.SheetNames.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2단계: 워크북을 XLSX ArrayBuffer로 변환
|
|
||||||
let xlsxArrayBuffer: ArrayBuffer;
|
|
||||||
try {
|
|
||||||
console.log("🔄 XLSX 형식으로 변환 중...");
|
|
||||||
const xlsxData = XLSX.write(workbook, {
|
|
||||||
type: "array",
|
|
||||||
bookType: "xlsx",
|
|
||||||
compression: true,
|
|
||||||
});
|
|
||||||
// xlsxData는 Uint8Array이므로 ArrayBuffer로 변환
|
|
||||||
if (xlsxData instanceof Uint8Array) {
|
|
||||||
xlsxArrayBuffer = xlsxData.buffer.slice(
|
|
||||||
xlsxData.byteOffset,
|
|
||||||
xlsxData.byteOffset + xlsxData.byteLength,
|
|
||||||
);
|
|
||||||
} else if (xlsxData instanceof ArrayBuffer) {
|
|
||||||
xlsxArrayBuffer = xlsxData;
|
|
||||||
} else {
|
|
||||||
// 다른 타입의 경우 새 ArrayBuffer 생성
|
|
||||||
xlsxArrayBuffer = new ArrayBuffer(xlsxData.length);
|
|
||||||
const view = new Uint8Array(xlsxArrayBuffer);
|
|
||||||
for (let i = 0; i < xlsxData.length; i++) {
|
|
||||||
view[i] = xlsxData[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✅ XLSX 변환 완료: ${xlsxArrayBuffer.byteLength} bytes`);
|
|
||||||
|
|
||||||
// ⏱️ ArrayBuffer 변환 완료 확인 및 검증
|
|
||||||
console.log("⏱️ ArrayBuffer 변환 검증 중...");
|
|
||||||
|
|
||||||
// ArrayBuffer 무결성 검증
|
|
||||||
if (!xlsxArrayBuffer || xlsxArrayBuffer.byteLength === 0) {
|
|
||||||
throw new Error("ArrayBuffer 변환 실패: 빈 버퍼");
|
|
||||||
}
|
|
||||||
|
|
||||||
// XLSX 파일 시그니처 사전 검증
|
|
||||||
const uint8Check = new Uint8Array(xlsxArrayBuffer);
|
|
||||||
const signatureCheck = Array.from(uint8Check.slice(0, 4))
|
|
||||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
if (signatureCheck !== "50 4b 03 04") {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ 잘못된 XLSX 시그니처: ${signatureCheck} (예상: 50 4b 03 04)`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`✅ ArrayBuffer 검증 완료: ${xlsxArrayBuffer.byteLength} bytes, 시그니처: ${signatureCheck}`,
|
|
||||||
);
|
|
||||||
} catch (writeError) {
|
|
||||||
console.error("❌ XLSX 변환 실패:", writeError);
|
|
||||||
throw new Error(
|
|
||||||
`XLSX 변환 실패: ${writeError instanceof Error ? writeError.message : writeError}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3단계: ArrayBuffer가 완전히 준비된 후 LuckyExcel로 처리
|
|
||||||
console.log("🍀 LuckyExcel로 변환된 XLSX 처리 중...");
|
|
||||||
|
|
||||||
// ArrayBuffer 최종 검증
|
|
||||||
if (!xlsxArrayBuffer) {
|
|
||||||
throw new Error("ArrayBuffer가 생성되지 않았습니다");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (xlsxArrayBuffer.byteLength === 0) {
|
|
||||||
throw new Error("ArrayBuffer 크기가 0입니다");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 원본 파일명에서 확장자를 .xlsx로 변경
|
|
||||||
const xlsxFileName = file.name.replace(/\.(csv|xls|xlsx)$/i, ".xlsx");
|
|
||||||
|
|
||||||
// 🔍 LuckyExcel로 전달되는 파일 정보 출력
|
|
||||||
console.log("📋 =================================");
|
|
||||||
console.log("📋 LuckyExcel로 전달되는 파일 정보:");
|
|
||||||
console.log("📋 =================================");
|
|
||||||
console.log("📋 타이밍:", new Date().toISOString());
|
|
||||||
console.log("📋 원본 파일명:", file.name);
|
|
||||||
console.log("📋 변환된 파일명:", xlsxFileName);
|
|
||||||
console.log("📋 ArrayBuffer 크기:", xlsxArrayBuffer.byteLength, "bytes");
|
|
||||||
console.log("📋 ArrayBuffer 타입:", xlsxArrayBuffer.constructor.name);
|
|
||||||
|
|
||||||
// ArrayBuffer의 처음 100바이트를 16진수로 출력 (헥스 덤프)
|
|
||||||
const uint8View = new Uint8Array(xlsxArrayBuffer);
|
|
||||||
const firstBytes = Array.from(
|
|
||||||
uint8View.slice(0, Math.min(100, uint8View.length)),
|
|
||||||
)
|
|
||||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
||||||
.join(" ");
|
|
||||||
console.log("📋 ArrayBuffer 처음 100바이트 (hex):", firstBytes);
|
|
||||||
|
|
||||||
// XLSX 파일 시그니처 확인 (PK\x03\x04 또는 50 4B 03 04)
|
|
||||||
const signature = Array.from(uint8View.slice(0, 4))
|
|
||||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
||||||
.join(" ");
|
|
||||||
console.log(
|
|
||||||
"📋 파일 시그니처:",
|
|
||||||
signature,
|
|
||||||
signature === "50 4b 03 04" ? "(✅ 유효한 XLSX)" : "(❌ 잘못된 시그니처)",
|
|
||||||
);
|
|
||||||
console.log("📋 =================================");
|
|
||||||
|
|
||||||
// 🚀 LuckyExcel 호출 직전 최종 검증
|
|
||||||
console.log("🚀 LuckyExcel 호출 직전 최종 검증:");
|
|
||||||
console.log("🚀 ArrayBuffer 타입:", typeof xlsxArrayBuffer);
|
|
||||||
console.log("🚀 ArrayBuffer 생성자 확인:", xlsxArrayBuffer.constructor.name);
|
|
||||||
console.log("🚀 ArrayBuffer 크기:", xlsxArrayBuffer.byteLength);
|
|
||||||
console.log("🚀 ArrayBuffer.isView:", ArrayBuffer.isView(xlsxArrayBuffer));
|
|
||||||
console.log("🚀 fileName:", xlsxFileName, "타입:", typeof xlsxFileName);
|
|
||||||
|
|
||||||
console.log("🚀 LuckyExcel 객체:", typeof LuckyExcel);
|
|
||||||
console.log(
|
|
||||||
"🚀 transformExcelToLucky 함수:",
|
|
||||||
typeof (LuckyExcel as any).transformExcelToLucky,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("🚀 LuckyExcel 호출 시작...");
|
|
||||||
|
|
||||||
// Promise를 사용한 LuckyExcel 처리
|
|
||||||
return new Promise<SheetData[]>((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
// LuckyExcel API는 (arrayBuffer, successCallback, errorCallback) 형태로 호출
|
|
||||||
// 공식 문서: LuckyExcel.transformExcelToLucky(file, successCallback, errorCallback)
|
|
||||||
(LuckyExcel as any).transformExcelToLucky(
|
|
||||||
xlsxArrayBuffer,
|
|
||||||
// 성공 콜백 함수 (두 번째 매개변수)
|
|
||||||
(exportJson: any, luckysheetfile: any) => {
|
|
||||||
try {
|
|
||||||
console.log("🍀 =================================");
|
|
||||||
console.log("🍀 LuckyExcel 변환 결과 상세 정보:");
|
|
||||||
console.log("🍀 =================================");
|
|
||||||
console.log("🍀 원본 파일명:", xlsxFileName);
|
|
||||||
console.log("🍀 exportJson 존재:", !!exportJson);
|
|
||||||
console.log("🍀 exportJson 타입:", typeof exportJson);
|
|
||||||
|
|
||||||
if (exportJson) {
|
|
||||||
console.log("🍀 exportJson 전체 구조:", exportJson);
|
|
||||||
console.log("🍀 exportJson.sheets 존재:", !!exportJson.sheets);
|
|
||||||
console.log(
|
|
||||||
"🍀 exportJson.sheets 타입:",
|
|
||||||
typeof exportJson.sheets,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
"🍀 exportJson.sheets 배열 여부:",
|
|
||||||
Array.isArray(exportJson.sheets),
|
|
||||||
);
|
|
||||||
console.log("🍀 시트 개수:", exportJson?.sheets?.length || 0);
|
|
||||||
|
|
||||||
if (exportJson.sheets && Array.isArray(exportJson.sheets)) {
|
|
||||||
exportJson.sheets.forEach((sheet: any, index: number) => {
|
|
||||||
console.log(`🍀 시트 ${index + 1}:`, {
|
|
||||||
name: sheet.name,
|
|
||||||
row: sheet.row,
|
|
||||||
column: sheet.column,
|
|
||||||
celldata길이: sheet.celldata?.length || 0,
|
|
||||||
키목록: 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(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 {
|
|
||||||
console.log("🔄 SheetJS 방식으로 대체 처리...");
|
|
||||||
const fallbackSheets = convertSheetJSToLuckyExcel(workbook);
|
|
||||||
resolve(fallbackSheets);
|
|
||||||
} catch (fallbackError) {
|
|
||||||
console.error("❌ SheetJS 대체 처리도 실패:", fallbackError);
|
|
||||||
reject(fallbackError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 오류 콜백 함수 (세 번째 매개변수)
|
|
||||||
(error: any) => {
|
|
||||||
console.error("❌ LuckyExcel 변환 오류:", error);
|
|
||||||
|
|
||||||
// LuckyExcel 오류 시 SheetJS 방식으로 대체
|
|
||||||
try {
|
|
||||||
console.log("🔄 SheetJS 방식으로 대체 처리...");
|
|
||||||
const fallbackSheets = convertSheetJSToLuckyExcel(workbook);
|
|
||||||
resolve(fallbackSheets);
|
|
||||||
} catch (fallbackError) {
|
|
||||||
console.error("❌ SheetJS 대체 처리도 실패:", fallbackError);
|
|
||||||
reject(fallbackError);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (luckyError) {
|
|
||||||
console.error("❌ LuckyExcel 호출 중 오류:", luckyError);
|
|
||||||
|
|
||||||
// LuckyExcel 호출 실패 시 SheetJS 방식으로 대체
|
|
||||||
try {
|
|
||||||
console.log("🔄 SheetJS 방식으로 대체 처리...");
|
|
||||||
const fallbackSheets = convertSheetJSToLuckyExcel(workbook);
|
|
||||||
resolve(fallbackSheets);
|
|
||||||
} catch (fallbackError) {
|
|
||||||
console.error("❌ SheetJS 대체 처리도 실패:", fallbackError);
|
|
||||||
reject(fallbackError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 엑셀 파일을 SheetData 배열로 변환 (개선된 버전)
|
|
||||||
* - 모든 파일을 SheetJS로 읽은 후 XLSX로 변환
|
|
||||||
* - 변환된 XLSX를 LuckyExcel로 처리
|
|
||||||
* - 실패 시 SheetJS 직접 변환으로 Fallback
|
|
||||||
*/
|
|
||||||
export async function processExcelFile(file: File): Promise<FileUploadResult> {
|
|
||||||
try {
|
|
||||||
const errorMessage = getFileErrorMessage(file);
|
|
||||||
if (errorMessage) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: errorMessage,
|
|
||||||
fileName: file.name,
|
|
||||||
fileSize: file.size,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 파일 형식 감지
|
|
||||||
const fileName = file.name.toLowerCase();
|
|
||||||
const isCSV = fileName.endsWith(".csv");
|
|
||||||
const isXLS = fileName.endsWith(".xls");
|
|
||||||
const isXLSX = fileName.endsWith(".xlsx");
|
|
||||||
|
|
||||||
if (!isCSV && !isXLS && !isXLSX) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error:
|
|
||||||
"지원되지 않는 파일 형식입니다. .csv, .xls, .xlsx 파일을 사용해주세요.",
|
|
||||||
fileName: file.name,
|
|
||||||
fileSize: file.size,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`📁 파일 처리 시작: ${file.name} (${isCSV ? "CSV" : isXLS ? "XLS" : "XLSX"})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 통합된 처리 방식: SheetJS → XLSX → LuckyExcel
|
|
||||||
const sheets = await processFileWithSheetJSToXLSX(file);
|
|
||||||
|
|
||||||
if (!sheets || sheets.length === 0) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: "파일에서 유효한 시트를 찾을 수 없습니다.",
|
|
||||||
fileName: file.name,
|
|
||||||
fileSize: file.size,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`🎉 파일 처리 완료: ${sheets.length}개 시트`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: sheets,
|
|
||||||
fileName: file.name,
|
|
||||||
fileSize: file.size,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ 파일 처리 중 오류 발생:", error);
|
|
||||||
|
|
||||||
let errorMessage = "파일을 읽는 중 오류가 발생했습니다.";
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
if (
|
|
||||||
error.message.includes("파일에 워크시트가 없습니다") ||
|
|
||||||
error.message.includes("워크북 구조 오류") ||
|
|
||||||
error.message.includes("파일 처리 실패") ||
|
|
||||||
error.message.includes("파일 읽기 실패") ||
|
|
||||||
error.message.includes("XLSX 변환 실패") ||
|
|
||||||
error.message.includes("파일이 비어있습니다") ||
|
|
||||||
error.message.includes("워크북을 생성할 수 없습니다") ||
|
|
||||||
error.message.includes("유효한 시트가 없습니다") ||
|
|
||||||
error.message.includes("시트 이름 정보가 없습니다") ||
|
|
||||||
error.message.includes("파일을 읽을 수 없습니다")
|
|
||||||
) {
|
|
||||||
errorMessage = error.message;
|
|
||||||
} else if (error.message.includes("transformExcelToLucky")) {
|
|
||||||
errorMessage =
|
|
||||||
"Excel 파일 변환에 실패했습니다. 파일이 손상되었거나 지원되지 않는 형식일 수 있습니다.";
|
|
||||||
} else {
|
|
||||||
errorMessage = `파일 처리 중 오류: ${error.message}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: errorMessage,
|
|
||||||
fileName: file.name,
|
|
||||||
fileSize: file.size,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 여러 파일 중 유효한 파일만 필터링
|
|
||||||
*/
|
|
||||||
export function filterValidFiles(files: FileList): File[] {
|
|
||||||
return Array.from(files).filter((file) => {
|
|
||||||
const errorMessage = getFileErrorMessage(file);
|
|
||||||
return errorMessage === "";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 파일 목록의 에러 정보 수집
|
|
||||||
*/
|
|
||||||
export function getFileErrors(
|
|
||||||
files: FileList,
|
|
||||||
): { file: File; error: string }[] {
|
|
||||||
const errors: { file: File; error: string }[] = [];
|
|
||||||
|
|
||||||
Array.from(files).forEach((file) => {
|
|
||||||
const errorMessage = getFileErrorMessage(file);
|
|
||||||
if (errorMessage !== "") {
|
|
||||||
errors.push({ file, error: errorMessage });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
1290
src/utils/fileProcessor.ts.bak
Normal file
1290
src/utils/fileProcessor.ts.bak
Normal file
File diff suppressed because it is too large
Load Diff
1
src/utils/luckysheetApi.ts
Normal file
1
src/utils/luckysheetApi.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
417
src/utils/styleTest.ts.bak
Normal file
417
src/utils/styleTest.ts.bak
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
/**
|
||||||
|
* xlsx-js-style 스타일 보존 테스트 유틸리티
|
||||||
|
* - 다양한 스타일이 적용된 Excel 파일 생성
|
||||||
|
* - 스타일 정보 확인 도구
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as XLSX from "xlsx-js-style";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스타일이 적용된 테스트 Excel 파일 생성
|
||||||
|
*/
|
||||||
|
export function createStyledTestExcel(): ArrayBuffer {
|
||||||
|
// 새 워크북 생성
|
||||||
|
const wb = XLSX.utils.book_new();
|
||||||
|
|
||||||
|
// 테스트 데이터 생성 - xlsx-js-style 공식 API 완전 활용
|
||||||
|
const testData = [
|
||||||
|
// 첫 번째 행 - 폰트 스타일 테스트
|
||||||
|
[
|
||||||
|
{
|
||||||
|
v: "굵은 글씨",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
font: {
|
||||||
|
name: "Courier", // 공식 문서 예시
|
||||||
|
sz: 24, // 공식 문서 예시
|
||||||
|
bold: true,
|
||||||
|
color: { rgb: "000000" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: "빨간 글씨",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
font: {
|
||||||
|
bold: true,
|
||||||
|
color: { rgb: "FF0000" }, // 공식 문서: {color: {rgb: "FF0000"}}
|
||||||
|
sz: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: "테마 색상",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
font: {
|
||||||
|
color: { theme: 4 }, // 공식 문서: {theme: 4} (Blue, Accent 1)
|
||||||
|
sz: 14,
|
||||||
|
italic: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 두 번째 행 - 배경색 테스트
|
||||||
|
[
|
||||||
|
{
|
||||||
|
v: "노란 배경",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
fill: {
|
||||||
|
patternType: "solid",
|
||||||
|
fgColor: { rgb: "FFFF00" }, // 공식 문서: {fgColor: {rgb: "E9E9E9"}}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: "테마 배경",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
fill: {
|
||||||
|
patternType: "solid",
|
||||||
|
fgColor: { theme: 1, tint: 0.4 }, // 공식 문서: {theme: 1, tint: 0.4} ("Blue, Accent 1, Lighter 40%")
|
||||||
|
},
|
||||||
|
font: { color: { rgb: "000000" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: "인덱스 색상",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
fill: {
|
||||||
|
patternType: "solid",
|
||||||
|
fgColor: { indexed: 5 }, // Excel 기본 색상표 - 노랑
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 세 번째 행 - 테두리 테스트
|
||||||
|
[
|
||||||
|
{
|
||||||
|
v: "얇은 테두리",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
border: {
|
||||||
|
top: { style: "thin", color: { rgb: "000000" } },
|
||||||
|
bottom: { style: "thin", color: { rgb: "000000" } },
|
||||||
|
left: { style: "thin", color: { rgb: "000000" } },
|
||||||
|
right: { style: "thin", color: { rgb: "000000" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: "두꺼운 테두리",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
border: {
|
||||||
|
top: { style: "thick", color: { theme: 2 } }, // 테마 색상 사용
|
||||||
|
bottom: { style: "thick", color: { theme: 2 } },
|
||||||
|
left: { style: "thick", color: { theme: 2 } },
|
||||||
|
right: { style: "thick", color: { theme: 2 } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: "다양한 테두리",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
border: {
|
||||||
|
top: { style: "dotted", color: { indexed: 4 } }, // 인덱스 색상 - 파랑
|
||||||
|
bottom: { style: "dashed", color: { indexed: 4 } },
|
||||||
|
left: { style: "dashDot", color: { indexed: 4 } },
|
||||||
|
right: { style: "double", color: { indexed: 4 } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 네 번째 행 - 복합 스타일 테스트
|
||||||
|
[
|
||||||
|
{
|
||||||
|
v: "복합 스타일",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
font: {
|
||||||
|
bold: true,
|
||||||
|
italic: true,
|
||||||
|
underline: true,
|
||||||
|
sz: 16,
|
||||||
|
color: { rgb: "FFFFFF" },
|
||||||
|
name: "Courier", // 공식 문서 예시
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
patternType: "solid",
|
||||||
|
fgColor: { theme: 7, tint: -0.2 }, // 어두운 보라색
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
top: { style: "medium", color: { rgb: "FFD700" } },
|
||||||
|
bottom: { style: "medium", color: { rgb: "FFD700" } },
|
||||||
|
left: { style: "medium", color: { rgb: "FFD700" } },
|
||||||
|
right: { style: "medium", color: { rgb: "FFD700" } },
|
||||||
|
},
|
||||||
|
alignment: {
|
||||||
|
horizontal: "center",
|
||||||
|
vertical: "middle",
|
||||||
|
wrapText: true, // 공식 문서: {wrapText: true}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: 1234.567,
|
||||||
|
t: "n",
|
||||||
|
s: {
|
||||||
|
numFmt: "0.00%", // 공식 문서: numFmt 예시
|
||||||
|
alignment: { horizontal: "right" },
|
||||||
|
font: { bold: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: "줄바꿈\n테스트",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
alignment: {
|
||||||
|
wrapText: true,
|
||||||
|
vertical: "top",
|
||||||
|
textRotation: 0, // 공식 문서: textRotation
|
||||||
|
},
|
||||||
|
font: { sz: 10 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 다섯 번째 행 - 고급 스타일 테스트
|
||||||
|
[
|
||||||
|
{
|
||||||
|
v: "취소선 텍스트",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
font: {
|
||||||
|
strike: true, // 공식 문서: {strike: true}
|
||||||
|
sz: 12,
|
||||||
|
color: { theme: 5, tint: 0.6 }, // 밝은 빨강
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: "회전 텍스트",
|
||||||
|
t: "s",
|
||||||
|
s: {
|
||||||
|
alignment: {
|
||||||
|
textRotation: 45, // 공식 문서: textRotation
|
||||||
|
horizontal: "center",
|
||||||
|
vertical: "middle",
|
||||||
|
},
|
||||||
|
font: { sz: 14, bold: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
v: new Date(),
|
||||||
|
t: "d",
|
||||||
|
s: {
|
||||||
|
numFmt: "m/dd/yy", // 공식 문서: 날짜 포맷 예시
|
||||||
|
font: { name: "Arial", sz: 10 },
|
||||||
|
alignment: { horizontal: "center" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// 워크시트 생성
|
||||||
|
const ws = XLSX.utils.aoa_to_sheet(testData);
|
||||||
|
|
||||||
|
// 병합 셀 추가
|
||||||
|
if (!ws["!merges"]) ws["!merges"] = [];
|
||||||
|
ws["!merges"].push(
|
||||||
|
{ s: { r: 0, c: 0 }, e: { r: 0, c: 1 } }, // A1:B1 병합
|
||||||
|
{ s: { r: 2, c: 2 }, e: { r: 3, c: 2 } }, // C3:C4 병합
|
||||||
|
);
|
||||||
|
|
||||||
|
// 열 너비 설정
|
||||||
|
ws["!cols"] = [
|
||||||
|
{ wpx: 120 }, // A열 너비
|
||||||
|
{ wpx: 100 }, // B열 너비
|
||||||
|
{ wpx: 80 }, // C열 너비
|
||||||
|
];
|
||||||
|
|
||||||
|
// 행 높이 설정
|
||||||
|
ws["!rows"] = [
|
||||||
|
{ hpx: 30 }, // 1행 높이
|
||||||
|
{ hpx: 25 }, // 2행 높이
|
||||||
|
{ hpx: 40 }, // 3행 높이
|
||||||
|
];
|
||||||
|
|
||||||
|
// 워크시트를 워크북에 추가
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws, "스타일테스트");
|
||||||
|
|
||||||
|
// 추가 시트 생성 (간단한 데이터)
|
||||||
|
const simpleData = [
|
||||||
|
["이름", "나이", "직업"],
|
||||||
|
["홍길동", 30, "개발자"],
|
||||||
|
["김철수", 25, "디자이너"],
|
||||||
|
["이영희", 35, "기획자"],
|
||||||
|
];
|
||||||
|
|
||||||
|
const ws2 = XLSX.utils.aoa_to_sheet(simpleData);
|
||||||
|
|
||||||
|
// 헤더 스타일 적용
|
||||||
|
["A1", "B1", "C1"].forEach((cellAddr) => {
|
||||||
|
if (ws2[cellAddr]) {
|
||||||
|
ws2[cellAddr].s = {
|
||||||
|
font: { bold: true, color: { rgb: "FFFFFF" } },
|
||||||
|
fill: { patternType: "solid", fgColor: { rgb: "5B9BD5" } },
|
||||||
|
alignment: { horizontal: "center" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws2, "간단한데이터");
|
||||||
|
|
||||||
|
// Excel 파일로 변환
|
||||||
|
const excelBuffer = XLSX.write(wb, {
|
||||||
|
type: "array",
|
||||||
|
bookType: "xlsx",
|
||||||
|
cellStyles: true,
|
||||||
|
cellDates: true,
|
||||||
|
bookSST: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ArrayBuffer로 변환
|
||||||
|
if (excelBuffer instanceof Uint8Array) {
|
||||||
|
return excelBuffer.buffer.slice(
|
||||||
|
excelBuffer.byteOffset,
|
||||||
|
excelBuffer.byteOffset + excelBuffer.byteLength,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return excelBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 셀 스타일 정보 분석
|
||||||
|
*/
|
||||||
|
export function analyzeSheetStyles(workbook: any): void {
|
||||||
|
console.log("🎨 =================================");
|
||||||
|
console.log("🎨 Excel 파일 스타일 정보 분석");
|
||||||
|
console.log("🎨 =================================");
|
||||||
|
|
||||||
|
// 🔍 워크북 전체 스타일 정보 확인
|
||||||
|
console.log("🔍 워크북 메타데이터:", {
|
||||||
|
Props: workbook.Props ? "있음" : "없음",
|
||||||
|
Custprops: workbook.Custprops ? "있음" : "없음",
|
||||||
|
Workbook: workbook.Workbook ? "있음" : "없음",
|
||||||
|
SSF: workbook.SSF ? "있음" : "없음",
|
||||||
|
SheetNames: workbook.SheetNames
|
||||||
|
? workbook.SheetNames.length + "개"
|
||||||
|
: "없음",
|
||||||
|
Sheets: workbook.Sheets
|
||||||
|
? Object.keys(workbook.Sheets).length + "개"
|
||||||
|
: "없음",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔍 워크북 스타일 정보 상세 분석
|
||||||
|
if (workbook.SSF) {
|
||||||
|
console.log("🔍 워크북 SSF 스타일 정보:", workbook.SSF);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workbook.Workbook && workbook.Workbook.Styles) {
|
||||||
|
console.log("🔍 워크북 Styles:", workbook.Workbook.Styles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 워크북의 모든 키 확인
|
||||||
|
console.log("🔍 워크북 전체 키들:", Object.keys(workbook));
|
||||||
|
|
||||||
|
if (!workbook.SheetNames || workbook.SheetNames.length === 0) {
|
||||||
|
console.log("🎨 ❌ 시트가 없습니다.");
|
||||||
|
console.log("🎨 ❌ 워크북 전체 구조:", Object.keys(workbook));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
workbook.SheetNames.forEach((sheetName: string, sheetIndex: number) => {
|
||||||
|
const sheet = workbook.Sheets[sheetName];
|
||||||
|
if (!sheet) return;
|
||||||
|
|
||||||
|
console.log(`🎨 시트 ${sheetIndex + 1}: "${sheetName}"`);
|
||||||
|
|
||||||
|
// 시트 메타데이터
|
||||||
|
console.log(`🎨 - 데이터 범위: ${sheet["!ref"] || "없음"}`);
|
||||||
|
console.log(`🎨 - 병합 셀: ${sheet["!merges"]?.length || 0}개`);
|
||||||
|
console.log(`🎨 - 열 설정: ${sheet["!cols"]?.length || 0}개`);
|
||||||
|
console.log(`🎨 - 행 설정: ${sheet["!rows"]?.length || 0}개`);
|
||||||
|
|
||||||
|
// 병합 셀 상세 정보
|
||||||
|
if (sheet["!merges"]) {
|
||||||
|
sheet["!merges"].forEach((merge: any, index: number) => {
|
||||||
|
console.log(
|
||||||
|
`🎨 - 병합 ${index + 1}: ${XLSX.utils.encode_cell(merge.s)}:${XLSX.utils.encode_cell(merge.e)}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 스타일이 적용된 셀 찾기
|
||||||
|
const styledCells: string[] = [];
|
||||||
|
const cellAddresses = Object.keys(sheet).filter(
|
||||||
|
(key) => !key.startsWith("!"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔍 시트 데이터 존재 여부 확인
|
||||||
|
console.log(`🔍 ${sheetName} 기본 정보: ${cellAddresses.length}개 셀 발견`);
|
||||||
|
|
||||||
|
cellAddresses.forEach((cellAddr) => {
|
||||||
|
const cell = sheet[cellAddr];
|
||||||
|
if (cell && cell.s) {
|
||||||
|
styledCells.push(cellAddr);
|
||||||
|
|
||||||
|
// 🔍 첫 3개 셀의 실제 스타일 구조 확인
|
||||||
|
if (styledCells.length <= 3) {
|
||||||
|
console.log(`🔍 셀 ${cellAddr} cell.s 원시값:`, cell.s);
|
||||||
|
console.log(`🔍 cell.s 타입:`, typeof cell.s);
|
||||||
|
console.log(`🔍 cell.s 키들:`, Object.keys(cell.s || {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 스타일 정보 간단 확인
|
||||||
|
const hasStyles = {
|
||||||
|
font: !!cell.s.font,
|
||||||
|
fill: !!cell.s.fill,
|
||||||
|
border: !!cell.s.border,
|
||||||
|
alignment: !!cell.s.alignment,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Object.values(hasStyles).some((v) => v)) {
|
||||||
|
console.log(`🎨 셀 ${cellAddr} 스타일:`, hasStyles);
|
||||||
|
} else if (styledCells.length <= 3) {
|
||||||
|
console.log(`❌ 셀 ${cellAddr} 스타일 없음:`, hasStyles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`🎨 - 스타일 적용된 셀: ${styledCells.length}개 (${styledCells.join(", ")})`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("🎨 =================================");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 브라우저에서 테스트 파일 다운로드
|
||||||
|
*/
|
||||||
|
export function downloadTestFile(): void {
|
||||||
|
try {
|
||||||
|
const buffer = createStyledTestExcel();
|
||||||
|
const blob = new Blob([buffer], {
|
||||||
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = "스타일테스트.xlsx";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
console.log("🎨 스타일 테스트 파일 다운로드 완료!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("🎨 테스트 파일 생성 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
147
src/vite-env.d.ts
vendored
147
src/vite-env.d.ts
vendored
@@ -61,3 +61,150 @@ 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: {
|
||||||
|
create: (options: {
|
||||||
|
container: string;
|
||||||
|
data: any[];
|
||||||
|
title?: string;
|
||||||
|
userInfo?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}) => void;
|
||||||
|
destroy: () => void;
|
||||||
|
resize?: () => void;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
LuckyExcel: {
|
||||||
|
transformExcelToLucky: (
|
||||||
|
file: File | ArrayBuffer,
|
||||||
|
successCallback: (exportJson: any, luckysheetfile: any) => void,
|
||||||
|
errorCallback: (error: any) => void,
|
||||||
|
) => void;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
$: any; // jQuery
|
||||||
|
Store?: any; // Luckysheet Store
|
||||||
|
luckysheet_function?: any; // Luckysheet function list
|
||||||
|
functionlist?: any[]; // 글로벌 functionlist
|
||||||
|
luckysheetConfigsetting?: any; // Luckysheet 설정 객체
|
||||||
|
luckysheetPostil?: any; // Luckysheet 포스틸 객체
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
|
|||||||
@@ -22,5 +22,6 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"],
|
||||||
|
"exclude": ["**/__tests__/**", "**/*.test.ts", "**/*.test.tsx"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "./tsconfig.app.json" },
|
{
|
||||||
{ "path": "./tsconfig.node.json" }
|
"path": "./tsconfig.app.json"
|
||||||
]
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"exclude": ["**/__tests__/**", "**/*.test.ts", "**/*.test.tsx"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,81 @@
|
|||||||
/// <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()],
|
||||||
|
|
||||||
|
// Node.js 호환성 문제 해결
|
||||||
|
define: {
|
||||||
|
global: "globalThis",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Node.js 모듈 호환성 설정
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
stream: "stream-browserify",
|
||||||
|
buffer: "buffer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 의존성 최적화 설정
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: [
|
||||||
|
// 중복 로딩 방지를 위해 redi와 univer 관련 제외
|
||||||
|
"@wendellhu/redi",
|
||||||
|
"@univerjs/core",
|
||||||
|
"@univerjs/design",
|
||||||
|
"@univerjs/ui",
|
||||||
|
"@univerjs/sheets",
|
||||||
|
"@univerjs/sheets-ui",
|
||||||
|
"@univerjs/docs",
|
||||||
|
"@univerjs/docs-ui",
|
||||||
|
"@univerjs/engine-render",
|
||||||
|
"@univerjs/engine-formula",
|
||||||
|
"@univerjs/sheets-formula",
|
||||||
|
"@univerjs/sheets-formula-ui",
|
||||||
|
"@univerjs/sheets-numfmt",
|
||||||
|
"@univerjs/sheets-numfmt-ui",
|
||||||
|
"@univerjs/facade",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// 빌드 설정
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
external: [],
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
// Univer 관련 라이브러리를 별도 청크로 분리
|
||||||
|
"univer-core": [
|
||||||
|
"@univerjs/core",
|
||||||
|
"@univerjs/design",
|
||||||
|
"@univerjs/engine-render",
|
||||||
|
"@univerjs/engine-formula",
|
||||||
|
],
|
||||||
|
"univer-sheets": [
|
||||||
|
"@univerjs/sheets",
|
||||||
|
"@univerjs/sheets-ui",
|
||||||
|
"@univerjs/sheets-formula",
|
||||||
|
"@univerjs/sheets-formula-ui",
|
||||||
|
"@univerjs/sheets-numfmt",
|
||||||
|
"@univerjs/sheets-numfmt-ui",
|
||||||
|
],
|
||||||
|
"univer-docs": ["@univerjs/docs", "@univerjs/docs-ui"],
|
||||||
|
"univer-ui": ["@univerjs/ui", "@univerjs/facade"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 서버 설정
|
||||||
|
server: {
|
||||||
|
fs: {
|
||||||
|
strict: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// @ts-ignore - vitest config
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
|
|||||||
Reference in New Issue
Block a user