"use client"; import React, { useState, useEffect } from "react"; import { DashboardElement } from "@/components/admin/dashboard/types"; import { getApiUrl } from "@/lib/utils/apiUrl"; interface CustomMetricTestWidgetProps { element: DashboardElement; } // 필터 적용 함수 const applyFilters = (rows: any[], filters?: Array<{ column: string; operator: string; value: string }>): any[] => { if (!filters || filters.length === 0) return rows; return rows.filter((row) => { return filters.every((filter) => { const cellValue = String(row[filter.column] || ""); const filterValue = filter.value; switch (filter.operator) { case "=": return cellValue === filterValue; case "!=": return cellValue !== filterValue; case ">": return parseFloat(cellValue) > parseFloat(filterValue); case "<": return parseFloat(cellValue) < parseFloat(filterValue); case ">=": return parseFloat(cellValue) >= parseFloat(filterValue); case "<=": return parseFloat(cellValue) <= parseFloat(filterValue); case "contains": return cellValue.includes(filterValue); case "not_contains": return !cellValue.includes(filterValue); default: return true; } }); }); }; // 집계 함수 실행 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; } }; export default function CustomMetricTestWidget({ element }: CustomMetricTestWidgetProps) { const [value, setValue] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const config = element?.customMetricConfig; console.log("📊 [CustomMetricTestWidget] 렌더링:", { element, config, dataSource: element?.dataSource, }); useEffect(() => { loadData(); // 자동 새로고침 (30초마다) const interval = setInterval(loadData, 30000); return () => clearInterval(interval); // eslint-disable-next-line react-hooks/exhaustive-deps }, [element]); const loadData = async () => { try { setLoading(true); setError(null); const dataSourceType = element?.dataSource?.type; // Database 타입 if (dataSourceType === "database") { if (!element?.dataSource?.query) { setValue(0); return; } const token = localStorage.getItem("authToken"); const response = await fetch(getApiUrl("/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 as any).connectionId, }), }); if (!response.ok) throw new Error("데이터 로딩 실패"); const result = await response.json(); if (result.success && result.data?.rows) { let rows = result.data.rows; // 필터 적용 if (config?.filters && config.filters.length > 0) { rows = applyFilters(rows, config.filters); } // 집계 계산 if (config?.valueColumn && config?.aggregation) { const calculatedValue = calculateMetric(rows, config.valueColumn, config.aggregation); setValue(calculatedValue); } else { setValue(0); } } else { throw new Error(result.message || "데이터 로드 실패"); } } // API 타입 else if (dataSourceType === "api") { if (!element?.dataSource?.endpoint) { setValue(0); return; } const token = localStorage.getItem("authToken"); const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ method: (element.dataSource as any).method || "GET", url: element.dataSource.endpoint, headers: (element.dataSource as any).headers || {}, body: (element.dataSource as any).body, authType: (element.dataSource as any).authType, authConfig: (element.dataSource as any).authConfig, }), }); if (!response.ok) throw new Error("API 호출 실패"); const result = await response.json(); if (result.success && result.data) { let rows: any[] = []; // API 응답 데이터 구조 확인 및 처리 if (Array.isArray(result.data)) { rows = result.data; } else if (result.data.results && Array.isArray(result.data.results)) { rows = result.data.results; } else if (result.data.items && Array.isArray(result.data.items)) { rows = result.data.items; } else if (result.data.data && Array.isArray(result.data.data)) { rows = result.data.data; } else { rows = [result.data]; } // 필터 적용 if (config?.filters && config.filters.length > 0) { rows = applyFilters(rows, config.filters); } // 집계 계산 if (config?.valueColumn && config?.aggregation) { const calculatedValue = calculateMetric(rows, config.valueColumn, config.aggregation); setValue(calculatedValue); } else { setValue(0); } } else { throw new Error("API 응답 형식 오류"); } } } catch (err) { console.error("데이터 로드 실패:", err); setError(err instanceof Error ? err.message : "데이터를 불러올 수 없습니다"); } finally { setLoading(false); } }; if (loading) { return (

데이터 로딩 중...

); } if (error) { return (

⚠️ {error}

); } // 설정 체크 const hasDataSource = (element?.dataSource?.type === "database" && element?.dataSource?.query) || (element?.dataSource?.type === "api" && element?.dataSource?.endpoint); const hasConfig = config?.valueColumn && config?.aggregation; // 설정이 없으면 안내 화면 if (!hasDataSource || !hasConfig) { return (

통계 카드

📊 단일 통계 위젯

  • • 데이터 소스에서 쿼리를 실행합니다
  • • 필터 조건으로 데이터를 필터링합니다
  • • 선택한 컬럼에 집계 함수를 적용합니다
  • • COUNT, SUM, AVG, MIN, MAX 지원

⚙️ 설정 방법

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

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

3. 계산 컬럼 및 방식 선택

4. 제목 및 단위 입력

); } // 소수점 자릿수 (기본: 0) const decimals = config?.decimals ?? 0; const formattedValue = value.toFixed(decimals); // 통계 카드 렌더링 (전체 크기 꽉 차게) return (
{/* 제목 */}
{config?.title || "통계"}
{/* 값 */}
{formattedValue} {config?.unit && {config.unit}}
{/* 필터 표시 (디버깅용, 작게) */} {config?.filters && config.filters.length > 0 && (
필터: {config.filters.length}개 적용됨
)}
); }