From 2050a22656869624cc8d39329a135c97224aae9c Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 14 Oct 2025 13:46:09 +0900 Subject: [PATCH 01/14] =?UTF-8?q?=EC=B0=A8=ED=8A=B8=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20=EA=B3=84=ED=9A=8D=20md=20=ED=8C=8C=EC=9D=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/CHART_SYSTEM_PLAN.md | 661 ++++++++++++++++++ 1 file changed, 661 insertions(+) create mode 100644 frontend/components/admin/dashboard/CHART_SYSTEM_PLAN.md diff --git a/frontend/components/admin/dashboard/CHART_SYSTEM_PLAN.md b/frontend/components/admin/dashboard/CHART_SYSTEM_PLAN.md new file mode 100644 index 00000000..b732dad2 --- /dev/null +++ b/frontend/components/admin/dashboard/CHART_SYSTEM_PLAN.md @@ -0,0 +1,661 @@ +# ๐Ÿ“Š ์ฐจํŠธ ์‹œ์Šคํ…œ ๊ตฌํ˜„ ๊ณ„ํš + +## ๊ฐœ์š” + +D3.js ๊ธฐ๋ฐ˜์˜ ๊ฐ•๋ ฅํ•œ ์ฐจํŠธ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•(DB ์ฟผ๋ฆฌ ๋˜๋Š” REST API)์œผ๋กœ ๊ฐ€์ ธ์™€ ๋‹ค์–‘ํ•œ ์ฐจํŠธ๋กœ ์‹œ๊ฐํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +--- + +## ๐ŸŽฏ ํ•ต์‹ฌ ์š”๊ตฌ์‚ฌํ•ญ + +### 1. ๋ฐ์ดํ„ฐ ์†Œ์Šค (2๊ฐ€์ง€ ๋ฐฉ์‹) + +#### A. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ปค๋„ฅ์…˜ + +- **ํ˜„์žฌ DB**: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ธฐ๋ณธ PostgreSQL ์—ฐ๊ฒฐ +- **์™ธ๋ถ€ DB**: ๊ธฐ์กด "์™ธ๋ถ€ ์ปค๋„ฅ์…˜ ๊ด€๋ฆฌ" ๋ฉ”๋‰ด์—์„œ ๋“ฑ๋ก๋œ ์ปค๋„ฅ์…˜๋งŒ ์‚ฌ์šฉ + - ์‹ ๊ทœ ์ปค๋„ฅ์…˜ ์ƒ์„ฑ์€ ์™ธ๋ถ€ ์ปค๋„ฅ์…˜ ๊ด€๋ฆฌ ๋ฉ”๋‰ด์—์„œ๋งŒ ๊ฐ€๋Šฅ + - ์ฐจํŠธ ์„ค์ •์—์„œ๋Š” ๋“ฑ๋ก๋œ ์ปค๋„ฅ์…˜ ๋ชฉ๋ก์—์„œ ์„ ํƒ๋งŒ ๊ฐ€๋Šฅ +- **์ฟผ๋ฆฌ ์ œํ•œ**: SELECT ๋ฌธ๋งŒ ํ—ˆ์šฉ (INSERT, UPDATE, DELETE, DROP ๋“ฑ ๊ธˆ์ง€) +- **์ฟผ๋ฆฌ ๊ฒ€์ฆ**: ์„œ๋ฒ„ ์ธก์—์„œ SQL Injection ๋ฐฉ์ง€ ๋ฐ ์ฟผ๋ฆฌ ํƒ€์ž… ๊ฒ€์ฆ + +#### B. REST API ํ˜ธ์ถœ + +- **HTTP Methods**: GET (๊ถŒ์žฅ) - ๋ฐ์ดํ„ฐ ์กฐํšŒ์— ์ถฉ๋ถ„ +- **๋ฐ์ดํ„ฐ ํ˜•์‹**: JSON ์‘๋‹ต๋งŒ ํ—ˆ์šฉ +- **ํ—ค๋” ์„ค์ •**: Authorization, Content-Type ๋“ฑ ์ปค์Šคํ…€ ํ—ค๋” ์ง€์› +- **์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ**: URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํ•„ํ„ฐ๋ง ์กฐ๊ฑด ์ „๋‹ฌ +- **์‘๋‹ต ํŒŒ์‹ฑ**: JSON ๊ตฌ์กฐ์—์„œ ์ฐจํŠธ ๋ฐ์ดํ„ฐ ์ถ”์ถœ +- **์—๋Ÿฌ ์ฒ˜๋ฆฌ**: HTTP ์ƒํƒœ ์ฝ”๋“œ ๋ฐ ํƒ€์ž„์•„์›ƒ ์ฒ˜๋ฆฌ + +> **์ฐธ๊ณ **: POST๋Š” ํ–ฅํ›„ ํ™•์žฅ (GraphQL, ๋ณต์žกํ•œ ํ•„ํ„ฐ๋ง)์„ ์œ„ํ•ด ์„ ํƒ์ ์œผ๋กœ ์ง€์› ๊ฐ€๋Šฅ + +### 2. ์ฐจํŠธ ํƒ€์ž… (D3.js ๊ธฐ๋ฐ˜) + +ํ˜„์žฌ ์ง€์› ์˜ˆ์ •: + +- **Bar Chart** (๋ง‰๋Œ€ ์ฐจํŠธ): ์ˆ˜ํ‰/์ˆ˜์ง ๋ง‰๋Œ€ +- **Line Chart** (์„  ์ฐจํŠธ): ๋‹จ์ผ/๋‹ค์ค‘ ์‹œ๋ฆฌ์ฆˆ +- **Area Chart** (์˜์—ญ ์ฐจํŠธ): ๋ˆ„์  ์˜์—ญ ์ง€์› +- **Pie Chart** (์› ์ฐจํŠธ): ๋„๋„› ์ฐจํŠธ ํฌํ•จ +- **Stacked Bar** (๋ˆ„์  ๋ง‰๋Œ€): ๋‹ค์ค‘ ์‹œ๋ฆฌ์ฆˆ ๋ˆ„์  +- **Combo Chart** (ํ˜ผํ•ฉ ์ฐจํŠธ): ๋ง‰๋Œ€ + ์„  ์กฐํ•ฉ + +### 3. ์ถ• ๋งคํ•‘ ์„ค์ • + +- **X์ถ•**: ์นดํ…Œ๊ณ ๋ฆฌ/์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ (๋ฌธ์ž์—ด, ๋‚ ์งœ) +- **Y์ถ•**: ์ˆซ์ž ๋ฐ์ดํ„ฐ (๋‹จ์ผ ๋˜๋Š” ๋‹ค์ค‘ ์„ ํƒ ๊ฐ€๋Šฅ) +- **๋‹ค์ค‘ Y์ถ•**: ์—ฌ๋Ÿฌ ์‹œ๋ฆฌ์ฆˆ๋ฅผ ํ•œ ์ฐจํŠธ์— ํ‘œ์‹œ (์˜ˆ: ๊ฐค๋Ÿญ์‹œ vs ์•„์ดํฐ ๋งค์ถœ) +- **์ž๋™ ๊ฐ์ง€**: ๋ฐ์ดํ„ฐ ํƒ€์ž…์— ๋”ฐ๋ผ ์ถ• ์ž๋™ ์ถ”์ฒœ +- **๋ฐ์ดํ„ฐ ๋ณ€ํ™˜**: ๋ฌธ์ž์—ด ๋‚ ์งœ๋ฅผ Date ๊ฐ์ฒด๋กœ ์ž๋™ ๋ณ€ํ™˜ + +### 4. ์ฐจํŠธ ์Šคํƒ€์ผ๋ง + +- **์ƒ‰์ƒ ํŒ”๋ ˆํŠธ**: ์‚ฌ์ „ ์ •์˜๋œ ์ƒ‰์ƒ ์„ธํŠธ ์„ ํƒ +- **์ปค์Šคํ…€ ์ƒ‰์ƒ**: ์‚ฌ์šฉ์ž ์ง€์ • ์ƒ‰์ƒ ์ž…๋ ฅ +- **๋ฒ”๋ก€**: ์œ„์น˜ ์„ค์ • (์ƒ๋‹จ, ํ•˜๋‹จ, ์ขŒ์ธก, ์šฐ์ธก, ์ˆจ๊น€) +- **์• ๋‹ˆ๋ฉ”์ด์…˜**: ์ฐจํŠธ ๋กœ๋“œ ์‹œ ๋ถ€๋“œ๋Ÿฌ์šด ์ „ํ™˜ ํšจ๊ณผ +- **ํˆดํŒ**: ๋ฐ์ดํ„ฐ ํฌ์ธํŠธ ํ˜ธ๋ฒ„ ์‹œ ์ƒ์„ธ ์ •๋ณด ํ‘œ์‹œ +- **๊ทธ๋ฆฌ๋“œ**: X/Y์ถ• ๊ทธ๋ฆฌ๋“œ ๋ผ์ธ ํ‘œ์‹œ/์ˆจ๊น€ + +--- + +## ๐Ÿ“ ํŒŒ์ผ ๊ตฌ์กฐ + +``` +frontend/components/admin/dashboard/ +โ”œโ”€โ”€ CHART_SYSTEM_PLAN.md # ์ด ํŒŒ์ผ +โ”œโ”€โ”€ types.ts # โœ… ๊ธฐ์กด (ํƒ€์ž… ํ™•์žฅ ํ•„์š”) +โ”œโ”€โ”€ ElementConfigModal.tsx # โœ… ๊ธฐ์กด (๋ฆฌํŒฉํ† ๋ง ํ•„์š”) +โ”‚ +โ”œโ”€โ”€ data-sources/ # ๐Ÿ†• ๋ฐ์ดํ„ฐ ์†Œ์Šค ๊ด€๋ จ +โ”‚ โ”œโ”€โ”€ DataSourceSelector.tsx # ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ ํƒ UI (DB vs API) +โ”‚ โ”œโ”€โ”€ DatabaseConfig.tsx # DB ์ปค๋„ฅ์…˜ ์„ค์ • UI +โ”‚ โ”œโ”€โ”€ ApiConfig.tsx # REST API ์„ค์ • UI +โ”‚ โ””โ”€โ”€ dataSourceUtils.ts # ๋ฐ์ดํ„ฐ ์†Œ์Šค ์œ ํ‹ธ๋ฆฌํ‹ฐ +โ”‚ +โ”œโ”€โ”€ chart-config/ # ๐Ÿ”„ ์ฐจํŠธ ์„ค์ • ๊ด€๋ จ (๋ฆฌํŒฉํ† ๋ง) +โ”‚ โ”œโ”€โ”€ QueryEditor.tsx # โœ… ๊ธฐ์กด (ํ™•์žฅ ํ•„์š”) +โ”‚ โ”œโ”€โ”€ ChartConfigPanel.tsx # โœ… ๊ธฐ์กด (ํ™•์žฅ ํ•„์š”) +โ”‚ โ”œโ”€โ”€ AxisMapper.tsx # ๐Ÿ†• ์ถ• ๋งคํ•‘ UI +โ”‚ โ”œโ”€โ”€ StyleConfig.tsx # ๐Ÿ†• ์Šคํƒ€์ผ ์„ค์ • UI +โ”‚ โ””โ”€โ”€ ChartPreview.tsx # ๐Ÿ†• ์‹ค์‹œ๊ฐ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ +โ”‚ +โ”œโ”€โ”€ charts/ # ๐Ÿ†• D3 ์ฐจํŠธ ์ปดํฌ๋„ŒํŠธ +โ”‚ โ”œโ”€โ”€ ChartRenderer.tsx # ์ฐจํŠธ ๋ Œ๋”๋Ÿฌ (๋ฉ”์ธ) +โ”‚ โ”œโ”€โ”€ BarChart.tsx # ๋ง‰๋Œ€ ์ฐจํŠธ +โ”‚ โ”œโ”€โ”€ LineChart.tsx # ์„  ์ฐจํŠธ +โ”‚ โ”œโ”€โ”€ AreaChart.tsx # ์˜์—ญ ์ฐจํŠธ +โ”‚ โ”œโ”€โ”€ PieChart.tsx # ์› ์ฐจํŠธ +โ”‚ โ”œโ”€โ”€ StackedBarChart.tsx # ๋ˆ„์  ๋ง‰๋Œ€ ์ฐจํŠธ +โ”‚ โ”œโ”€โ”€ ComboChart.tsx # ํ˜ผํ•ฉ ์ฐจํŠธ +โ”‚ โ”œโ”€โ”€ chartUtils.ts # ์ฐจํŠธ ์œ ํ‹ธ๋ฆฌํ‹ฐ +โ”‚ โ””โ”€โ”€ d3Helpers.ts # D3 ํ—ฌํผ ํ•จ์ˆ˜ +โ”‚ +โ””โ”€โ”€ CanvasElement.tsx # โœ… ๊ธฐ์กด (์ฐจํŠธ ๋ Œ๋”๋ง ํ†ตํ•ฉ) +``` + +--- + +## ๐Ÿ”ง ํƒ€์ž… ์ •์˜ ํ™•์žฅ + +### ๊ธฐ์กด ํƒ€์ž… ์—…๋ฐ์ดํŠธ + +```typescript +// types.ts + +// ๋ฐ์ดํ„ฐ ์†Œ์Šค ํƒ€์ž… ํ™•์žฅ +export interface ChartDataSource { + type: "database" | "api"; // 'static' ์ œ๊ฑฐ + + // DB ์ปค๋„ฅ์…˜ ๊ด€๋ จ + connectionType?: "current" | "external"; // ํ˜„์žฌ DB vs ์™ธ๋ถ€ DB + externalConnectionId?: string; // ์™ธ๋ถ€ DB ์ปค๋„ฅ์…˜ ID + query?: string; // SQL ์ฟผ๋ฆฌ (SELECT๋งŒ) + + // API ๊ด€๋ จ + endpoint?: string; // API URL + method?: "GET"; // HTTP ๋ฉ”์„œ๋“œ (GET๋งŒ ์ง€์›) + headers?: Record; // ์ปค์Šคํ…€ ํ—ค๋” + queryParams?: Record; // URL ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ + jsonPath?: string; // JSON ์‘๋‹ต์—์„œ ๋ฐ์ดํ„ฐ ์ถ”์ถœ ๊ฒฝ๋กœ (์˜ˆ: "data.results") + + // ๊ณตํ†ต + refreshInterval?: number; // ์ž๋™ ์ƒˆ๋กœ๊ณ ์นจ (์ดˆ) + lastExecuted?: string; // ๋งˆ์ง€๋ง‰ ์‹คํ–‰ ์‹œ๊ฐ„ + lastError?: string; // ๋งˆ์ง€๋ง‰ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ +} + +// ์™ธ๋ถ€ DB ์ปค๋„ฅ์…˜ ์ •๋ณด (๊ธฐ์กด ์™ธ๋ถ€ ์ปค๋„ฅ์…˜ ๊ด€๋ฆฌ์—์„œ ๊ฐ€์ ธ์˜ด) +export interface ExternalConnection { + id: string; + name: string; // ์‚ฌ์šฉ์ž ์ง€์ • ์ด๋ฆ„ (ํ‘œ์‹œ์šฉ) + type: "postgresql" | "mysql" | "mssql" | "oracle"; + // ๋‚˜๋จธ์ง€ ์ •๋ณด๋Š” ์™ธ๋ถ€ ์ปค๋„ฅ์…˜ ๊ด€๋ฆฌ์—์„œ๋งŒ ๊ด€๋ฆฌ +} + +// ์ฐจํŠธ ์„ค์ • ํ™•์žฅ +export interface ChartConfig { + // ์ถ• ๋งคํ•‘ + xAxis: string; // X์ถ• ํ•„๋“œ๋ช… + yAxis: string | string[]; // Y์ถ• ํ•„๋“œ๋ช… (๋‹ค์ค‘ ๊ฐ€๋Šฅ) + + // ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ + groupBy?: string; // ๊ทธ๋ฃนํ•‘ ํ•„๋“œ + aggregation?: "sum" | "avg" | "count" | "max" | "min"; + sortBy?: string; // ์ •๋ ฌ ๊ธฐ์ค€ ํ•„๋“œ + sortOrder?: "asc" | "desc"; // ์ •๋ ฌ ์ˆœ์„œ + limit?: number; // ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ ์ œํ•œ + + // ์Šคํƒ€์ผ + colors?: string[]; // ์ฐจํŠธ ์ƒ‰์ƒ ํŒ”๋ ˆํŠธ + title?: string; // ์ฐจํŠธ ์ œ๋ชฉ + showLegend?: boolean; // ๋ฒ”๋ก€ ํ‘œ์‹œ + legendPosition?: "top" | "bottom" | "left" | "right"; // ๋ฒ”๋ก€ ์œ„์น˜ + + // ์ถ• ์„ค์ • + xAxisLabel?: string; // X์ถ• ๋ผ๋ฒจ + yAxisLabel?: string; // Y์ถ• ๋ผ๋ฒจ + showGrid?: boolean; // ๊ทธ๋ฆฌ๋“œ ํ‘œ์‹œ + + // ์• ๋‹ˆ๋ฉ”์ด์…˜ + enableAnimation?: boolean; // ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ™œ์„ฑํ™” + animationDuration?: number; // ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹œ๊ฐ„ (ms) + + // ํˆดํŒ + showTooltip?: boolean; // ํˆดํŒ ํ‘œ์‹œ + tooltipFormat?: string; // ํˆดํŒ ํฌ๋งท (ํ…œํ”Œ๋ฆฟ) + + // ์ฐจํŠธ๋ณ„ ํŠน์ˆ˜ ์„ค์ • + barOrientation?: "vertical" | "horizontal"; // ๋ง‰๋Œ€ ๋ฐฉํ–ฅ + lineStyle?: "smooth" | "straight"; // ์„  ์Šคํƒ€์ผ + areaOpacity?: number; // ์˜์—ญ ํˆฌ๋ช…๋„ + pieInnerRadius?: number; // ๋„๋„› ์ฐจํŠธ ๋‚ด๋ถ€ ๋ฐ˜์ง€๋ฆ„ (0-1) + stackMode?: "normal" | "percent"; // ๋ˆ„์  ๋ชจ๋“œ +} + +// API ์‘๋‹ต ๊ตฌ์กฐ +export interface ApiResponse { + success: boolean; + data: T; + message?: string; + error?: string; +} + +// ์ฐจํŠธ ๋ฐ์ดํ„ฐ (๋ณ€ํ™˜ ํ›„) +export interface ChartData { + labels: string[]; // X์ถ• ๋ ˆ์ด๋ธ” + datasets: ChartDataset[]; // Y์ถ• ๋ฐ์ดํ„ฐ์…‹ (๋‹ค์ค‘ ์‹œ๋ฆฌ์ฆˆ) +} + +export interface ChartDataset { + label: string; // ์‹œ๋ฆฌ์ฆˆ ์ด๋ฆ„ + data: number[]; // ๋ฐ์ดํ„ฐ ๊ฐ’ + color?: string; // ์ƒ‰์ƒ +} +``` + +--- + +## ๐Ÿ“ ๊ตฌํ˜„ ๋‹จ๊ณ„ + +### Phase 1: ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • UI (4-5์‹œ๊ฐ„) + +#### Step 1.1: ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ ํƒ๊ธฐ + +- [ ] `DataSourceSelector.tsx` ์ƒ์„ฑ +- [ ] DB vs API ์„ ํƒ ๋ผ๋””์˜ค ๋ฒ„ํŠผ +- [ ] ์„ ํƒ์— ๋”ฐ๋ผ ํ•˜์œ„ UI ๋™์  ๋ Œ๋”๋ง +- [ ] ์ƒํƒœ ๊ด€๋ฆฌ (ํ˜„์žฌ ์„ ํƒ๋œ ์†Œ์Šค ํƒ€์ž…) + +#### Step 1.2: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • + +- [ ] `DatabaseConfig.tsx` ์ƒ์„ฑ +- [ ] ํ˜„์žฌ DB / ์™ธ๋ถ€ DB ์„ ํƒ ๋ผ๋””์˜ค ๋ฒ„ํŠผ +- [ ] ์™ธ๋ถ€ DB ์„ ํƒ ์‹œ: + - **๊ธฐ์กด ์™ธ๋ถ€ ์ปค๋„ฅ์…˜ ๊ด€๋ฆฌ์—์„œ ๋“ฑ๋ก๋œ ์ปค๋„ฅ์…˜ ๋ชฉ๋ก ๋ถˆ๋Ÿฌ์˜ค๊ธฐ** + - ๋“œ๋กญ๋‹ค์šด์œผ๋กœ ์ปค๋„ฅ์…˜ ์„ ํƒ (ID, ์ด๋ฆ„, ํƒ€์ž… ํ‘œ์‹œ) + - "์™ธ๋ถ€ ์ปค๋„ฅ์…˜ ๊ด€๋ฆฌ๋กœ ์ด๋™" ๋งํฌ ์ œ๊ณต + - ์„ ํƒ๋œ ์ปค๋„ฅ์…˜ ์ •๋ณด ํ‘œ์‹œ (์ฝ๊ธฐ ์ „์šฉ) +- [ ] SQL ์—๋””ํ„ฐ ํ†ตํ•ฉ (๊ธฐ์กด `QueryEditor` ์žฌ์‚ฌ์šฉ) +- [ ] ์ฟผ๋ฆฌ ํ…Œ์ŠคํŠธ ๋ฒ„ํŠผ (์„ ํƒ๋œ ์ปค๋„ฅ์…˜์œผ๋กœ ์‹คํ–‰) + +#### Step 1.3: REST API ์„ค์ • + +- [ ] `ApiConfig.tsx` ์ƒ์„ฑ +- [ ] API ์—”๋“œํฌ์ธํŠธ URL ์ž…๋ ฅ +- [ ] HTTP ๋ฉ”์„œ๋“œ: GET ๊ณ ์ • (UI์—์„œ ํ‘œ์‹œ๋งŒ) +- [ ] URL ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ UI (ํ‚ค-๊ฐ’ ์Œ) + - ๋™์  ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€/์ œ๊ฑฐ ๋ฒ„ํŠผ + - ์˜ˆ์‹œ: `?category=electronics&limit=10` +- [ ] ํ—ค๋” ์ถ”๊ฐ€ UI (ํ‚ค-๊ฐ’ ์Œ) + - Authorization ํ—ค๋” ๋น ๋ฅธ ์ž…๋ ฅ + - ์ผ๋ฐ˜์ ์ธ ํ—ค๋” ํ…œํ”Œ๋ฆฟ ์ œ๊ณต +- [ ] JSON Path ์„ค์ • (๋ฐ์ดํ„ฐ ์ถ”์ถœ ๊ฒฝ๋กœ) + - ์˜ˆ์‹œ: `data.results`, `items`, `response.data` +- [ ] ํ…Œ์ŠคํŠธ ์š”์ฒญ ๋ฒ„ํŠผ +- [ ] ์‘๋‹ต ๋ฏธ๋ฆฌ๋ณด๊ธฐ (JSON ๊ตฌ์กฐ ํ‘œ์‹œ) + +#### Step 1.4: ๋ฐ์ดํ„ฐ ์†Œ์Šค ์œ ํ‹ธ๋ฆฌํ‹ฐ + +- [ ] `dataSourceUtils.ts` ์ƒ์„ฑ +- [ ] DB ์ปค๋„ฅ์…˜ ๊ฒ€์ฆ ํ•จ์ˆ˜ +- [ ] API ์š”์ฒญ ์‹คํ–‰ ํ•จ์ˆ˜ +- [ ] JSON Path ํŒŒ์‹ฑ ํ•จ์ˆ˜ +- [ ] ๋ฐ์ดํ„ฐ ์ •๊ทœํ™” ํ•จ์ˆ˜ (DB/API ๊ฒฐ๊ณผ๋ฅผ ํ†ต์ผ๋œ ํ˜•์‹์œผ๋กœ) + +### Phase 2: ์„œ๋ฒ„ ์ธก API ๊ตฌํ˜„ (2-3์‹œ๊ฐ„) + +#### Step 2.1: ์™ธ๋ถ€ ์ปค๋„ฅ์…˜ ๋ชฉ๋ก ์กฐํšŒ API + +- [ ] `GET /api/external-connections` - ๊ธฐ์กด ์™ธ๋ถ€ ์ปค๋„ฅ์…˜ ๊ด€๋ฆฌ์˜ ์ปค๋„ฅ์…˜ ๋ชฉ๋ก ์กฐํšŒ +- [ ] ์‘๋‹ต: `{ id, name, type }` ์ตœ์†Œ ์ •๋ณด๋งŒ ๋ฐ˜ํ™˜ (๋ณด์•ˆ) +- [ ] ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ + +#### Step 2.2: ์ฟผ๋ฆฌ ์‹คํ–‰ API (ํ™•์žฅ) + +- [ ] ๊ธฐ์กด `POST /api/dashboards/execute-query` ํ™•์žฅ +- [ ] ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ์ง€์› +- [ ] SELECT ์ฟผ๋ฆฌ ๊ฒ€์ฆ (์ •๊ทœ์‹ + SQL ํŒŒ์„œ) +- [ ] SQL Injection ๋ฐฉ์ง€ +- [ ] ์ฟผ๋ฆฌ ํƒ€์ž„์•„์›ƒ ์„ค์ • +- [ ] ๊ฒฐ๊ณผ ํ–‰ ์ˆ˜ ์ œํ•œ (์ตœ๋Œ€ 1000ํ–‰) +- [ ] ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋ฐ ๋กœ๊น… + +#### Step 2.3: REST API ํ”„๋ก์‹œ + +- [ ] `GET /api/dashboards/fetch-api` - API ํ˜ธ์ถœ ํ”„๋ก์‹œ (GET ํ”„๋ก์‹œ) +- [ ] ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋Œ€์ƒ URL, ํ—ค๋”, JSON Path ์ „๋‹ฌ +- [ ] CORS ์šฐํšŒ +- [ ] ์š”์ฒญ ํ—ค๋” ์ „๋‹ฌ (Authorization ๋“ฑ) +- [ ] ์‘๋‹ต ์บ์‹ฑ (์„ ํƒ์ , 5๋ถ„) +- [ ] ํƒ€์ž„์•„์›ƒ ์„ค์ • (30์ดˆ) +- [ ] JSON Path ์ ์šฉ (์„œ๋ฒ„ ์ธก์—์„œ ๋ฐ์ดํ„ฐ ์ถ”์ถœ) +- [ ] ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋ฐ ์ƒํƒœ ์ฝ”๋“œ ๋ณ€ํ™˜ + +### Phase 3: ์ฐจํŠธ ์„ค์ • UI ๊ฐœ์„  (3-4์‹œ๊ฐ„) + +#### Step 3.1: ์ถ• ๋งคํผ + +- [ ] `AxisMapper.tsx` ์ƒ์„ฑ +- [ ] X์ถ• ํ•„๋“œ ์„ ํƒ ๋“œ๋กญ๋‹ค์šด +- [ ] Y์ถ• ํ•„๋“œ ๋‹ค์ค‘ ์„ ํƒ (์ฒดํฌ๋ฐ•์Šค) +- [ ] ๋ฐ์ดํ„ฐ ํƒ€์ž… ์ž๋™ ๊ฐ์ง€ ๋ฐ ํ‘œ์‹œ +- [ ] ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์ฒซ 3ํ–‰) +- [ ] ์ถ• ๋ผ๋ฒจ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• + +#### Step 3.2: ์Šคํƒ€์ผ ์„ค์ • + +- [ ] `StyleConfig.tsx` ์ƒ์„ฑ +- [ ] ์ƒ‰์ƒ ํŒ”๋ ˆํŠธ ์„ ํƒ (์‚ฌ์ „ ์ •์˜ + ์ปค์Šคํ…€) +- [ ] ๋ฒ”๋ก€ ์œ„์น˜ ์„ ํƒ +- [ ] ๊ทธ๋ฆฌ๋“œ ํ‘œ์‹œ/์ˆจ๊น€ +- [ ] ์• ๋‹ˆ๋ฉ”์ด์…˜ ์„ค์ • +- [ ] ์ฐจํŠธ๋ณ„ ํŠน์ˆ˜ ์˜ต์…˜ + - ๋ง‰๋Œ€ ์ฐจํŠธ: ์ˆ˜ํ‰/์ˆ˜์ง + - ์„  ์ฐจํŠธ: ๋ถ€๋“œ๋Ÿฌ์›€ ์ •๋„ + - ์› ์ฐจํŠธ: ๋„๋„› ๋ชจ๋“œ + +#### Step 3.3: ์‹ค์‹œ๊ฐ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ + +- [ ] `ChartPreview.tsx` ์ƒ์„ฑ +- [ ] ์ถ•์†Œ๋œ ์ฐจํŠธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (300x200) +- [ ] ์„ค์ • ๋ณ€๊ฒฝ ์‹œ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ +- [ ] ๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ +- [ ] ์—๋Ÿฌ ํ‘œ์‹œ + +### Phase 4: D3 ์ฐจํŠธ ์ปดํฌ๋„ŒํŠธ (6-8์‹œ๊ฐ„) + +#### Step 4.1: ์ฐจํŠธ ๋ Œ๋”๋Ÿฌ (๊ณตํ†ต) + +- [ ] `ChartRenderer.tsx` ์ƒ์„ฑ +- [ ] ์ฐจํŠธ ํƒ€์ž…์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง +- [ ] ๋ฐ์ดํ„ฐ ์ •๊ทœํ™” ๋ฐ ๋ณ€ํ™˜ +- [ ] ๊ณตํ†ต ๋ ˆ์ด์•„์›ƒ (์ œ๋ชฉ, ๋ฒ”๋ก€) +- [ ] ๋ฐ˜์‘ํ˜• ํฌ๊ธฐ ์กฐ์ ˆ +- [ ] ์—๋Ÿฌ ๋ฐ”์šด๋”๋ฆฌ + +#### Step 4.2: ๋ง‰๋Œ€ ์ฐจํŠธ + +- [ ] `BarChart.tsx` ์ƒ์„ฑ +- [ ] D3 ์Šค์ผ€์ผ ์„ค์ • (x: ๋ฒ”์ฃผํ˜•, y: ์„ ํ˜•) +- [ ] ๋ง‰๋Œ€ ๋ Œ๋”๋ง (rect ์š”์†Œ) +- [ ] ์ถ• ๋ Œ๋”๋ง (d3-axis) +- [ ] ํˆดํŒ ๊ตฌํ˜„ +- [ ] ์• ๋‹ˆ๋ฉ”์ด์…˜ (๋†’์ด ์ „ํ™˜) +- [ ] ์ˆ˜ํ‰/์ˆ˜์ง ๋ชจ๋“œ ์ง€์› +- [ ] ๋‹ค์ค‘ ์‹œ๋ฆฌ์ฆˆ (๊ทธ๋ฃนํ™”) + +#### Step 4.3: ์„  ์ฐจํŠธ + +- [ ] `LineChart.tsx` ์ƒ์„ฑ +- [ ] D3 ๋ผ์ธ ์ œ๋„ˆ๋ ˆ์ดํ„ฐ (d3.line) +- [ ] ๋ถ€๋“œ๋Ÿฌ์šด ๊ณก์„  (d3.curveMonotoneX) +- [ ] ๋ฐ์ดํ„ฐ ํฌ์ธํŠธ ํ‘œ์‹œ (circle) +- [ ] ํˆดํŒ ๊ตฌํ˜„ +- [ ] ์• ๋‹ˆ๋ฉ”์ด์…˜ (path ๊ธธ์ด ์ „ํ™˜) +- [ ] ๋‹ค์ค‘ ์‹œ๋ฆฌ์ฆˆ (์—ฌ๋Ÿฌ ์„ ) +- [ ] ๋ˆ„๋ฝ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ + +#### Step 4.4: ์˜์—ญ ์ฐจํŠธ + +- [ ] `AreaChart.tsx` ์ƒ์„ฑ +- [ ] D3 ์˜์—ญ ์ œ๋„ˆ๋ ˆ์ดํ„ฐ (d3.area) +- [ ] ํˆฌ๋ช…๋„ ์„ค์ • +- [ ] ๋ˆ„์  ๋ชจ๋“œ ์ง€์› (d3.stack) +- [ ] ์„  ์ฐจํŠธ ๊ธฐ๋Šฅ ์žฌ์‚ฌ์šฉ +- [ ] ์• ๋‹ˆ๋ฉ”์ด์…˜ + +#### Step 4.5: ์› ์ฐจํŠธ + +- [ ] `PieChart.tsx` ์ƒ์„ฑ +- [ ] D3 ํŒŒ์ด ๋ ˆ์ด์•„์›ƒ (d3.pie) +- [ ] ์•„ํฌ ์ œ๋„ˆ๋ ˆ์ดํ„ฐ (d3.arc) +- [ ] ๋„๋„› ๋ชจ๋“œ (innerRadius) +- [ ] ๋ผ๋ฒจ ๋ฐฐ์น˜ (์ค‘์‹ฌ ๋˜๋Š” ์™ธ๋ถ€) +- [ ] ํˆดํŒ ๊ตฌํ˜„ +- [ ] ์• ๋‹ˆ๋ฉ”์ด์…˜ (ํšŒ์ „ ์ „ํ™˜) +- [ ] ํผ์„ผํŠธ ํ‘œ์‹œ + +#### Step 4.6: ๋ˆ„์  ๋ง‰๋Œ€ ์ฐจํŠธ + +- [ ] `StackedBarChart.tsx` ์ƒ์„ฑ +- [ ] D3 ์Šคํƒ ๋ ˆ์ด์•„์›ƒ (d3.stack) +- [ ] ๋‹ค์ค‘ ์‹œ๋ฆฌ์ฆˆ ๋ˆ„์  +- [ ] ์ผ๋ฐ˜ ๋ˆ„์  vs ํผ์„ผํŠธ ๋ชจ๋“œ +- [ ] ๋ง‰๋Œ€ ์ฐจํŠธ ๋กœ์ง ์žฌ์‚ฌ์šฉ +- [ ] ๋ฒ”๋ก€ ์ƒ‰์ƒ ๋งคํ•‘ + +#### Step 4.7: ํ˜ผํ•ฉ ์ฐจํŠธ + +- [ ] `ComboChart.tsx` ์ƒ์„ฑ +- [ ] ๋ง‰๋Œ€ + ์„  ์กฐํ•ฉ +- [ ] ์ด์ค‘ Y์ถ• (์ขŒ์ธก: ๋ง‰๋Œ€, ์šฐ์ธก: ์„ ) +- [ ] ์Šค์ผ€์ผ ๋…๋ฆฝ ์„ค์ • +- [ ] ๋ง‰๋Œ€/์„  ์ฐจํŠธ ๋กœ์ง ๊ฒฐํ•ฉ +- [ ] ๋ณต์žกํ•œ ํˆดํŒ (๋‘ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ) + +#### Step 4.8: ์ฐจํŠธ ์œ ํ‹ธ๋ฆฌํ‹ฐ + +- [ ] `chartUtils.ts` ์ƒ์„ฑ +- [ ] ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ํ•จ์ˆ˜ (QueryResult โ†’ ChartData) +- [ ] ๋‚ ์งœ ํŒŒ์‹ฑ ๋ฐ ํฌ๋งทํŒ… +- [ ] ์ˆซ์ž ํฌ๋งทํŒ… (์ฒœ ๋‹จ์œ„ ์ฝค๋งˆ, ์†Œ์ˆ˜์ ) +- [ ] ์ƒ‰์ƒ ํŒ”๋ ˆํŠธ ์ •์˜ +- [ ] ๋ฐ˜์‘ํ˜• ํฌ๊ธฐ ๊ณ„์‚ฐ + +#### Step 4.9: D3 ํ—ฌํผ + +- [ ] `d3Helpers.ts` ์ƒ์„ฑ +- [ ] ๊ณตํ†ต ์Šค์ผ€์ผ ์ƒ์„ฑ +- [ ] ์ถ• ์ƒ์„ฑ ๋ฐ ์Šคํƒ€์ผ๋ง +- [ ] ๊ทธ๋ฆฌ๋“œ ๋ผ์ธ ์ถ”๊ฐ€ +- [ ] ํˆดํŒ DOM ์ƒ์„ฑ/์ œ๊ฑฐ +- [ ] SVG ๋งˆ์ง„ ๊ณ„์‚ฐ + +### Phase 5: ์ฐจํŠธ ํ†ตํ•ฉ ๋ฐ ๋ Œ๋”๋ง (2-3์‹œ๊ฐ„) + +#### Step 5.1: CanvasElement ํ†ตํ•ฉ + +- [ ] `CanvasElement.tsx` ์ˆ˜์ • +- [ ] ์ฐจํŠธ ์š”์†Œ ๊ฐ์ง€ (element.type === 'chart') +- [ ] `ChartRenderer` ์ปดํฌ๋„ŒํŠธ ์ž„ํฌํŠธ ๋ฐ ๋ Œ๋”๋ง +- [ ] ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ +- [ ] ์—๋Ÿฌ ์ƒํƒœ ํ‘œ์‹œ +- [ ] ์ž๋™ ์ƒˆ๋กœ๊ณ ์นจ ๋กœ์ง + +#### Step 5.2: ๋ฐ์ดํ„ฐ ํŽ˜์นญ + +- [ ] ์ฐจํŠธ ๋งˆ์šดํŠธ ์‹œ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ +- [ ] ์ž๋™ ์ƒˆ๋กœ๊ณ ์นจ ํƒ€์ด๋จธ ์„ค์ • +- [ ] ์ˆ˜๋™ ์ƒˆ๋กœ๊ณ ์นจ ๋ฒ„ํŠผ +- [ ] ๋กœ๋”ฉ/์—๋Ÿฌ/์„ฑ๊ณต ์ƒํƒœ ๊ด€๋ฆฌ +- [ ] ์บ์‹ฑ (์„ ํƒ์ ) + +#### Step 5.3: ElementConfigModal ๋ฆฌํŒฉํ† ๋ง + +- [ ] ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ ํƒ UI ํ†ตํ•ฉ +- [ ] 3๋‹จ๊ณ„ ํ”Œ๋กœ์šฐ ๊ตฌํ˜„ + 1. ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ ํƒ ๋ฐ ์„ค์ • + 2. ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ ๋ฐ ๊ฒ€์ฆ + 3. ์ถ• ๋งคํ•‘ ๋ฐ ์Šคํƒ€์ผ ์„ค์ • +- [ ] ์ง„ํ–‰ ํ‘œ์‹œ๊ธฐ (์Šคํ… ์ธ๋””์ผ€์ดํ„ฐ) +- [ ] ๋’ค๋กœ/๋‹ค์Œ ๋ฒ„ํŠผ + +### Phase 6: ํ…Œ์ŠคํŠธ ๋ฐ ์ตœ์ ํ™” (2-3์‹œ๊ฐ„) + +#### Step 6.1: ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ + +- [ ] ๊ฐ ์ฐจํŠธ ํƒ€์ž… ๋ Œ๋”๋ง ํ™•์ธ +- [ ] DB ์ฟผ๋ฆฌ ์‹คํ–‰ ๋ฐ ์ฐจํŠธ ์ƒ์„ฑ +- [ ] API ํ˜ธ์ถœ ๋ฐ ์ฐจํŠธ ์ƒ์„ฑ +- [ ] ๋‹ค์ค‘ ์‹œ๋ฆฌ์ฆˆ ์ฐจํŠธ ํ™•์ธ +- [ ] ์ž๋™ ์ƒˆ๋กœ๊ณ ์นจ ๋™์ž‘ ํ™•์ธ +- [ ] ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ™•์ธ + +#### Step 6.2: UI/UX ๊ฐœ์„  + +- [ ] ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ ์ถ”๊ฐ€ +- [ ] ๋นˆ ๋ฐ์ดํ„ฐ ์ƒํƒœ UI +- [ ] ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๊ฐœ์„  +- [ ] ํˆดํŒ ์Šคํƒ€์ผ๋ง +- [ ] ๋ฒ”๋ก€ ์Šคํƒ€์ผ๋ง +- [ ] ๋ฐ˜์‘ํ˜• ๋ ˆ์ด์•„์›ƒ ํ™•์ธ + +#### Step 6.3: ์„ฑ๋Šฅ ์ตœ์ ํ™” + +- [ ] D3 ๋ Œ๋”๋ง ์ตœ์ ํ™” (๋ถˆํ•„์š”ํ•œ ์žฌ๋ Œ๋”๋ง ๋ฐฉ์ง€) +- [ ] ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ (์ƒ˜ํ”Œ๋ง, ํŽ˜์ด์ง•) +- [ ] ๋ฉ”๋ชจ์ด์ œ์ด์…˜ (useMemo, useCallback) +- [ ] SVG ์ตœ์ ํ™” +- [ ] ์ฐจํŠธ ๋ฐ์ดํ„ฐ ์บ์‹ฑ + +--- + +## ๐Ÿ”’ ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ + +### SQL Injection ๋ฐฉ์ง€ + +- ์„œ๋ฒ„ ์ธก์—์„œ ์ฟผ๋ฆฌ ํƒ€์ž… ์—„๊ฒฉ ๊ฒ€์ฆ (SELECT๋งŒ ํ—ˆ์šฉ) +- ์ •๊ทœ์‹ + SQL ํŒŒ์„œ ์‚ฌ์šฉ +- Prepared Statement ์‚ฌ์šฉ (ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ) +- ์œ„ํ—˜ํ•œ ํ‚ค์›Œ๋“œ ์ฐจ๋‹จ (DROP, DELETE, UPDATE, INSERT, EXEC ๋“ฑ) + +### ์™ธ๋ถ€ DB ์ปค๋„ฅ์…˜ ๋ณด์•ˆ + +- ๊ธฐ์กด "์™ธ๋ถ€ ์ปค๋„ฅ์…˜ ๊ด€๋ฆฌ"์—์„œ ๋ณด์•ˆ ์ฒ˜๋ฆฌ๋จ +- ์ฐจํŠธ ์‹œ์Šคํ…œ์—์„œ๋Š” ์ปค๋„ฅ์…˜ ID๋งŒ ์‚ฌ์šฉ +- ๋ฏผ๊ฐ ์ •๋ณด(๋น„๋ฐ€๋ฒˆํ˜ธ, ํ˜ธ์ŠคํŠธ ๋“ฑ)๋Š” ์ฐจํŠธ ์„ค์ •์— ๋…ธ์ถœํ•˜์ง€ ์•Š์Œ +- ํƒ€์ž„์•„์›ƒ ์„ค์ • (30์ดˆ) + +### API ๋ณด์•ˆ + +- CORS ์ •์ฑ… ํ™•์ธ +- ๋ฏผ๊ฐํ•œ ํ—ค๋” ๋กœ๊น… ๋ฐฉ์ง€ (Authorization ๋“ฑ) +- ์š”์ฒญ ํฌ๊ธฐ ์ œํ•œ +- Rate Limiting (API ํ˜ธ์ถœ ๋นˆ๋„ ์ œํ•œ) + +--- + +## ๐ŸŽจ UI/UX ๊ฐœ์„  ์‚ฌํ•ญ + +### ์„ค์ • ํ”Œ๋กœ์šฐ + +1. **๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ ํƒ** + - ํฐ ์•„์ด์ฝ˜๊ณผ ์„ค๋ช…์œผ๋กœ DB vs API ์„ ํƒ + - ๊ฐ ๋ฐฉ์‹์˜ ์žฅ๋‹จ์  ์•ˆ๋‚ด + +2. **๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ** + - DB: SQL ์—๋””ํ„ฐ + ์‹คํ–‰ ๋ฒ„ํŠผ + - API: URL, ๋ฉ”์„œ๋“œ, ํ—ค๋”, ๋ณธ๋ฌธ ์ž…๋ ฅ + - ํ…Œ์ŠคํŠธ ๋ฒ„ํŠผ์œผ๋กœ ์ฆ‰์‹œ ํ™•์ธ + +3. **๋ฐ์ดํ„ฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ** + - ์ฟผ๋ฆฌ/API ์‹คํ–‰ ๊ฒฐ๊ณผ๋ฅผ ํ…Œ์ด๋ธ”๋กœ ํ‘œ์‹œ (์ตœ๋Œ€ 10ํ–‰) + - ์ปฌ๋Ÿผ๋ช…๊ณผ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ + +4. **์ฐจํŠธ ์„ค์ •** + - X/Y์ถ• ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ๋งคํ•‘ + - ์‹ค์‹œ๊ฐ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์ž‘์€ ์ฐจํŠธ) + - ์Šคํƒ€์ผ ํ”„๋ฆฌ์…‹ ์„ ํƒ + +### ํ”ผ๋“œ๋ฐฑ ๋ฉ”์‹œ์ง€ + +- โœ… ์„ฑ๊ณต: "๋ฐ์ดํ„ฐ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ๋ถˆ๋Ÿฌ์™”์Šต๋‹ˆ๋‹ค (45ํ–‰)" +- โš ๏ธ ๊ฒฝ๊ณ : "์ฟผ๋ฆฌ ์‹คํ–‰์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค" +- โŒ ์˜ค๋ฅ˜: "๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค: ์ž˜๋ชป๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ" + +### ๋กœ๋”ฉ ์ƒํƒœ + +- ์Šค์ผˆ๋ ˆํ†ค UI (์ฐจํŠธ ์œค๊ณฝ) +- ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ (๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ) +- ์ทจ์†Œ ๋ฒ„ํŠผ (์žฅ์‹œ๊ฐ„ ์‹คํ–‰ ์ฟผ๋ฆฌ) + +--- + +## ๐Ÿ“Š ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ๋ฐ ์‹œ๋‚˜๋ฆฌ์˜ค + +### ์‹œ๋‚˜๋ฆฌ์˜ค 1: ์›”๋ณ„ ๋งค์ถœ ์ถ”์ด (DB ์ฟผ๋ฆฌ) + +```sql +SELECT + TO_CHAR(order_date, 'YYYY-MM') as month, + SUM(total_amount) as sales +FROM orders +WHERE order_date >= CURRENT_DATE - INTERVAL '12 months' +GROUP BY TO_CHAR(order_date, 'YYYY-MM') +ORDER BY month; +``` + +- **์ฐจํŠธ ํƒ€์ž…**: Line Chart +- **X์ถ•**: month +- **Y์ถ•**: sales + +### ์‹œ๋‚˜๋ฆฌ์˜ค 2: ์ œํ’ˆ ๋น„๊ต (๋‹ค์ค‘ ์‹œ๋ฆฌ์ฆˆ) + +```sql +SELECT + DATE_TRUNC('month', order_date) as month, + SUM(CASE WHEN product_category = '๊ฐค๋Ÿญ์‹œ' THEN amount ELSE 0 END) as galaxy, + SUM(CASE WHEN product_category = '์•„์ดํฐ' THEN amount ELSE 0 END) as iphone +FROM orders +WHERE order_date >= CURRENT_DATE - INTERVAL '12 months' +GROUP BY DATE_TRUNC('month', order_date) +ORDER BY month; +``` + +- **์ฐจํŠธ ํƒ€์ž…**: Combo Chart (Bar + Line) +- **X์ถ•**: month +- **Y์ถ•**: [galaxy, iphone] (๋‹ค์ค‘) + +### ์‹œ๋‚˜๋ฆฌ์˜ค 3: ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋งค์ถœ (์› ์ฐจํŠธ) + +```sql +SELECT + category, + SUM(amount) as total +FROM sales +WHERE sale_date >= CURRENT_DATE - INTERVAL '1 month' +GROUP BY category +ORDER BY total DESC +LIMIT 10; +``` + +- **์ฐจํŠธ ํƒ€์ž…**: Pie Chart (Donut) +- **X์ถ•**: category +- **Y์ถ•**: total + +### ์‹œ๋‚˜๋ฆฌ์˜ค 4: REST API (์‹ค์‹œ๊ฐ„ ํ™˜์œจ) + +- **API**: `https://api.exchangerate-api.com/v4/latest/USD` +- **JSON Path**: `rates` +- **๋ณ€ํ™˜**: Object๋ฅผ ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ (ํ†ตํ™”: ํ™˜์œจ) +- **์ฐจํŠธ ํƒ€์ž…**: Bar Chart +- **X์ถ•**: ํ†ตํ™” ์ฝ”๋“œ (KRW, JPY, EUR ๋“ฑ) +- **Y์ถ•**: ํ™˜์œจ + +--- + +## โœ… ์™„๋ฃŒ ๊ธฐ์ค€ + +### Phase 1: ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • + +- [x] DB ์ปค๋„ฅ์…˜ ์„ค์ • UI ์ž‘๋™ +- [x] ์™ธ๋ถ€ DB ์ปค๋„ฅ์…˜ ์ €์žฅ ๋ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ +- [x] API ์„ค์ • UI ์ž‘๋™ +- [x] ํ…Œ์ŠคํŠธ ๋ฒ„ํŠผ์œผ๋กœ ์ฆ‰์‹œ ํ™•์ธ ๊ฐ€๋Šฅ + +### Phase 2: ์„œ๋ฒ„ API + +- [x] ์™ธ๋ถ€ DB ์ปค๋„ฅ์…˜ CRUD API ์ž‘๋™ +- [x] ์ฟผ๋ฆฌ ์‹คํ–‰ API (ํ˜„์žฌ/์™ธ๋ถ€ DB) +- [x] SELECT ์ฟผ๋ฆฌ ๊ฒ€์ฆ ๋ฐ SQL Injection ๋ฐฉ์ง€ +- [x] API ํ”„๋ก์‹œ ์ž‘๋™ + +### Phase 3: ์ฐจํŠธ ์„ค์ • UI + +- [x] ์ถ• ๋งคํ•‘ UI ์ง๊ด€์  +- [x] ๋‹ค์ค‘ Y์ถ• ์„ ํƒ ๊ฐ€๋Šฅ +- [x] ์Šคํƒ€์ผ ์„ค์ • UI ์ž‘๋™ +- [x] ์‹ค์‹œ๊ฐ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ‘œ์‹œ + +### Phase 4: D3 ์ฐจํŠธ + +- [x] 6๊ฐ€์ง€ ์ฐจํŠธ ํƒ€์ž… ๋ชจ๋‘ ๋ Œ๋”๋ง +- [x] ํˆดํŒ ํ‘œ์‹œ +- [x] ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ถ€๋“œ๋Ÿฌ์›€ +- [x] ๋ฐ˜์‘ํ˜• ํฌ๊ธฐ ์กฐ์ ˆ +- [x] ๋‹ค์ค‘ ์‹œ๋ฆฌ์ฆˆ ์ง€์› + +### Phase 5: ํ†ตํ•ฉ + +- [x] ์บ”๋ฒ„์Šค์—์„œ ์ฐจํŠธ ํ‘œ์‹œ +- [x] ์ž๋™ ์ƒˆ๋กœ๊ณ ์นจ ์ž‘๋™ +- [x] ์„ค์ • ๋ชจ๋‹ฌ 3๋‹จ๊ณ„ ํ”Œ๋กœ์šฐ ์™„๋ฃŒ +- [x] ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ/์—๋Ÿฌ ์ƒํƒœ ํ‘œ์‹œ + +### Phase 6: ํ…Œ์ŠคํŠธ + +- [x] ๋ชจ๋“  ์ฐจํŠธ ํƒ€์ž… ์ •์ƒ ์ž‘๋™ +- [x] DB/API ๋ฐ์ดํ„ฐ ์†Œ์Šค ๋ชจ๋‘ ์ž‘๋™ +- [x] ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์ ์ ˆ +- [x] ์„ฑ๋Šฅ ์ด์Šˆ ์—†์Œ (1000ํ–‰ ๋ฐ์ดํ„ฐ) + +--- + +## ๐Ÿš€ ํ–ฅํ›„ ํ™•์žฅ ๊ณ„ํš + +- **์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ**: WebSocket ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ถ”๊ฐ€ +- **๊ณ ๊ธ‰ ์ฐจํŠธ**: Scatter Plot, Heatmap, Radar Chart +- **๋ฐ์ดํ„ฐ ๋ณ€ํ™˜**: ํ•„ํ„ฐ๋ง, ์ •๋ ฌ, ๊ณ„์‚ฐ ํ•„๋“œ ์ถ”๊ฐ€ +- **์ฐจํŠธ ์ƒํ˜ธ์ž‘์šฉ**: ํด๋ฆญ/๋“œ๋ž˜๊ทธ๋กœ ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง +- **๋‚ด๋ณด๋‚ด๊ธฐ**: PNG, SVG, PDF ์ €์žฅ +- **ํ…œํ”Œ๋ฆฟ**: ์‚ฌ์ „ ์ •์˜๋œ ์ฐจํŠธ ํ…œํ”Œ๋ฆฟ (์—…์ข…๋ณ„) + +--- + +## ๐Ÿ“… ์˜ˆ์ƒ ์ผ์ • + +- **Phase 1**: 1์ผ (๋ฐ์ดํ„ฐ ์†Œ์Šค UI) +- **Phase 2**: 0.5์ผ (์„œ๋ฒ„ API) - ๊ธฐ์กด ์™ธ๋ถ€ ์ปค๋„ฅ์…˜ ๊ด€๋ฆฌ ํ™œ์šฉ์œผ๋กœ ๋‹จ์ถ• +- **Phase 3**: 1์ผ (์ฐจํŠธ ์„ค์ • UI) +- **Phase 4**: 2์ผ (D3 ์ฐจํŠธ ์ปดํฌ๋„ŒํŠธ) +- **Phase 5**: 0.5์ผ (ํ†ตํ•ฉ) +- **Phase 6**: 0.5์ผ (ํ…Œ์ŠคํŠธ) + +**์ด ์˜ˆ์ƒ ์‹œ๊ฐ„**: 5.5์ผ (44์‹œ๊ฐ„) + +--- + +**๊ตฌํ˜„ ์‹œ์ž‘์ผ**: 2025-10-14 +**๋ชฉํ‘œ ์™„๋ฃŒ์ผ**: 2025-10-20 +**ํ˜„์žฌ ์ง„ํ–‰๋ฅ **: 0% (๊ณ„ํš ์ˆ˜๋ฆฝ ์™„๋ฃŒ) + +--- + +## ๐ŸŽฏ ๋‹ค์Œ ๋‹จ๊ณ„ + +1. Phase 1 ์‹œ์ž‘: `DataSourceSelector.tsx` ์ƒ์„ฑ +2. ํƒ€์ž… ์ •์˜ ํ™•์žฅ: `types.ts` ์—…๋ฐ์ดํŠธ +3. ์„œ๋ฒ„ API ์—”๋“œํฌ์ธํŠธ ์„ค๊ณ„ ๋ฐ ๊ตฌํ˜„ +4. D3.js ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜ ๋ฐ ๊ธฐ๋ณธ ์ฐจํŠธ PoC -- 2.43.0 From e667ee7106a65def213c374b4e0b94ecdb2f29d1 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Tue, 14 Oct 2025 13:59:54 +0900 Subject: [PATCH 02/14] =?UTF-8?q?=EC=B0=A8=ED=8A=B8=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20phase1=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/CHART_SYSTEM_PLAN.md | 46 +- .../admin/dashboard/ChartConfigPanel.tsx | 346 +++++++------ .../admin/dashboard/ElementConfigModal.tsx | 227 ++++++--- .../admin/dashboard/QueryEditor.tsx | 477 +++++++++--------- .../dashboard/data-sources/ApiConfig.tsx | 318 ++++++++++++ .../data-sources/DataSourceSelector.tsx | 89 ++++ .../dashboard/data-sources/DatabaseConfig.tsx | 205 ++++++++ .../dashboard/data-sources/dataSourceUtils.ts | 193 +++++++ frontend/components/admin/dashboard/types.ts | 86 +++- 9 files changed, 1485 insertions(+), 502 deletions(-) create mode 100644 frontend/components/admin/dashboard/data-sources/ApiConfig.tsx create mode 100644 frontend/components/admin/dashboard/data-sources/DataSourceSelector.tsx create mode 100644 frontend/components/admin/dashboard/data-sources/DatabaseConfig.tsx create mode 100644 frontend/components/admin/dashboard/data-sources/dataSourceUtils.ts diff --git a/frontend/components/admin/dashboard/CHART_SYSTEM_PLAN.md b/frontend/components/admin/dashboard/CHART_SYSTEM_PLAN.md index b732dad2..8483f02b 100644 --- a/frontend/components/admin/dashboard/CHART_SYSTEM_PLAN.md +++ b/frontend/components/admin/dashboard/CHART_SYSTEM_PLAN.md @@ -203,46 +203,46 @@ export interface ChartDataset { #### Step 1.1: ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ ํƒ๊ธฐ -- [ ] `DataSourceSelector.tsx` ์ƒ์„ฑ -- [ ] DB vs API ์„ ํƒ ๋ผ๋””์˜ค ๋ฒ„ํŠผ -- [ ] ์„ ํƒ์— ๋”ฐ๋ผ ํ•˜์œ„ UI ๋™์  ๋ Œ๋”๋ง -- [ ] ์ƒํƒœ ๊ด€๋ฆฌ (ํ˜„์žฌ ์„ ํƒ๋œ ์†Œ์Šค ํƒ€์ž…) +- [x] `DataSourceSelector.tsx` ์ƒ์„ฑ +- [x] DB vs API ์„ ํƒ ๋ผ๋””์˜ค ๋ฒ„ํŠผ +- [x] ์„ ํƒ์— ๋”ฐ๋ผ ํ•˜์œ„ UI ๋™์  ๋ Œ๋”๋ง +- [x] ์ƒํƒœ ๊ด€๋ฆฌ (ํ˜„์žฌ ์„ ํƒ๋œ ์†Œ์Šค ํƒ€์ž…) #### Step 1.2: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • -- [ ] `DatabaseConfig.tsx` ์ƒ์„ฑ -- [ ] ํ˜„์žฌ DB / ์™ธ๋ถ€ DB ์„ ํƒ ๋ผ๋””์˜ค ๋ฒ„ํŠผ -- [ ] ์™ธ๋ถ€ DB ์„ ํƒ ์‹œ: +- [x] `DatabaseConfig.tsx` ์ƒ์„ฑ +- [x] ํ˜„์žฌ DB / ์™ธ๋ถ€ DB ์„ ํƒ ๋ผ๋””์˜ค ๋ฒ„ํŠผ +- [x] ์™ธ๋ถ€ DB ์„ ํƒ ์‹œ: - **๊ธฐ์กด ์™ธ๋ถ€ ์ปค๋„ฅ์…˜ ๊ด€๋ฆฌ์—์„œ ๋“ฑ๋ก๋œ ์ปค๋„ฅ์…˜ ๋ชฉ๋ก ๋ถˆ๋Ÿฌ์˜ค๊ธฐ** - ๋“œ๋กญ๋‹ค์šด์œผ๋กœ ์ปค๋„ฅ์…˜ ์„ ํƒ (ID, ์ด๋ฆ„, ํƒ€์ž… ํ‘œ์‹œ) - "์™ธ๋ถ€ ์ปค๋„ฅ์…˜ ๊ด€๋ฆฌ๋กœ ์ด๋™" ๋งํฌ ์ œ๊ณต - ์„ ํƒ๋œ ์ปค๋„ฅ์…˜ ์ •๋ณด ํ‘œ์‹œ (์ฝ๊ธฐ ์ „์šฉ) -- [ ] SQL ์—๋””ํ„ฐ ํ†ตํ•ฉ (๊ธฐ์กด `QueryEditor` ์žฌ์‚ฌ์šฉ) -- [ ] ์ฟผ๋ฆฌ ํ…Œ์ŠคํŠธ ๋ฒ„ํŠผ (์„ ํƒ๋œ ์ปค๋„ฅ์…˜์œผ๋กœ ์‹คํ–‰) +- [x] SQL ์—๋””ํ„ฐ ํ†ตํ•ฉ (๊ธฐ์กด `QueryEditor` ์žฌ์‚ฌ์šฉ) +- [x] ์ฟผ๋ฆฌ ํ…Œ์ŠคํŠธ ๋ฒ„ํŠผ (์„ ํƒ๋œ ์ปค๋„ฅ์…˜์œผ๋กœ ์‹คํ–‰) #### Step 1.3: REST API ์„ค์ • -- [ ] `ApiConfig.tsx` ์ƒ์„ฑ -- [ ] API ์—”๋“œํฌ์ธํŠธ URL ์ž…๋ ฅ -- [ ] HTTP ๋ฉ”์„œ๋“œ: GET ๊ณ ์ • (UI์—์„œ ํ‘œ์‹œ๋งŒ) -- [ ] URL ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ UI (ํ‚ค-๊ฐ’ ์Œ) +- [x] `ApiConfig.tsx` ์ƒ์„ฑ +- [x] API ์—”๋“œํฌ์ธํŠธ URL ์ž…๋ ฅ +- [x] HTTP ๋ฉ”์„œ๋“œ: GET ๊ณ ์ • (UI์—์„œ ํ‘œ์‹œ๋งŒ) +- [x] URL ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ UI (ํ‚ค-๊ฐ’ ์Œ) - ๋™์  ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€/์ œ๊ฑฐ ๋ฒ„ํŠผ - ์˜ˆ์‹œ: `?category=electronics&limit=10` -- [ ] ํ—ค๋” ์ถ”๊ฐ€ UI (ํ‚ค-๊ฐ’ ์Œ) +- [x] ํ—ค๋” ์ถ”๊ฐ€ UI (ํ‚ค-๊ฐ’ ์Œ) - Authorization ํ—ค๋” ๋น ๋ฅธ ์ž…๋ ฅ - ์ผ๋ฐ˜์ ์ธ ํ—ค๋” ํ…œํ”Œ๋ฆฟ ์ œ๊ณต -- [ ] JSON Path ์„ค์ • (๋ฐ์ดํ„ฐ ์ถ”์ถœ ๊ฒฝ๋กœ) +- [x] JSON Path ์„ค์ • (๋ฐ์ดํ„ฐ ์ถ”์ถœ ๊ฒฝ๋กœ) - ์˜ˆ์‹œ: `data.results`, `items`, `response.data` -- [ ] ํ…Œ์ŠคํŠธ ์š”์ฒญ ๋ฒ„ํŠผ -- [ ] ์‘๋‹ต ๋ฏธ๋ฆฌ๋ณด๊ธฐ (JSON ๊ตฌ์กฐ ํ‘œ์‹œ) +- [x] ํ…Œ์ŠคํŠธ ์š”์ฒญ ๋ฒ„ํŠผ +- [x] ์‘๋‹ต ๋ฏธ๋ฆฌ๋ณด๊ธฐ (JSON ๊ตฌ์กฐ ํ‘œ์‹œ) #### Step 1.4: ๋ฐ์ดํ„ฐ ์†Œ์Šค ์œ ํ‹ธ๋ฆฌํ‹ฐ -- [ ] `dataSourceUtils.ts` ์ƒ์„ฑ -- [ ] DB ์ปค๋„ฅ์…˜ ๊ฒ€์ฆ ํ•จ์ˆ˜ -- [ ] API ์š”์ฒญ ์‹คํ–‰ ํ•จ์ˆ˜ -- [ ] JSON Path ํŒŒ์‹ฑ ํ•จ์ˆ˜ -- [ ] ๋ฐ์ดํ„ฐ ์ •๊ทœํ™” ํ•จ์ˆ˜ (DB/API ๊ฒฐ๊ณผ๋ฅผ ํ†ต์ผ๋œ ํ˜•์‹์œผ๋กœ) +- [x] `dataSourceUtils.ts` ์ƒ์„ฑ +- [x] DB ์ปค๋„ฅ์…˜ ๊ฒ€์ฆ ํ•จ์ˆ˜ +- [x] API ์š”์ฒญ ์‹คํ–‰ ํ•จ์ˆ˜ +- [x] JSON Path ํŒŒ์‹ฑ ํ•จ์ˆ˜ +- [x] ๋ฐ์ดํ„ฐ ์ •๊ทœํ™” ํ•จ์ˆ˜ (DB/API ๊ฒฐ๊ณผ๋ฅผ ํ†ต์ผ๋œ ํ˜•์‹์œผ๋กœ) ### Phase 2: ์„œ๋ฒ„ ์ธก API ๊ตฌํ˜„ (2-3์‹œ๊ฐ„) @@ -649,7 +649,7 @@ LIMIT 10; **๊ตฌํ˜„ ์‹œ์ž‘์ผ**: 2025-10-14 **๋ชฉํ‘œ ์™„๋ฃŒ์ผ**: 2025-10-20 -**ํ˜„์žฌ ์ง„ํ–‰๋ฅ **: 0% (๊ณ„ํš ์ˆ˜๋ฆฝ ์™„๋ฃŒ) +**ํ˜„์žฌ ์ง„ํ–‰๋ฅ **: 22% (Phase 1 ์™„๋ฃŒ + shadcn/ui ํ†ตํ•ฉ โœ…) --- diff --git a/frontend/components/admin/dashboard/ChartConfigPanel.tsx b/frontend/components/admin/dashboard/ChartConfigPanel.tsx index d67cfefb..67e69da8 100644 --- a/frontend/components/admin/dashboard/ChartConfigPanel.tsx +++ b/frontend/components/admin/dashboard/ChartConfigPanel.tsx @@ -1,7 +1,16 @@ -'use client'; +"use client"; -import React, { useState, useCallback } from 'react'; -import { ChartConfig, QueryResult } from './types'; +import React, { useState, useCallback } from "react"; +import { ChartConfig, QueryResult } from "./types"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Label } from "@/components/ui/label"; +import { Card } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Separator } from "@/components/ui/separator"; +import { Settings, TrendingUp, AlertCircle } from "lucide-react"; interface ChartConfigPanelProps { config?: ChartConfig; @@ -19,27 +28,32 @@ export function ChartConfigPanel({ config, queryResult, onConfigChange }: ChartC const [currentConfig, setCurrentConfig] = useState(config || {}); // ์„ค์ • ์—…๋ฐ์ดํŠธ - const updateConfig = useCallback((updates: Partial) => { - const newConfig = { ...currentConfig, ...updates }; - setCurrentConfig(newConfig); - onConfigChange(newConfig); - }, [currentConfig, onConfigChange]); + const updateConfig = useCallback( + (updates: Partial) => { + const newConfig = { ...currentConfig, ...updates }; + setCurrentConfig(newConfig); + onConfigChange(newConfig); + }, + [currentConfig, onConfigChange], + ); // ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปฌ๋Ÿผ ๋ชฉ๋ก const availableColumns = queryResult?.columns || []; const sampleData = queryResult?.rows?.[0] || {}; return ( -
-

โš™๏ธ ์ฐจํŠธ ์„ค์ •

+
+
+ +

์ฐจํŠธ ์„ค์ •

+
{/* ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๊ฐ€ ์—†์„ ๋•Œ */} {!queryResult && ( -
-
- ๐Ÿ’ก ๋จผ์ € SQL ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜จ ํ›„ ์ฐจํŠธ๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -
-
+ + + ๋จผ์ € SQL ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜จ ํ›„ ์ฐจํŠธ๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + )} {/* ๋ฐ์ดํ„ฐ ํ•„๋“œ ๋งคํ•‘ */} @@ -47,154 +61,157 @@ export function ChartConfigPanel({ config, queryResult, onConfigChange }: ChartC <> {/* ์ฐจํŠธ ์ œ๋ชฉ */}
- - ์ฐจํŠธ ์ œ๋ชฉ + updateConfig({ title: e.target.value })} placeholder="์ฐจํŠธ ์ œ๋ชฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”" - className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm" />
+ + {/* X์ถ• ์„ค์ • */}
- +
{/* Y์ถ• ์„ค์ • (๋‹ค์ค‘ ์„ ํƒ ๊ฐ€๋Šฅ) */}
- + +
+ {availableColumns.map((col) => { + const isSelected = Array.isArray(currentConfig.yAxis) + ? currentConfig.yAxis.includes(col) + : currentConfig.yAxis === col; + + return ( +
+ { + const currentYAxis = Array.isArray(currentConfig.yAxis) + ? currentConfig.yAxis + : currentConfig.yAxis + ? [currentConfig.yAxis] + : []; + + let newYAxis: string | string[]; + if (checked) { + newYAxis = [...currentYAxis, col]; + } else { + newYAxis = currentYAxis.filter((c) => c !== col); + } + + // ๋‹จ์ผ ๊ฐ’์ด๋ฉด ๋ฌธ์ž์—ด๋กœ, ๋‹ค์ค‘ ๊ฐ’์ด๋ฉด ๋ฐฐ์—ด๋กœ + if (newYAxis.length === 1) { + newYAxis = newYAxis[0]; + } + + updateConfig({ yAxis: newYAxis }); + }} + /> + +
+ ); + })} +
+
+

+ ํŒ: ์—ฌ๋Ÿฌ ํ•ญ๋ชฉ์„ ์„ ํƒํ•˜๋ฉด ๋น„๊ต ์ฐจํŠธ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค (์˜ˆ: ๊ฐค๋Ÿญ์‹œ vs ์•„์ดํฐ) +

+ + {/* ์ง‘๊ณ„ ํ•จ์ˆ˜ */}
-
{/* ๊ทธ๋ฃนํ•‘ ํ•„๋“œ (์„ ํƒ์‚ฌํ•ญ) */}
- - + +
+ + {/* ์ฐจํŠธ ์ƒ‰์ƒ */}
- +
{[ - ['#3B82F6', '#EF4444', '#10B981', '#F59E0B'], // ๊ธฐ๋ณธ - ['#8B5CF6', '#EC4899', '#06B6D4', '#84CC16'], // ๋ฐ์€ - ['#1F2937', '#374151', '#6B7280', '#9CA3AF'], // ํšŒ์ƒ‰ - ['#DC2626', '#EA580C', '#CA8A04', '#65A30D'], // ๋”ฐ๋œปํ•œ + ["#3B82F6", "#EF4444", "#10B981", "#F59E0B"], // ๊ธฐ๋ณธ + ["#8B5CF6", "#EC4899", "#06B6D4", "#84CC16"], // ๋ฐ์€ + ["#1F2937", "#374151", "#6B7280", "#9CA3AF"], // ํšŒ์ƒ‰ + ["#DC2626", "#EA580C", "#CA8A04", "#65A30D"], // ๋”ฐ๋œปํ•œ ].map((colorSet, setIdx) => ( +
- {/* ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜ */} -
- - + {/* ์ง„ํ–‰ ์ƒํ™ฉ ํ‘œ์‹œ */} +
+
+
+ ๋‹จ๊ณ„ {currentStep} / 3: {currentStep === 1 && "๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ ํƒ"} + {currentStep === 2 && "๋ฐ์ดํ„ฐ ์„ค์ •"} + {currentStep === 3 && "์ฐจํŠธ ์„ค์ •"} +
+ {Math.round((currentStep / 3) * 100)}% ์™„๋ฃŒ +
+
- {/* ํƒญ ๋‚ด์šฉ */} + {/* ๋‹จ๊ณ„๋ณ„ ๋‚ด์šฉ */}
- {activeTab === "query" && ( - + {currentStep === 1 && ( + )} - {activeTab === "chart" && ( + {currentStep === 2 && ( +
+ {dataSource.type === "database" ? ( + <> + + + + ) : ( + + )} +
+ )} + + {currentStep === 3 && ( )}
{/* ๋ชจ๋‹ฌ ํ‘ธํ„ฐ */} -
-
- {dataSource.query && ( - <> - ๐Ÿ’พ ์ฟผ๋ฆฌ: {dataSource.query.length > 50 ? `${dataSource.query.substring(0, 50)}...` : dataSource.query} - +
+
+ {queryResult && ( + + ๐Ÿ“Š {queryResult.rows.length}๊ฐœ ๋ฐ์ดํ„ฐ ๋กœ๋“œ๋จ + )}
- + )} + - + + {currentStep < 3 ? ( + + ) : ( + + )}
diff --git a/frontend/components/admin/dashboard/QueryEditor.tsx b/frontend/components/admin/dashboard/QueryEditor.tsx index 5aa70a80..c826961d 100644 --- a/frontend/components/admin/dashboard/QueryEditor.tsx +++ b/frontend/components/admin/dashboard/QueryEditor.tsx @@ -1,7 +1,16 @@ -'use client'; +"use client"; -import React, { useState, useCallback } from 'react'; -import { ChartDataSource, QueryResult } from './types'; +import React, { useState, useCallback } from "react"; +import { ChartDataSource, QueryResult } from "./types"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Label } from "@/components/ui/label"; +import { Card } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Play, Loader2, Database, Code } from "lucide-react"; interface QueryEditorProps { dataSource?: ChartDataSource; @@ -16,7 +25,7 @@ interface QueryEditorProps { * - ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • */ export function QueryEditor({ dataSource, onDataSourceChange, onQueryTest }: QueryEditorProps) { - const [query, setQuery] = useState(dataSource?.query || ''); + const [query, setQuery] = useState(dataSource?.query || ""); const [isExecuting, setIsExecuting] = useState(false); const [queryResult, setQueryResult] = useState(null); const [error, setError] = useState(null); @@ -24,7 +33,7 @@ export function QueryEditor({ dataSource, onDataSourceChange, onQueryTest }: Que // ์ฟผ๋ฆฌ ์‹คํ–‰ const executeQuery = useCallback(async () => { if (!query.trim()) { - setError('์ฟผ๋ฆฌ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.'); + setError("์ฟผ๋ฆฌ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."); return; } @@ -33,24 +42,24 @@ export function QueryEditor({ dataSource, onDataSourceChange, onQueryTest }: Que try { // ์‹ค์ œ API ํ˜ธ์ถœ - const response = await fetch('http://localhost:8080/api/dashboards/execute-query', { - method: 'POST', + const response = await fetch("http://localhost:8080/api/dashboards/execute-query", { + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem('token') || 'test-token'}` // JWT ํ† ํฐ ์‚ฌ์šฉ + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token") || "test-token"}`, // JWT ํ† ํฐ ์‚ฌ์šฉ }, - body: JSON.stringify({ query: query.trim() }) + body: JSON.stringify({ query: query.trim() }), }); if (!response.ok) { const errorData = await response.json(); - throw new Error(errorData.message || '์ฟผ๋ฆฌ ์‹คํ–‰์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + throw new Error(errorData.message || "์ฟผ๋ฆฌ ์‹คํ–‰์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } const apiResult = await response.json(); - + if (!apiResult.success) { - throw new Error(apiResult.message || '์ฟผ๋ฆฌ ์‹คํ–‰์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + throw new Error(apiResult.message || "์ฟผ๋ฆฌ ์‹คํ–‰์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } // API ๊ฒฐ๊ณผ๋ฅผ QueryResult ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ @@ -58,22 +67,21 @@ export function QueryEditor({ dataSource, onDataSourceChange, onQueryTest }: Que columns: apiResult.data.columns, rows: apiResult.data.rows, totalRows: apiResult.data.rowCount, - executionTime: 0 // API์—์„œ ์‹คํ–‰ ์‹œ๊ฐ„์„ ์ œ๊ณตํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ 0์œผ๋กœ ์„ค์ • + executionTime: 0, // API์—์„œ ์‹คํ–‰ ์‹œ๊ฐ„์„ ์ œ๊ณตํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ 0์œผ๋กœ ์„ค์ • }; - + setQueryResult(result); onQueryTest?.(result); // ๋ฐ์ดํ„ฐ ์†Œ์Šค ์—…๋ฐ์ดํŠธ onDataSourceChange({ - type: 'database', + type: "database", query: query.trim(), refreshInterval: dataSource?.refreshInterval || 30000, - lastExecuted: new Date().toISOString() + lastExecuted: new Date().toISOString(), }); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : '์ฟผ๋ฆฌ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'; + const errorMessage = err instanceof Error ? err.message : "์ฟผ๋ฆฌ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; setError(errorMessage); // console.error('Query execution error:', err); } finally { @@ -105,7 +113,7 @@ FROM orders WHERE order_date >= CURRENT_DATE - INTERVAL '12 months' GROUP BY DATE_TRUNC('month', order_date) ORDER BY month;`, - + users: `-- ์‚ฌ์šฉ์ž ๊ฐ€์ž… ์ถ”์ด SELECT DATE_TRUNC('week', created_at) as week, @@ -114,7 +122,7 @@ FROM users WHERE created_at >= CURRENT_DATE - INTERVAL '3 months' GROUP BY DATE_TRUNC('week', created_at) ORDER BY week;`, - + products: `-- ์ƒํ’ˆ๋ณ„ ํŒ๋งค๋Ÿ‰ SELECT product_name, @@ -137,192 +145,179 @@ SELECT FROM regional_sales WHERE year = EXTRACT(YEAR FROM CURRENT_DATE) GROUP BY region -ORDER BY Q4 DESC;` +ORDER BY Q4 DESC;`, }; - setQuery(samples[sampleType as keyof typeof samples] || ''); + setQuery(samples[sampleType as keyof typeof samples] || ""); }, []); return ( -
+
{/* ์ฟผ๋ฆฌ ์—๋””ํ„ฐ ํ—ค๋” */} -
-

๐Ÿ“ SQL ์ฟผ๋ฆฌ ์—๋””ํ„ฐ

-
- +
+
+ +

SQL ์ฟผ๋ฆฌ ์—๋””ํ„ฐ

+
{/* ์ƒ˜ํ”Œ ์ฟผ๋ฆฌ ๋ฒ„ํŠผ๋“ค */} -
- ์ƒ˜ํ”Œ ์ฟผ๋ฆฌ: - - - - - -
+ +
+ + + + + + +
+
{/* SQL ์ฟผ๋ฆฌ ์ž…๋ ฅ ์˜์—ญ */} -
-