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 쿼리 에디터
{isExecuting ? (
@@ -188,7 +185,7 @@ ORDER BY 하위부서수 DESC`,
{/* 샘플 쿼리 아코디언 */}
-
+
{sampleQueryOpen ? : }
샘플 쿼리
@@ -196,33 +193,33 @@ ORDER BY 하위부서수 DESC`,
insertSampleQuery("users")}
- className="flex items-center gap-1 rounded border border-border bg-background px-2 py-1 text-[11px] transition-colors hover:bg-muted"
+ className="border-border bg-background hover:bg-muted flex items-center gap-1 rounded border px-2 py-1 text-[11px] transition-colors"
>
부서별 사용자
insertSampleQuery("dept")}
- className="flex items-center gap-1 rounded border border-border bg-background px-2 py-1 text-[11px] transition-colors hover:bg-muted"
+ className="border-border bg-background hover:bg-muted flex items-center gap-1 rounded border px-2 py-1 text-[11px] transition-colors"
>
부서 정보
insertSampleQuery("usersByDate")}
- className="rounded border border-border bg-background px-2 py-1 text-[11px] transition-colors hover:bg-muted"
+ className="border-border bg-background hover:bg-muted rounded border px-2 py-1 text-[11px] transition-colors"
>
월별 가입 추이
insertSampleQuery("usersByPosition")}
- className="rounded border border-border bg-background px-2 py-1 text-[11px] transition-colors hover:bg-muted"
+ className="border-border bg-background hover:bg-muted rounded border px-2 py-1 text-[11px] transition-colors"
>
직급별 분포
insertSampleQuery("deptHierarchy")}
- className="rounded border border-border bg-background px-2 py-1 text-[11px] transition-colors hover:bg-muted"
+ className="border-border bg-background hover:bg-muted rounded border px-2 py-1 text-[11px] transition-colors"
>
부서 계층
@@ -247,46 +244,6 @@ ORDER BY 하위부서수 DESC`,
- {/* 새로고침 간격 설정 */}
-
- 자동 새로고침:
-
- onDataSourceChange({
- ...dataSource,
- type: "database",
- query,
- refreshInterval: parseInt(value),
- })
- }
- >
-
-
-
-
-
- 수동
-
-
- 10초
-
-
- 30초
-
-
- 1분
-
-
- 5분
-
-
- 10분
-
-
-
-
-
{/* 오류 메시지 */}
{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. 자동 새로고침 간격 */}
+
+
자동 새로고침
+
onConfigChange({ refreshInterval: parseInt(value) })}
+ >
+
+
+
+
+ 없음
+ 10초
+ 30초
+ 1분
+ 5분
+
+
+
+ 통계 데이터를 자동으로 갱신하는 주기
+
+
+
{/* 미리보기 */}
{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}초마다 갱신
+ )}
+
+ )}
);
}