diff --git a/frontend/components/admin/dashboard/QueryEditor.tsx b/frontend/components/admin/dashboard/QueryEditor.tsx index 8f0b65e0..50e57689 100644 --- a/frontend/components/admin/dashboard/QueryEditor.tsx +++ b/frontend/components/admin/dashboard/QueryEditor.tsx @@ -1,12 +1,11 @@ "use client"; import React, { useState, useCallback } from "react"; -import { ChartDataSource, QueryResult, ChartConfig } from "./types"; +import { ChartDataSource, QueryResult } from "./types"; import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection"; import { dashboardApi } from "@/lib/api/dashboard"; 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"; @@ -14,7 +13,6 @@ import { Alert, AlertDescription } from "@/components/ui/alert"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Play, Loader2, Database, Code, ChevronDown, ChevronRight } from "lucide-react"; -import { applyQueryFilters } from "./utils/queryHelpers"; interface QueryEditorProps { dataSource?: ChartDataSource; @@ -106,7 +104,6 @@ export function QueryEditor({ dataSource, onDataSourceChange, onQueryTest }: Que ...dataSource, type: "database", query: query.trim(), - refreshInterval: dataSource?.refreshInterval ?? 0, lastExecuted: new Date().toISOString(), }); } catch (err) { @@ -168,8 +165,8 @@ ORDER BY 하위부서수 DESC`, {/* 쿼리 에디터 헤더 */}
- -

SQL 쿼리 에디터

+ +

SQL 쿼리 에디터

@@ -247,46 +244,6 @@ ORDER BY 하위부서수 DESC`,
- {/* 새로고침 간격 설정 */} -
- - -
- {/* 오류 메시지 */} {error && ( @@ -300,15 +257,15 @@ ORDER BY 하위부서수 DESC`, {/* 쿼리 결과 미리보기 */} {queryResult && ( -
+
- 쿼리 결과 + 쿼리 결과 {queryResult.rows.length}행
- 실행 시간: {queryResult.executionTime}ms + 실행 시간: {queryResult.executionTime}ms
@@ -339,13 +296,13 @@ ORDER BY 하위부서수 DESC`, {queryResult.rows.length > 10 && ( -
+
... 및 {queryResult.rows.length - 10}개 더 (미리보기는 10행까지만 표시)
)}
) : ( -
결과가 없습니다.
+
결과가 없습니다.
)}
@@ -353,169 +310,3 @@ ORDER BY 하위부서수 DESC`, ); } - -/** - * 샘플 쿼리 결과 생성 함수 - */ -function generateSampleQueryResult(query: string): QueryResult { - // 쿼리에서 키워드 추출하여 적절한 샘플 데이터 생성 - const queryLower = query.toLowerCase(); - - // 디버깅용 로그 - // console.log('generateSampleQueryResult called with query:', query.substring(0, 100)); - - // 가장 구체적인 조건부터 먼저 체크 (순서 중요!) - const isComparison = - queryLower.includes("galaxy") || - queryLower.includes("갤럭시") || - queryLower.includes("아이폰") || - queryLower.includes("iphone"); - const isRegional = queryLower.includes("region") || queryLower.includes("지역"); - const isMonthly = queryLower.includes("month"); - const isSales = queryLower.includes("sales") || queryLower.includes("매출"); - const isUsers = queryLower.includes("users") || queryLower.includes("사용자"); - const isProducts = queryLower.includes("product") || queryLower.includes("상품"); - const isWeekly = queryLower.includes("week"); - - // console.log('Sample data type detection:', { - // isComparison, - // isRegional, - // isWeekly, - // isProducts, - // isMonthly, - // isSales, - // isUsers, - // querySnippet: query.substring(0, 200) - // }); - - let columns: string[]; - let rows: Record[]; - - // 더 구체적인 조건부터 먼저 체크 (순서 중요!) - if (isComparison) { - // console.log('✅ Using COMPARISON data'); - // 제품 비교 데이터 (다중 시리즈) - columns = ["month", "galaxy_sales", "iphone_sales", "other_sales"]; - rows = [ - { month: "2024-01", galaxy_sales: 450000, iphone_sales: 620000, other_sales: 130000 }, - { month: "2024-02", galaxy_sales: 520000, iphone_sales: 680000, other_sales: 150000 }, - { month: "2024-03", galaxy_sales: 480000, iphone_sales: 590000, other_sales: 110000 }, - { month: "2024-04", galaxy_sales: 610000, iphone_sales: 650000, other_sales: 160000 }, - { month: "2024-05", galaxy_sales: 720000, iphone_sales: 780000, other_sales: 180000 }, - { month: "2024-06", galaxy_sales: 680000, iphone_sales: 690000, other_sales: 170000 }, - { month: "2024-07", galaxy_sales: 750000, iphone_sales: 800000, other_sales: 170000 }, - { month: "2024-08", galaxy_sales: 690000, iphone_sales: 720000, other_sales: 170000 }, - { month: "2024-09", galaxy_sales: 730000, iphone_sales: 750000, other_sales: 170000 }, - { month: "2024-10", galaxy_sales: 800000, iphone_sales: 810000, other_sales: 170000 }, - { month: "2024-11", galaxy_sales: 870000, iphone_sales: 880000, other_sales: 170000 }, - { month: "2024-12", galaxy_sales: 950000, iphone_sales: 990000, other_sales: 160000 }, - ]; - // COMPARISON 데이터를 반환하고 함수 종료 - // console.log('COMPARISON data generated:', { - // columns, - // rowCount: rows.length, - // sampleRow: rows[0], - // allRows: rows, - // fieldTypes: { - // month: typeof rows[0].month, - // galaxy_sales: typeof rows[0].galaxy_sales, - // iphone_sales: typeof rows[0].iphone_sales, - // other_sales: typeof rows[0].other_sales - // }, - // firstFewRows: rows.slice(0, 3), - // lastFewRows: rows.slice(-3) - // }); - return { - columns, - rows, - totalRows: rows.length, - executionTime: Math.floor(Math.random() * 200) + 100, - }; - } else if (isRegional) { - // console.log('✅ Using REGIONAL data'); - // 지역별 분기별 매출 - columns = ["지역", "Q1", "Q2", "Q3", "Q4"]; - rows = [ - { 지역: "서울", Q1: 1200000, Q2: 1350000, Q3: 1420000, Q4: 1580000 }, - { 지역: "경기", Q1: 980000, Q2: 1120000, Q3: 1180000, Q4: 1290000 }, - { 지역: "부산", Q1: 650000, Q2: 720000, Q3: 780000, Q4: 850000 }, - { 지역: "대구", Q1: 450000, Q2: 490000, Q3: 520000, Q4: 580000 }, - { 지역: "인천", Q1: 520000, Q2: 580000, Q3: 620000, Q4: 690000 }, - { 지역: "광주", Q1: 380000, Q2: 420000, Q3: 450000, Q4: 490000 }, - { 지역: "대전", Q1: 410000, Q2: 460000, Q3: 490000, Q4: 530000 }, - ]; - } else if (isWeekly && isUsers) { - // console.log('✅ Using USERS data'); - // 사용자 가입 추이 - columns = ["week", "new_users"]; - rows = [ - { week: "2024-W10", new_users: 23 }, - { week: "2024-W11", new_users: 31 }, - { week: "2024-W12", new_users: 28 }, - { week: "2024-W13", new_users: 35 }, - { week: "2024-W14", new_users: 42 }, - { week: "2024-W15", new_users: 38 }, - { week: "2024-W16", new_users: 45 }, - { week: "2024-W17", new_users: 52 }, - { week: "2024-W18", new_users: 48 }, - { week: "2024-W19", new_users: 55 }, - { week: "2024-W20", new_users: 61 }, - { week: "2024-W21", new_users: 58 }, - ]; - } else if (isProducts && !isComparison) { - // console.log('✅ Using PRODUCTS data'); - // 상품별 판매량 - columns = ["product_name", "total_sold", "revenue"]; - rows = [ - { product_name: "스마트폰", total_sold: 156, revenue: 234000000 }, - { product_name: "노트북", total_sold: 89, revenue: 178000000 }, - { product_name: "태블릿", total_sold: 134, revenue: 67000000 }, - { product_name: "이어폰", total_sold: 267, revenue: 26700000 }, - { product_name: "스마트워치", total_sold: 98, revenue: 49000000 }, - { product_name: "키보드", total_sold: 78, revenue: 15600000 }, - { product_name: "마우스", total_sold: 145, revenue: 8700000 }, - { product_name: "모니터", total_sold: 67, revenue: 134000000 }, - { product_name: "프린터", total_sold: 34, revenue: 17000000 }, - { product_name: "웹캠", total_sold: 89, revenue: 8900000 }, - ]; - } else if (isMonthly && isSales && !isComparison) { - // console.log('✅ Using MONTHLY SALES data'); - // 월별 매출 데이터 - columns = ["month", "sales", "order_count"]; - rows = [ - { month: "2024-01", sales: 1200000, order_count: 45 }, - { month: "2024-02", sales: 1350000, order_count: 52 }, - { month: "2024-03", sales: 1180000, order_count: 41 }, - { month: "2024-04", sales: 1420000, order_count: 58 }, - { month: "2024-05", sales: 1680000, order_count: 67 }, - { month: "2024-06", sales: 1540000, order_count: 61 }, - { month: "2024-07", sales: 1720000, order_count: 71 }, - { month: "2024-08", sales: 1580000, order_count: 63 }, - { month: "2024-09", sales: 1650000, order_count: 68 }, - { month: "2024-10", sales: 1780000, order_count: 75 }, - { month: "2024-11", sales: 1920000, order_count: 82 }, - { month: "2024-12", sales: 2100000, order_count: 89 }, - ]; - } else { - // console.log('⚠️ Using DEFAULT data'); - // 기본 샘플 데이터 - columns = ["category", "value", "count"]; - rows = [ - { category: "A", value: 100, count: 10 }, - { category: "B", value: 150, count: 15 }, - { category: "C", value: 120, count: 12 }, - { category: "D", value: 180, count: 18 }, - { category: "E", value: 90, count: 9 }, - { category: "F", value: 200, count: 20 }, - { category: "G", value: 110, count: 11 }, - { category: "H", value: 160, count: 16 }, - ]; - } - - return { - columns, - rows, - totalRows: rows.length, - executionTime: Math.floor(Math.random() * 200) + 100, // 100-300ms - }; -} diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts index e0fdb3a1..982fe770 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -397,6 +397,7 @@ export interface CustomMetricConfig { unit?: string; // 표시 단위 (원, 건, % 등) color?: "indigo" | "green" | "blue" | "purple" | "orange" | "gray"; // 카드 색상 decimals?: number; // 소수점 자릿수 (기본: 0) + refreshInterval?: number; // 자동 새로고침 간격 (초, 0이면 비활성) // 필터 조건 filters?: Array<{ diff --git a/frontend/components/admin/dashboard/widget-sections/CustomMetricSection.tsx b/frontend/components/admin/dashboard/widget-sections/CustomMetricSection.tsx index f7f89e96..6dbbb60c 100644 --- a/frontend/components/admin/dashboard/widget-sections/CustomMetricSection.tsx +++ b/frontend/components/admin/dashboard/widget-sections/CustomMetricSection.tsx @@ -231,6 +231,29 @@ export function CustomMetricSection({ queryResult, config, onConfigChange }: Cus /> + {/* 6. 자동 새로고침 간격 */} +
+ + +

+ 통계 데이터를 자동으로 갱신하는 주기 +

+
+ {/* 미리보기 */} {config.valueColumn && config.aggregation && (
diff --git a/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx b/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx index 1b78801e..1aa36559 100644 --- a/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx +++ b/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx @@ -70,6 +70,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg const [value, setValue] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [lastUpdateTime, setLastUpdateTime] = useState(null); const config = element?.customMetricConfig; @@ -82,11 +83,15 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg useEffect(() => { loadData(); - // 자동 새로고침 (30초마다) - const interval = setInterval(loadData, 30000); - return () => clearInterval(interval); + // 자동 새로고침 (설정된 간격마다, 0이면 비활성) + const refreshInterval = config?.refreshInterval ?? 30; // 기본값: 30초 + + if (refreshInterval > 0) { + const interval = setInterval(loadData, refreshInterval * 1000); + return () => clearInterval(interval); + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [element]); + }, [element, config?.refreshInterval]); const loadData = async () => { try { @@ -132,6 +137,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg if (config?.valueColumn && config?.aggregation) { const calculatedValue = calculateMetric(rows, config.valueColumn, config.aggregation); setValue(calculatedValue); + setLastUpdateTime(new Date()); // 업데이트 시간 기록 } else { setValue(0); } @@ -192,6 +198,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg if (config?.valueColumn && config?.aggregation) { const calculatedValue = calculateMetric(rows, config.valueColumn, config.aggregation); setValue(calculatedValue); + setLastUpdateTime(new Date()); // 업데이트 시간 기록 } else { setValue(0); } @@ -283,6 +290,16 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg {formattedValue} {config?.unit && {config.unit}}
+ + {/* 마지막 업데이트 시간 */} + {lastUpdateTime && ( +
+ {lastUpdateTime.toLocaleTimeString("ko-KR")} + {config?.refreshInterval && config.refreshInterval > 0 && ( + • {config.refreshInterval}초마다 갱신 + )} +
+ )} ); } diff --git a/frontend/components/dashboard/widgets/CustomMetricWidget.tsx b/frontend/components/dashboard/widgets/CustomMetricWidget.tsx index fcd5593f..7c39c731 100644 --- a/frontend/components/dashboard/widgets/CustomMetricWidget.tsx +++ b/frontend/components/dashboard/widgets/CustomMetricWidget.tsx @@ -70,17 +70,22 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps) const [value, setValue] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [lastUpdateTime, setLastUpdateTime] = useState(null); const config = element?.customMetricConfig; useEffect(() => { loadData(); - // 자동 새로고침 (30초마다) - const interval = setInterval(loadData, 30000); - return () => clearInterval(interval); + // 자동 새로고침 (설정된 간격마다, 0이면 비활성) + const refreshInterval = config?.refreshInterval ?? 30; // 기본값: 30초 + + if (refreshInterval > 0) { + const interval = setInterval(loadData, refreshInterval * 1000); + return () => clearInterval(interval); + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [element]); + }, [element, config?.refreshInterval]); const loadData = async () => { try { @@ -198,15 +203,16 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps) setError(err instanceof Error ? err.message : "데이터를 불러올 수 없습니다"); } finally { setLoading(false); + setLastUpdateTime(new Date()); } }; if (loading) { return ( -
+
-

데이터 로딩 중...

+

데이터 로딩 중...

); @@ -214,12 +220,12 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps) if (error) { return ( -
+
-

⚠️ {error}

+

⚠️ {error}

@@ -238,10 +244,10 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps) // 설정이 없으면 안내 화면 if (!hasDataSource || !hasConfig) { return ( -
+
-

통계 카드

-
+

통계 카드

+

📊 단일 통계 위젯

  • • 데이터 소스에서 쿼리를 실행합니다
  • @@ -250,7 +256,7 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps)
  • • COUNT, SUM, AVG, MIN, MAX 지원
-
+

⚙️ 설정 방법

1. 데이터 탭에서 쿼리 실행

2. 필터 조건 추가 (선택사항)

@@ -268,7 +274,7 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps) // 통계 카드 렌더링 return ( -
+
{/* 제목 */}
{config?.title || "통계"}
@@ -277,6 +283,16 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps) {formattedValue} {config?.unit && {config.unit}}
+ + {/* 마지막 업데이트 시간 */} + {lastUpdateTime && ( +
+ {lastUpdateTime.toLocaleTimeString("ko-KR")} + {config?.refreshInterval && config.refreshInterval > 0 && ( + • {config.refreshInterval}초마다 갱신 + )} +
+ )}
); }