"use client"; import React, { useState, useEffect } from "react"; import { DashboardElement } from "@/components/admin/dashboard/types"; interface CustomMetricWidgetProps { element?: DashboardElement; } // 집계 함수 실행 const calculateMetric = (rows: any[], field: string, aggregation: string): number => { if (rows.length === 0) return 0; switch (aggregation) { case "count": return rows.length; case "sum": { return rows.reduce((sum, row) => sum + (parseFloat(row[field]) || 0), 0); } case "avg": { const sum = rows.reduce((s, row) => s + (parseFloat(row[field]) || 0), 0); return rows.length > 0 ? sum / rows.length : 0; } case "min": { return Math.min(...rows.map((row) => parseFloat(row[field]) || 0)); } case "max": { return Math.max(...rows.map((row) => parseFloat(row[field]) || 0)); } default: return 0; } }; // 색상 스타일 매핑 const colorMap = { indigo: { bg: "bg-indigo-50", text: "text-indigo-600", border: "border-indigo-200" }, green: { bg: "bg-green-50", text: "text-green-600", border: "border-green-200" }, blue: { bg: "bg-blue-50", text: "text-blue-600", border: "border-blue-200" }, purple: { bg: "bg-purple-50", text: "text-purple-600", border: "border-purple-200" }, orange: { bg: "bg-orange-50", text: "text-orange-600", border: "border-orange-200" }, gray: { bg: "bg-gray-50", text: "text-gray-600", border: "border-gray-200" }, }; export default function CustomMetricWidget({ element }: CustomMetricWidgetProps) { const [metrics, setMetrics] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { console.log("🎯 CustomMetricWidget mounted, element:", element); console.log("📊 dataSource:", element?.dataSource); console.log("📈 customMetricConfig:", element?.customMetricConfig); loadData(); // 자동 새로고침 (30초마다) const interval = setInterval(loadData, 30000); return () => clearInterval(interval); }, [element]); const loadData = async () => { try { setLoading(true); setError(null); // 쿼리나 설정이 없으면 초기 상태로 반환 if (!element?.dataSource?.query || !element?.customMetricConfig?.metrics) { console.log("⚠️ 쿼리 또는 지표 설정이 없습니다"); console.log("- dataSource.query:", element?.dataSource?.query); console.log("- customMetricConfig.metrics:", element?.customMetricConfig?.metrics); setMetrics([]); setLoading(false); return; } console.log("✅ 쿼리 실행 시작:", element.dataSource.query); const token = localStorage.getItem("authToken"); const response = await fetch("/api/dashboards/execute-query", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ query: element.dataSource.query, connectionType: element.dataSource.connectionType || "current", connectionId: element.dataSource.connectionId, }), }); if (!response.ok) throw new Error("데이터 로딩 실패"); const result = await response.json(); if (result.success && result.data?.rows) { const rows = result.data.rows; // 각 메트릭 계산 const calculatedMetrics = element.customMetricConfig.metrics.map((metric) => { const value = calculateMetric(rows, metric.field, metric.aggregation); return { ...metric, calculatedValue: value, }; }); setMetrics(calculatedMetrics); } else { throw new Error(result.message || "데이터 로드 실패"); } } catch (err) { console.error("메트릭 로드 실패:", err); setError(err instanceof Error ? err.message : "데이터를 불러올 수 없습니다"); } finally { setLoading(false); } }; if (loading) { return (

데이터 로딩 중...

); } if (error) { return (

⚠️ {error}

); } if (!element?.dataSource?.query || !element?.customMetricConfig?.metrics || metrics.length === 0) { return (

사용자 커스텀 카드

📊 맞춤형 지표 위젯

  • • SQL 쿼리로 데이터를 불러옵니다
  • • 선택한 컬럼의 데이터로 지표를 계산합니다
  • • COUNT, SUM, AVG, MIN, MAX 등 집계 함수 지원
  • • 사용자 정의 단위 설정 가능

⚙️ 설정 방법

SQL 쿼리를 입력하고 지표를 추가하세요

); } return (
{/* 스크롤 가능한 콘텐츠 영역 */}
{metrics.map((metric) => { const colors = colorMap[metric.color as keyof typeof colorMap] || colorMap.gray; const formattedValue = metric.calculatedValue.toFixed(metric.decimals); return (
{metric.label}
{formattedValue} {metric.unit}
); })}
); }