From d96dd6b7a2d2985496fb38b2b07b436bca569880 Mon Sep 17 00:00:00 2001 From: LeeDonMin Date: Fri, 4 Apr 2025 12:03:37 +0900 Subject: [PATCH 1/5] test no pull --- package.json | 2 ++ pnpm-lock.yaml | 18 ++++++++++++ src/app/modal/chartinput-form.tsx | 49 +++++++++++++++++++++++++++++-- src/worker/csvWorker.ts | 14 +++++++++ 4 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 src/worker/csvWorker.ts diff --git a/package.json b/package.json index 424b6a4..ab11e5a 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.8", "@tanstack/react-table": "^8.21.2", + "@types/papaparse": "^5.3.15", "axios": "^1.8.4", "chart.js": "^4.4.8", "class-variance-authority": "^0.7.1", @@ -40,6 +41,7 @@ "jotai": "^2.12.2", "lucide-react": "^0.483.0", "next": "15.2.4", + "papaparse": "^5.5.2", "pretendard": "^1.3.9", "react": "^19.0.0", "react-chartjs": "^1.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06c6bd0..f407121 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ importers: '@tanstack/react-table': specifier: ^8.21.2 version: 8.21.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@types/papaparse': + specifier: ^5.3.15 + version: 5.3.15 axios: specifier: ^1.8.4 version: 1.8.4 @@ -95,6 +98,9 @@ importers: next: specifier: 15.2.4 version: 15.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + papaparse: + specifier: ^5.5.2 + version: 5.5.2 pretendard: specifier: ^1.3.9 version: 1.3.9 @@ -1063,6 +1069,9 @@ packages: '@types/node@20.17.25': resolution: {integrity: sha512-bT+r2haIlplJUYtlZrEanFHdPIZTeiMeh/fSOEbOOfWf9uTn+lg8g0KU6Q3iMgjd9FLuuMAgfCNSkjUbxL6E3Q==} + '@types/papaparse@5.3.15': + resolution: {integrity: sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw==} + '@types/react-chartjs-2@2.5.7': resolution: {integrity: sha512-waqYqiNULIVUqaKO7MGUpFmWrVtH7gVPOzqwV4y4zgUyu/JiDwC005PpveO442HKnby9kLgp3t1SB2sld+ACLw==} deprecated: This is a stub types definition for react-chartjs-2 (https://github.com/gor181/react-chartjs-2). react-chartjs-2 provides its own type definitions, so you don't need @types/react-chartjs-2 installed! @@ -2267,6 +2276,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + papaparse@5.5.2: + resolution: {integrity: sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -3636,6 +3648,10 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/papaparse@5.3.15': + dependencies: + '@types/node': 20.17.25 + '@types/react-chartjs-2@2.5.7(chart.js@4.4.8)(react@19.0.0)': dependencies: react-chartjs-2: 5.3.0(chart.js@4.4.8)(react@19.0.0) @@ -5011,6 +5027,8 @@ snapshots: dependencies: p-limit: 3.1.0 + papaparse@5.5.2: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 diff --git a/src/app/modal/chartinput-form.tsx b/src/app/modal/chartinput-form.tsx index 9f00db9..370d05e 100644 --- a/src/app/modal/chartinput-form.tsx +++ b/src/app/modal/chartinput-form.tsx @@ -1,5 +1,5 @@ 'use client'; -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Dialog, @@ -7,6 +7,7 @@ import { DialogHeader, DialogTitle, DialogClose, + DialogDescription, } from '@/components/ui/dialog'; import { ESGCombobox } from './combobox'; // ESG 항목 선택 컴포넌트 import DataTable from './datatable'; // 데이터 입력 테이블 컴포넌트 @@ -21,6 +22,8 @@ import { } from '@/components/ui/select'; // Select 관련 컴포넌트 추가 import { v4 as uuidv4 } from 'uuid'; import { ChartType, ChartData } from '@/types/chart'; // ChartDataset 임포트 제거 +import { useDropzone } from 'react-dropzone'; +import { Upload, X } from 'lucide-react'; interface ESGChartDialogProps { open: boolean; @@ -38,6 +41,30 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp const [labels, setLabels] = useState([]); // labels 상태 추가 const [datasets, setDatasets] = useState([]); // datasets 상태 타입 수정 const [isLoading, setIsLoading] = useState(false); // 로딩 상태 추가 + const [csvData, setCsvData] = useState([]); + const [worker, setWorker] = useState(null); + useEffect(() => { + // Web Worker 초기화 + const csvWorker = new Worker(new URL('../../worker/csvWorker.ts', import.meta.url), { + type: 'module', + }); + csvWorker.onmessage = event => { + setCsvData(event.data); // CSV 데이터 업데이트 + }; + setWorker(csvWorker); + + return () => { + csvWorker.terminate(); // 컴포넌트 언마운트 시 종료 + }; + }, []); + const handleFile = (acceptedFiles: File[]) => { + const file = acceptedFiles[0]; + if (file && worker) { + worker.postMessage(file); // Web Worker로 파일 전송 + } + const dataset: ChartData['datasets'] = []; + console.log(csvData); + }; const handleNext = () => { if (step === 'combobox') { @@ -148,7 +175,10 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp setLabels([]); // labels 초기화 setDatasets([]); // datasets 초기화 }; - + const { getRootProps, getInputProps } = useDropzone({ + onDrop: acceptedFiles => handleFile(acceptedFiles), + accept: { 'text/csv': ['.csv'] }, + }); // 차트 타입별 샘플 데이터 생성 함수 리팩토링 (더 이상 사용되지 않을 수 있으므로 주석 처리 또는 삭제 가능) /* const getSampleData = (type: ChartType): { labels?: string[], datasets?: ChartData['datasets'] } => { @@ -215,6 +245,7 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp return ( + test 차트 추가 )} + {step === 'datatable' && ( +
+ +
+ +

+ CSV 파일을 추가하려면 파일 선택
또는 여기로 파일을 끌고 오세요 +

+
+
+ )} {step === 'datatable' && (
{/* 데이터 입력 테이블에 콜백 및 초기값 전달 */} diff --git a/src/worker/csvWorker.ts b/src/worker/csvWorker.ts new file mode 100644 index 0000000..375bc1a --- /dev/null +++ b/src/worker/csvWorker.ts @@ -0,0 +1,14 @@ +import Papa from 'papaparse'; + +self.onmessage = event => { + const file = event.data; + console.log('Received file:', file); + Papa.parse(file, { + worker: true, // Web Worker에서 처리 + header: true, // CSV의 첫 줄을 헤더로 사용 + skipEmptyLines: true, // 빈 줄 제거 + complete: result => { + self.postMessage(result.data); // 파싱된 데이터 반환 + }, + }); +}; From 0a2e7730b4e8836bece3a77a3fd92f79072261c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=88=EB=AF=BC?= Date: Sun, 6 Apr 2025 17:20:39 +0900 Subject: [PATCH 2/5] =?UTF-8?q?web=20worker=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=8F=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=ED=8E=B8=EC=A7=91=20?= =?UTF-8?q?=EC=86=8D=EB=8F=84=20=ED=96=A5=EC=83=81=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 + pnpm-lock.yaml | 65 ++++++++ src/app/modal/chartinput-form.tsx | 221 +++++++++---------------- src/app/modal/datatable.tsx | 258 +++++++++++++++++++++--------- src/types/chart.ts | 2 +- src/worker/csvWorker.ts | 13 +- 6 files changed, 332 insertions(+), 230 deletions(-) diff --git a/package.json b/package.json index ab11e5a..a4c8984 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,9 @@ "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.8", "@tanstack/react-table": "^8.21.2", + "@tanstack/react-virtual": "^3.13.6", "@types/papaparse": "^5.3.15", + "@types/react-window": "^1.8.8", "axios": "^1.8.4", "chart.js": "^4.4.8", "class-variance-authority": "^0.7.1", @@ -52,6 +54,7 @@ "react-icons": "^5.5.0", "react-resizable-panels": "^2.1.7", "react-router-dom": "^7.4.0", + "react-window": "^1.8.11", "tailwind-merge": "^3.0.2", "tailwindcss-animate": "^1.0.7", "tw-animate-css": "^1.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f407121..b09e70d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,9 +65,15 @@ importers: '@tanstack/react-table': specifier: ^8.21.2 version: 8.21.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@tanstack/react-virtual': + specifier: ^3.13.6 + version: 3.13.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@types/papaparse': specifier: ^5.3.15 version: 5.3.15 + '@types/react-window': + specifier: ^1.8.8 + version: 1.8.8 axios: specifier: ^1.8.4 version: 1.8.4 @@ -131,6 +137,9 @@ importers: react-router-dom: specifier: ^7.4.0 version: 7.4.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react-window: + specifier: ^1.8.11 + version: 1.8.11(react-dom@19.0.0(react@19.0.0))(react@19.0.0) tailwind-merge: specifier: ^3.0.2 version: 3.0.2 @@ -199,6 +208,10 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@babel/runtime@7.27.0': + resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} + engines: {node: '>=6.9.0'} + '@emnapi/core@1.3.1': resolution: {integrity: sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==} @@ -1047,10 +1060,19 @@ packages: react: '>=16.8' react-dom: '>=16.8' + '@tanstack/react-virtual@3.13.6': + resolution: {integrity: sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/table-core@8.21.2': resolution: {integrity: sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==} engines: {node: '>=12'} + '@tanstack/virtual-core@3.13.6': + resolution: {integrity: sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==} + '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} @@ -1081,6 +1103,9 @@ packages: peerDependencies: '@types/react': ^19.0.0 + '@types/react-window@1.8.8': + resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} + '@types/react@19.0.12': resolution: {integrity: sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==} @@ -2135,6 +2160,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2445,6 +2473,13 @@ packages: '@types/react': optional: true + react-window@1.8.11: + resolution: {integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==} + engines: {node: '>8.0.0'} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react@19.0.0: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} @@ -2457,6 +2492,9 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -2808,6 +2846,10 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@babel/runtime@7.27.0': + dependencies: + regenerator-runtime: 0.14.1 + '@emnapi/core@1.3.1': dependencies: '@emnapi/wasi-threads': 1.0.1 @@ -3629,8 +3671,16 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + '@tanstack/react-virtual@3.13.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@tanstack/virtual-core': 3.13.6 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@tanstack/table-core@8.21.2': {} + '@tanstack/virtual-core@3.13.6': {} + '@tybys/wasm-util@0.9.0': dependencies: tslib: 2.8.1 @@ -3663,6 +3713,10 @@ snapshots: dependencies: '@types/react': 19.0.12 + '@types/react-window@1.8.8': + dependencies: + '@types/react': 19.0.12 + '@types/react@19.0.12': dependencies: csstype: 3.1.3 @@ -4870,6 +4924,8 @@ snapshots: math-intrinsics@1.1.0: {} + memoize-one@5.2.1: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -5170,6 +5226,13 @@ snapshots: optionalDependencies: '@types/react': 19.0.12 + react-window@1.8.11(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@babel/runtime': 7.27.0 + memoize-one: 5.2.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react@19.0.0: {} readable-stream@3.6.2: @@ -5189,6 +5252,8 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + regenerator-runtime@0.14.1: {} + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 diff --git a/src/app/modal/chartinput-form.tsx b/src/app/modal/chartinput-form.tsx index 370d05e..32cb36f 100644 --- a/src/app/modal/chartinput-form.tsx +++ b/src/app/modal/chartinput-form.tsx @@ -1,5 +1,5 @@ 'use client'; -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback, useEffect, useRef } from 'react'; import { Button } from '@/components/ui/button'; import { Dialog, @@ -9,8 +9,8 @@ import { DialogClose, DialogDescription, } from '@/components/ui/dialog'; -import { ESGCombobox } from './combobox'; // ESG 항목 선택 컴포넌트 -import DataTable from './datatable'; // 데이터 입력 테이블 컴포넌트 +import { ESGCombobox } from './combobox'; +import DataTable from './datatable'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { @@ -19,67 +19,81 @@ import { SelectItem, SelectTrigger, SelectValue, -} from '@/components/ui/select'; // Select 관련 컴포넌트 추가 +} from '@/components/ui/select'; import { v4 as uuidv4 } from 'uuid'; -import { ChartType, ChartData } from '@/types/chart'; // ChartDataset 임포트 제거 +import { ChartType, ChartData } from '@/types/chart'; import { useDropzone } from 'react-dropzone'; -import { Upload, X } from 'lucide-react'; +import { Upload } from 'lucide-react'; interface ESGChartDialogProps { open: boolean; setOpen: (open: boolean) => void; - onChartAdd?: (chart: ChartData) => void; // 차트 추가 콜백 + onChartAdd?: (chart: ChartData) => void; } export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProps) { - const [step, setStep] = useState<'combobox' | 'datatable'>('combobox'); // 현재 단계 상태 + const [step, setStep] = useState<'combobox' | 'datatable'>('combobox'); const [chartType, setChartType] = useState('bar'); const [chartTitle, setChartTitle] = useState(''); const [chartDescription, setChartDescription] = useState(''); const [colSpan, setColSpan] = useState<1 | 2 | 3 | 4>(1); - const [selectedESG, setSelectedESG] = useState(null); // ESG 항목 상태 추가 - const [labels, setLabels] = useState([]); // labels 상태 추가 - const [datasets, setDatasets] = useState([]); // datasets 상태 타입 수정 - const [isLoading, setIsLoading] = useState(false); // 로딩 상태 추가 - const [csvData, setCsvData] = useState([]); + const [selectedESG, setSelectedESG] = useState(null); + const [labels, setLabels] = useState([]); + const [datasets, setDatasets] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [tableKey, setTableKey] = useState(0); + const prevDataLength = useRef({ labels: 0, datasets: 0 }); const [worker, setWorker] = useState(null); + + // Web Worker 초기화 useEffect(() => { - // Web Worker 초기화 const csvWorker = new Worker(new URL('../../worker/csvWorker.ts', import.meta.url), { type: 'module', }); + csvWorker.onmessage = event => { - setCsvData(event.data); // CSV 데이터 업데이트 + const { labels, datasets } = event.data; + setLabels(labels); + setDatasets(datasets); }; + setWorker(csvWorker); return () => { - csvWorker.terminate(); // 컴포넌트 언마운트 시 종료 + csvWorker.terminate(); }; }, []); + const handleFile = (acceptedFiles: File[]) => { const file = acceptedFiles[0]; if (file && worker) { - worker.postMessage(file); // Web Worker로 파일 전송 + worker.postMessage(file); } - const dataset: ChartData['datasets'] = []; - console.log(csvData); }; + useEffect(() => { + if (!datasets || !labels) return; + if ( + prevDataLength.current.labels !== labels.length || + prevDataLength.current.datasets !== datasets.length + ) { + setTableKey(prevKey => prevKey + 1); + prevDataLength.current = { labels: labels.length, datasets: datasets.length }; + } + }, [labels, datasets]); + const handleNext = () => { if (step === 'combobox') { - setStep('datatable'); // 다음 단계로 전환 + setStep('datatable'); } else { handleSave(); } }; - // 이전 단계로 돌아가는 함수 추가 const handleBack = () => { setStep('combobox'); }; - // 데이터 테이블 변경 콜백 (DataTable에서 호출) const handleDataChange = useCallback( (newLabels: string[], newDatasets: ChartData['datasets']) => { setLabels(newLabels); @@ -88,13 +102,11 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp [] ); - // ESG 항목 변경 콜백 (ESGCombobox에서 호출) const handleESGChange = useCallback((value: string | null) => { setSelectedESG(value); }, []); const handleSave = async () => { - // async 추가 if (!chartTitle) { alert('차트 제목을 입력해주세요'); return; @@ -103,7 +115,7 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp alert('ESG 항목을 선택해주세요.'); return; } - // 데이터 유효성 검사 활성화 + if ( step === 'datatable' && (labels.length === 0 || @@ -115,24 +127,22 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp return; } - setIsLoading(true); // 로딩 시작 + setIsLoading(true); - // 새 차트 객체 생성 시 상태 값 사용 const newChart: ChartData = { id: uuidv4(), title: chartTitle, description: chartDescription, type: chartType, colSpan: colSpan, - esg: selectedESG, // ESG 항목 추가 (null 아님을 위에서 확인) - labels: labels, // 상태에서 가져온 labels 사용 - datasets: datasets, // 상태에서 가져온 datasets 사용 + esg: selectedESG, + labels: labels, + datasets: datasets, createdAt: new Date(), updatedAt: new Date(), }; try { - // 백엔드 API 호출 (엔드포인트는 예시) const response = await fetch('/api/charts', { method: 'POST', headers: { @@ -142,27 +152,21 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp }); if (!response.ok) { - // 오류 처리 (예: 사용자에게 알림) throw new Error(`API 오류: ${response.statusText}`); } - const savedChart = await response.json(); // 저장된 차트 데이터 (선택적) - console.log('차트 저장 성공:', savedChart); - - // 부모 컴포넌트로 전달 + const savedChart = await response.json(); if (onChartAdd) { - onChartAdd(savedChart); // 저장된 데이터 전달 (백엔드 응답 사용) + onChartAdd(savedChart); } - // 폼 초기화 및 닫기 resetForm(); setOpen(false); } catch (error) { console.error('차트 저장 실패:', error); - // 사용자에게 오류 알림 (예: alert 또는 toast 메시지) alert('차트 저장 중 오류가 발생했습니다.'); } finally { - setIsLoading(false); // 로딩 종료 + setIsLoading(false); } }; @@ -171,97 +175,33 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp setChartDescription(''); setChartType('bar'); setColSpan(1); - setSelectedESG(null); // ESG 항목 초기화 - setLabels([]); // labels 초기화 - setDatasets([]); // datasets 초기화 + setSelectedESG(null); + setLabels([]); + setDatasets([]); }; + const { getRootProps, getInputProps } = useDropzone({ onDrop: acceptedFiles => handleFile(acceptedFiles), accept: { 'text/csv': ['.csv'] }, }); - // 차트 타입별 샘플 데이터 생성 함수 리팩토링 (더 이상 사용되지 않을 수 있으므로 주석 처리 또는 삭제 가능) - /* - const getSampleData = (type: ChartType): { labels?: string[], datasets?: ChartData['datasets'] } => { - switch (type) { - case 'bar': - return { - labels: ['카테고리1', '카테고리2', '카테고리3'], - datasets: [ - { - label: '샘플 데이터', // label 추가 - data: [65, 78, 82], // data로 변경 - backgroundColor: ['blue', 'green', 'purple'] // backgroundColor로 변경 - } - ] - }; - case 'line': - return { - labels: ['1월', '2월', '3월', '4월', '5월', '6월'], - datasets: [ - { - label: 'Dataset 1', // label로 변경 - data: [65, 59, 80, 81, 56, 55] // data로 변경 - }, - { - label: 'Dataset 2', // label로 변경 - data: [28, 48, 40, 19, 86, 27] // data로 변경 - } - ] - }; - case 'pie': - case 'donut': // donut 타입 추가 - return { - labels: ['항목 1', '항목 2', '항목 3'], // labels 추가 - datasets: [ - { - label: '샘플 데이터', // label 추가 - data: [45, 30, 25], // data로 변경 (숫자형) - backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56'] // 배경색 추가 - } - ] - }; - case 'area': // area 타입 추가 - return { - labels: ['1월', '2월', '3월', '4월', '5월', '6월'], - datasets: [ - { - label: '샘플 데이터', // label 추가 - data: [30, 45, 40, 55, 60, 65], // data로 변경 - borderColor: 'rgb(75, 192, 192)', // borderColor 추가 - backgroundColor: 'rgba(75, 192, 192, 0.2)', // backgroundColor 추가 - fill: true // fill 속성 추가 - } - ] - }; - default: - // 모든 타입에 대해 처리했는지 확인 (never 타입 활용) - const exhaustiveCheck: never = type; - console.error(`Unhandled chart type: ${exhaustiveCheck}`); - return {}; - } - }; - */ return ( - test + CSV 업로드 후 차트를 설정하세요 차트 추가 setOpen(false)} - > + />
{step === 'combobox' && ( -
- {/* 차트 제목 입력 */} +
- +
- {/* ESG 항목 선택 */}
- {/* ESGCombobox에 콜백 및 값 전달 */}
- {/* 차트 설명 입력 */}
- +
- {/* 차트 유형 선택 */}
- {/* 차트 크기 선택 */}
-
- -

- CSV 파일을 추가하려면 파일 선택
또는 여기로 파일을 끌고 오세요 -

+ <> +
+ +
+ +

+ CSV 파일을 추가하려면 파일 선택
또는 여기로 파일을 끌고 오세요 +

+
-
- )} - {step === 'datatable' && ( -
- {/* 데이터 입력 테이블에 콜백 및 초기값 전달 */} + -
+ )}
- {/* 버튼 컨테이너 스타일 수정 및 이전 버튼 추가 */} -
+
{step === 'datatable' && ( - )} -
diff --git a/src/app/modal/datatable.tsx b/src/app/modal/datatable.tsx index 0e5aa84..20fc09d 100644 --- a/src/app/modal/datatable.tsx +++ b/src/app/modal/datatable.tsx @@ -8,6 +8,7 @@ import { TableRow, TableHead, TableCell, + TableFooter } from '@/components/ui/table'; import { Input } from '@/components/ui/input'; import { Plus, Trash2, Edit } from 'lucide-react'; @@ -25,7 +26,7 @@ export default function DataTable({ onDataChange, }: DataTableProps) { const [columns, setColumns] = React.useState(() => { - if (initialLabels && initialLabels.length > 0) { + if (initialLabels.length > 0) { const datasetLabels = initialDatasets?.map(ds => ds?.label || '') || []; return ['Label', ...datasetLabels]; } @@ -33,7 +34,7 @@ export default function DataTable({ }); const [rows, setRows] = React.useState(() => { - if (initialLabels && initialLabels.length > 0) { + if (initialLabels.length > 0) { return initialLabels.map((label, index) => ({ id: Date.now() + index, columns: [label, ...(initialDatasets?.map(ds => ds?.data?.[index] ?? '') || [])], @@ -44,6 +45,43 @@ export default function DataTable({ const [editingColumnIndex, setEditingColumnIndex] = React.useState(null); const [tempColumnName, setTempColumnName] = React.useState(''); + const inputRef = React.useRef(null); + + const containerRef = React.useRef(null); + const rowHeight = 48; + const [scrollTop, setScrollTop] = React.useState(0); + const visibleRowCount = 13; + + const totalHeight = rows.length * rowHeight; + const startIndex = Math.floor(scrollTop / rowHeight); + const endIndex = Math.min(rows.length, startIndex + visibleRowCount); + const visibleRows = rows.slice(startIndex, endIndex); + const topPadding = startIndex * rowHeight; + const bottomPadding = totalHeight - (endIndex * rowHeight); + const MAX_WHEEL_DELTA = 100; + React.useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const wheelHandler = (e: WheelEvent) => { + e.preventDefault(); + + const limitedDelta = Math.max(-MAX_WHEEL_DELTA, Math.min(MAX_WHEEL_DELTA, e.deltaY)); + const newScrollTop = Math.max( + 0, + Math.min(container.scrollHeight, container.scrollTop + limitedDelta) + ); + + container.scrollTop = newScrollTop; + }; + + container.addEventListener('wheel', wheelHandler, { passive: false }); + + return () => { + container.removeEventListener('wheel', wheelHandler); + }; + }, []); + React.useEffect(() => { const newLabels = rows.map(row => row.columns[0]); @@ -58,10 +96,21 @@ export default function DataTable({ onDataChange(newLabels, newDatasets); }, [rows, columns, onDataChange]); + React.useEffect(() => { + if (editingColumnIndex !== null && inputRef.current) { + inputRef.current.focus(); + } + }, [editingColumnIndex]); + const handleInputChange = (rowIndex: number, colIndex: number, value: string) => { - const updatedRows = [...rows]; - updatedRows[rowIndex].columns[colIndex] = value; - setRows(updatedRows); + setRows(prevRows => { + const updated = [...prevRows]; + const row = { ...updated[rowIndex] }; + row.columns = [...row.columns]; + row.columns[colIndex] = value; + updated[rowIndex] = row; + return updated; + }); }; const addRow = () => { @@ -80,8 +129,8 @@ export default function DataTable({ const handleColumnHeaderDoubleClick = (index: number) => { if (index === 0) return; - setEditingColumnIndex(index); setTempColumnName(columns[index]); + setEditingColumnIndex(index); }; const handleColumnNameChange = (event: React.ChangeEvent) => { @@ -103,78 +152,135 @@ export default function DataTable({ } }; + const handleScroll = (e: React.UIEvent) => { + setScrollTop(e.currentTarget.scrollTop); + }; + + + + + const EditableCell = React.memo( + ({ + value, + onChange, + placeholder, + }: { + value: string; + onChange: (value: string) => void; + placeholder: string; + }) => { + const [localValue, setLocalValue] = React.useState(value); + + React.useEffect(() => { + setLocalValue(value); + }, [value]); + + const handleBlur = () => { + if (localValue !== value) { + onChange(localValue); + } + // Blur the input to remove focus + }; + + return ( + e.stopPropagation()} + // onMouseDown={(e) => e.stopPropagation()} + onChange={e => {setLocalValue(e.target.value);}} + onBlur={handleBlur} + placeholder={placeholder} + className="text-sm h-8 min-w-full" + /> + + ); + } + ); + return ( -
-
-
- - - - {columns.map((col, colIndex) => ( - 0 ? 'cursor-pointer' : ''}`} - onDoubleClick={() => handleColumnHeaderDoubleClick(colIndex)} - > - {editingColumnIndex === colIndex ? ( - - ) : ( - col - )} - - ))} - - - - - - - {rows.map(row => ( - - {row.columns.map((cell, colIndex) => ( - - - handleInputChange( - rows.findIndex(r => r.id === row.id), - colIndex, - e.target.value - ) - } - placeholder={`Enter ${columns[colIndex]}`} - /> - - ))} - - - - - +
+ +
+ + + {columns.map((col, colIndex) => ( + 0 ? 'cursor-pointer' : ''}`} + onDoubleClick={() => handleColumnHeaderDoubleClick(colIndex)} + > + + {editingColumnIndex === colIndex ? ( + + + + ) : ( + + + <>{col} + + )} + + + + ))} + + + + + + + + + + + {visibleRows.map((row, i) => ( + + {row.columns.map((cell, colIndex) => ( + + handleInputChange(startIndex + i, colIndex, val)} + placeholder={`Enter ${columns[colIndex]}`} + /> + ))} - -
-
-
- + + + + + + ))} + + + + + + +
+ ); } diff --git a/src/types/chart.ts b/src/types/chart.ts index 0f8687e..b5842f3 100644 --- a/src/types/chart.ts +++ b/src/types/chart.ts @@ -38,7 +38,7 @@ export interface ChartData { esg: string; // ESG 항목 식별자 추가 labels?: string[]; // labels 속성 추가 (선택적) // datasets와 options는 chart.js 핵심 타입을 사용하도록 수정 - datasets?: ChartDataset[]; + datasets?: ChartDataset[]; options?: ChartOptions; colSpan?: number; createdAt?: Date; // 기존 createdAt 속성 (가정) diff --git a/src/worker/csvWorker.ts b/src/worker/csvWorker.ts index 375bc1a..6edb31b 100644 --- a/src/worker/csvWorker.ts +++ b/src/worker/csvWorker.ts @@ -1,14 +1,19 @@ import Papa from 'papaparse'; - +import { ChartType, ChartData } from '@/types/chart'; // ChartType과 ChartData 타입 임포트 self.onmessage = event => { const file = event.data; console.log('Received file:', file); Papa.parse(file, { worker: true, // Web Worker에서 처리 - header: true, // CSV의 첫 줄을 헤더로 사용 + header: false, // CSV의 첫 줄을 헤더로 사용 skipEmptyLines: true, // 빈 줄 제거 - complete: result => { - self.postMessage(result.data); // 파싱된 데이터 반환 + complete: result => { + const labels = Array.from({length: result.data.length}, (_, i) => i+1); // 1부터 시작하는 레이블 생성 + const dataset: ChartData['datasets'] = (result.data[0] as string[][]).map((_, colindex) => { + return ({label:`${colindex+1}`,data:(result.data as string[][]).map(row => row[colindex])}); + }); + + self.postMessage({labels:labels,datasets:dataset}); // 파싱된 데이터 반환 }, }); }; From abcc5e9921727869ee8afa5e3c34d9d02a965b2e Mon Sep 17 00:00:00 2001 From: Gyeoul Date: Sun, 6 Apr 2025 21:21:21 +0900 Subject: [PATCH 3/5] Merge branch 'LDM-9' into LDM-10 --- src/app/modal/chartinput-form.tsx | 88 ++++++++++++++++++++++++++----- src/app/modal/datatable.tsx | 9 ++-- src/types/chart.ts | 8 +-- 3 files changed, 83 insertions(+), 22 deletions(-) diff --git a/src/app/modal/chartinput-form.tsx b/src/app/modal/chartinput-form.tsx index 32cb36f..974b237 100644 --- a/src/app/modal/chartinput-form.tsx +++ b/src/app/modal/chartinput-form.tsx @@ -1,6 +1,8 @@ 'use client'; import { useState, useCallback, useEffect, useRef } from 'react'; +import { useDropzone } from 'react-dropzone'; import { Button } from '@/components/ui/button'; +import { Upload, X } from 'lucide-react'; import { Dialog, DialogContent, @@ -22,8 +24,7 @@ import { } from '@/components/ui/select'; import { v4 as uuidv4 } from 'uuid'; import { ChartType, ChartData } from '@/types/chart'; -import { useDropzone } from 'react-dropzone'; -import { Upload } from 'lucide-react'; +import { Value } from '@radix-ui/react-select'; interface ESGChartDialogProps { open: boolean; @@ -38,9 +39,10 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp const [chartDescription, setChartDescription] = useState(''); const [colSpan, setColSpan] = useState<1 | 2 | 3 | 4>(1); const [selectedESG, setSelectedESG] = useState(null); - const [labels, setLabels] = useState([]); + const [labels, setLabels] = useState([]); const [datasets, setDatasets] = useState([]); const [isLoading, setIsLoading] = useState(false); + const [file, setFile] = useState(); const [tableKey, setTableKey] = useState(0); const prevDataLength = useRef({ labels: 0, datasets: 0 }); const [worker, setWorker] = useState(null); @@ -64,13 +66,6 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp }; }, []); - const handleFile = (acceptedFiles: File[]) => { - const file = acceptedFiles[0]; - if (file && worker) { - worker.postMessage(file); - } - }; - useEffect(() => { if (!datasets || !labels) return; if ( @@ -82,6 +77,13 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp } }, [labels, datasets]); + const handleFile = (acceptedFiles: File[]) => { + const file = acceptedFiles[0]; + if (file && worker) { + worker.postMessage(file); + } + }; + const handleNext = () => { if (step === 'combobox') { setStep('datatable'); @@ -95,7 +97,7 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp }; const handleDataChange = useCallback( - (newLabels: string[], newDatasets: ChartData['datasets']) => { + (newLabels: string[] | number[], newDatasets: ChartData['datasets']) => { setLabels(newLabels); setDatasets(newDatasets); }, @@ -180,11 +182,56 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp setDatasets([]); }; + const onDrop = useCallback((acceptedFiles: File[]) => { + const file = acceptedFiles[0]; + setFile(file); + if (file) { + const reader = new FileReader(); + + reader.onload = (event) => { + if (!event.target?.result) return; + const csvText = event.target.result as string; + const parsedData = parseCSV(csvText); + + const dataset: ChartData['datasets'] = []; + parsedData.data.forEach((val, index) => { + dataset.push( + { + label: `${index + 1}`, // Provide a default string label + data: val, // Use the data array + } + ); + }); + setLabels(parsedData.labels); + setDatasets(dataset); + }; + console.log(labels, datasets); + reader.readAsText(file, "UTF-8"); + handleDataChange(labels, datasets); + } + }, [labels, datasets]); + const { getRootProps, getInputProps } = useDropzone({ - onDrop: acceptedFiles => handleFile(acceptedFiles), - accept: { 'text/csv': ['.csv'] }, + onDrop, + accept: { 'text/csv': ['.csv'], }, }); + const parseCSV = (csvText: string) => { + const rows = csvText.split("\n").map(row => row.trim()).filter(row => row); // 줄바꿈 기준으로 분리 + const labels = Array.from({ length: rows.length }, (_, i) => i); + const beforedata: number[][] = []; + for (let i = 0; i < rows.length; i++) { // 첫 번째 줄은 헤더이므로 건너뜀 + const value = rows[i].split(",").map(row => parseValue(row)); + beforedata.push(value); // 숫자로 변환 + } + const data = beforedata[0].map((_, colIndex) => + beforedata.map(row => row[colIndex]) + ); + return { labels, data }; + }; + + const parseValue = (value: any) => isNaN(value) ? value : Number(value); + return ( @@ -262,7 +309,20 @@ export function ESGChartDialog({ open, setOpen, onChartAdd }: ESGChartDialogProp
)} - + {step === 'datatable' && ( +
+ +
+ +

+ CSV 파일을 추가하려면 파일 선택
또는 여기로 파일을 끌고 오세요 +

+
+
+ )} {step === 'datatable' && ( <>
void; } @@ -25,6 +24,8 @@ export default function DataTable({ initialDatasets = [], onDataChange, }: DataTableProps) { + + const [columns, setColumns] = React.useState(() => { if (initialLabels.length > 0) { const datasetLabels = initialDatasets?.map(ds => ds?.label || '') || []; @@ -89,8 +90,8 @@ export default function DataTable({ label: colName, data: rows.map(row => { const value = row.columns[colIndex + 1]; - const numericValue = Number(value); - return isNaN(numericValue) ? 0 : numericValue; + const numericValue =value; + return numericValue; }), })); onDataChange(newLabels, newDatasets); diff --git a/src/types/chart.ts b/src/types/chart.ts index b5842f3..79fcc9e 100644 --- a/src/types/chart.ts +++ b/src/types/chart.ts @@ -8,16 +8,16 @@ export type ChartType = ChartTypeCore | 'area' | 'donut'; // 차트 유형별 데이터 인터페이스 export interface BarChartData { - categories: string[]; + categories: string[]|number[]; values: number[]; colors: string[]; } export interface LineChartData { - labels: string[]; + labels: string[]|number[]; datasets: Array<{ name: string; - values: number[]; + values:string[]|number[]; }>; } @@ -36,7 +36,7 @@ export interface ChartData { type: ChartType; // 애플리케이션용 타입 사용 description?: string; esg: string; // ESG 항목 식별자 추가 - labels?: string[]; // labels 속성 추가 (선택적) + labels?: string[]|number[]; // labels 속성 추가 (선택적) // datasets와 options는 chart.js 핵심 타입을 사용하도록 수정 datasets?: ChartDataset[]; options?: ChartOptions; From 6c6e4a5f762c85677a2a68e7cea929af81b72346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=88=EB=AF=BC?= Date: Mon, 7 Apr 2025 14:04:39 +0900 Subject: [PATCH 4/5] datatable.tsx --- src/app/modal/datatable.tsx | 153 ++++++++++++++++++++---------------- 1 file changed, 86 insertions(+), 67 deletions(-) diff --git a/src/app/modal/datatable.tsx b/src/app/modal/datatable.tsx index 20fc09d..22c8b13 100644 --- a/src/app/modal/datatable.tsx +++ b/src/app/modal/datatable.tsx @@ -13,6 +13,7 @@ import { import { Input } from '@/components/ui/input'; import { Plus, Trash2, Edit } from 'lucide-react'; import { ChartData } from '@/types/chart'; +import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'; interface DataTableProps { initialLabels?: string[]; @@ -59,6 +60,7 @@ export default function DataTable({ const topPadding = startIndex * rowHeight; const bottomPadding = totalHeight - (endIndex * rowHeight); const MAX_WHEEL_DELTA = 100; + React.useEffect(() => { const container = containerRef.current; if (!container) return; @@ -66,13 +68,26 @@ export default function DataTable({ const wheelHandler = (e: WheelEvent) => { e.preventDefault(); - const limitedDelta = Math.max(-MAX_WHEEL_DELTA, Math.min(MAX_WHEEL_DELTA, e.deltaY)); - const newScrollTop = Math.max( - 0, - Math.min(container.scrollHeight, container.scrollTop + limitedDelta) - ); + const absDeltaX = Math.abs(e.deltaX); + const absDeltaY = Math.abs(e.deltaY); - container.scrollTop = newScrollTop; + if (absDeltaX > absDeltaY) { + // x축 스크롤 + const limitedDeltaX = Math.max(-MAX_WHEEL_DELTA, Math.min(MAX_WHEEL_DELTA, e.deltaX)); + const newScrollLeft = Math.max( + 0, + Math.min(container.scrollWidth, container.scrollLeft + limitedDeltaX) + ); + container.scrollLeft = newScrollLeft; + } else { + // y축 스크롤 + const limitedDeltaY = Math.max(-MAX_WHEEL_DELTA, Math.min(MAX_WHEEL_DELTA, e.deltaY)); + const newScrollTop = Math.max( + 0, + Math.min(container.scrollHeight, container.scrollTop + limitedDeltaY) + ); + container.scrollTop = newScrollTop; + } }; container.addEventListener('wheel', wheelHandler, { passive: false }); @@ -82,7 +97,6 @@ export default function DataTable({ }; }, []); - React.useEffect(() => { const newLabels = rows.map(row => row.columns[0]); const newDatasets: ChartData['datasets'] = columns.slice(1).map((colName, colIndex) => ({ @@ -122,7 +136,7 @@ export default function DataTable({ }; const addColumn = () => { - const newColumnName = `데이터셋 ${columns.length}`; + const newColumnName = `${columns.length}`; setColumns([...columns, newColumnName]); setRows(rows.map(row => ({ ...row, columns: [...row.columns, ''] }))); }; @@ -198,72 +212,68 @@ export default function DataTable({ ); return ( -
+ {/* 스크롤 영역 */} +
- - - - - {columns.map((col, colIndex) => ( - 0 ? 'cursor-pointer' : ''}`} - onDoubleClick={() => handleColumnHeaderDoubleClick(colIndex)} - > - - {editingColumnIndex === colIndex ? ( - - - - ) : ( - - - <>{col} - - )} - - - - ))} - - +
+ +
+ + + {columns.map((col, colIndex) => ( + handleColumnHeaderDoubleClick(colIndex)} + > + {editingColumnIndex === colIndex ? ( + + ) : ( + col + )} + + ))} + + - + - - - +
+
+ +
+ {/* 테이블 바디 */} + + + + {visibleRows.map((row, i) => ( - + {row.columns.map((cell, colIndex) => ( - - handleInputChange(startIndex + i, colIndex, val)} - placeholder={`Enter ${columns[colIndex]}`} - /> - + + handleInputChange(startIndex + i, colIndex, val)} + placeholder={`Enter ${columns[colIndex]}`} + /> + ))} - + @@ -274,13 +284,22 @@ export default function DataTable({ ))} + + + - - -
+
+
+ {/* Add Row 버튼 */} +
+
+
+ ); } From 3aa3e679e1455a7d663f2f76ff23ef8a19ca9e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=88=EB=AF=BC?= Date: Mon, 7 Apr 2025 15:36:35 +0900 Subject: [PATCH 5/5] =?UTF-8?q?datatable=20=EA=B0=80=EB=A1=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=EB=B0=8F=20=ED=96=89=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20&=20header=20fixed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/modal/datatable.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/modal/datatable.tsx b/src/app/modal/datatable.tsx index 4f37a35..b397644 100644 --- a/src/app/modal/datatable.tsx +++ b/src/app/modal/datatable.tsx @@ -1,4 +1,5 @@ 'use client'; + import * as React from 'react'; import { Table, @@ -25,8 +26,6 @@ export default function DataTable({ initialDatasets = [], onDataChange, }: DataTableProps) { - - const [columns, setColumns] = React.useState(() => { if (initialLabels.length > 0) { const datasetLabels = initialDatasets?.map(ds => ds?.label || '') || []; @@ -294,7 +293,8 @@ export default function DataTable({
{/* Add Row 버튼 */} -
+
+ 열 추가