"use client"; import React, { useEffect, useState } from "react"; import { DashboardElement, QueryResult, ChartData } from "../types"; import { Chart } from "./Chart"; import { transformQueryResultToChartData } from "../utils/chartDataTransform"; import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection"; import { dashboardApi } from "@/lib/api/dashboard"; import { applyQueryFilters } from "../utils/queryHelpers"; interface ChartRendererProps { element: DashboardElement; data?: QueryResult; width?: number; height?: number; } /** * 차트 렌더러 컴포넌트 (D3 기반) * - 데이터 소스에서 데이터 페칭 * - QueryResult를 ChartData로 변환 * - D3 Chart 컴포넌트에 전달 */ export function ChartRenderer({ element, data, width = 250, height = 200 }: ChartRendererProps) { const [chartData, setChartData] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // 데이터 페칭 useEffect(() => { const fetchData = async () => { // 이미 data가 전달된 경우 사용 if (data) { const transformed = transformQueryResultToChartData(data, element.chartConfig || {}); setChartData(transformed); return; } // 데이터 소스가 설정되어 있으면 페칭 if (element.dataSource && element.chartConfig) { setIsLoading(true); setError(null); try { let queryResult: QueryResult; // REST API vs Database 분기 if (element.dataSource.type === "api" && element.dataSource.endpoint) { // REST API - 백엔드 프록시를 통한 호출 (CORS 우회) const params = new URLSearchParams(); if (element.dataSource.queryParams) { Object.entries(element.dataSource.queryParams).forEach(([key, value]) => { if (key && value) { params.append(key, value); } }); } const response = await fetch("http://localhost:8080/api/dashboards/fetch-external-api", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ url: element.dataSource.endpoint, method: "GET", headers: element.dataSource.headers || {}, queryParams: Object.fromEntries(params), }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = await response.json(); if (!result.success) { throw new Error(result.message || "외부 API 호출 실패"); } const apiData = result.data; // JSON Path 처리 let processedData = apiData; if (element.dataSource.jsonPath) { const paths = element.dataSource.jsonPath.split("."); for (const path of paths) { if (processedData && typeof processedData === "object" && path in processedData) { processedData = processedData[path]; } else { throw new Error(`JSON Path "${element.dataSource.jsonPath}"에서 데이터를 찾을 수 없습니다`); } } } const rows = Array.isArray(processedData) ? processedData : [processedData]; const columns = rows.length > 0 ? Object.keys(rows[0]) : []; queryResult = { columns, rows, totalRows: rows.length, executionTime: 0, }; } else if (element.dataSource.query) { // Database (현재 DB 또는 외부 DB) if (element.dataSource.connectionType === "external" && element.dataSource.externalConnectionId) { // 외부 DB - 필터 적용 const filteredQuery = applyQueryFilters(element.dataSource.query, element.chartConfig); const result = await ExternalDbConnectionAPI.executeQuery( parseInt(element.dataSource.externalConnectionId), filteredQuery, ); if (!result.success) { throw new Error(result.message || "외부 DB 쿼리 실행 실패"); } queryResult = { columns: result.data?.[0] ? Object.keys(result.data[0]) : [], rows: result.data || [], totalRows: result.data?.length || 0, executionTime: 0, }; } else { // 현재 DB - 필터 적용 const filteredQuery = applyQueryFilters(element.dataSource.query, element.chartConfig); const result = await dashboardApi.executeQuery(filteredQuery); queryResult = { columns: result.columns, rows: result.rows, totalRows: result.rowCount, executionTime: 0, }; } } else { throw new Error("데이터 소스가 올바르게 설정되지 않았습니다"); } // ChartData로 변환 const transformed = transformQueryResultToChartData(queryResult, element.chartConfig); setChartData(transformed); } catch (err) { const errorMessage = err instanceof Error ? err.message : "데이터 로딩 실패"; setError(errorMessage); } finally { setIsLoading(false); } } }; fetchData(); // 자동 새로고침 설정 (0이면 수동이므로 interval 설정 안 함) const refreshInterval = element.dataSource?.refreshInterval; if (refreshInterval && refreshInterval > 0) { const interval = setInterval(fetchData, refreshInterval); return () => clearInterval(interval); } }, [ element.dataSource?.query, element.dataSource?.connectionType, element.dataSource?.externalConnectionId, element.dataSource?.refreshInterval, element.chartConfig, data, ]); // 로딩 중 if (isLoading) { return (
데이터 로딩 중...
); } // 에러 if (error) { return (
⚠️
오류 발생
{error}
); } // 데이터나 설정이 없으면 const isPieChart = element.subtype === "pie" || element.subtype === "donut"; const isApiSource = element.dataSource?.type === "api"; const needsYAxis = !(isPieChart || isApiSource) || (!element.chartConfig?.aggregation && !element.chartConfig?.yAxis); if (!chartData || !element.chartConfig?.xAxis || (needsYAxis && !element.chartConfig?.yAxis)) { return (
📊
데이터를 설정해주세요
⚙️ 버튼을 클릭하여 설정
); } // D3 차트 렌더링 return (
); }