"use client"; import React, { useEffect, useState, useRef } 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"; import { getApiUrl } from "@/lib/utils/apiUrl"; interface ChartRendererProps { element: DashboardElement; data?: QueryResult; width?: number; height?: number; } /** * 차트 렌더러 컴포넌트 (D3 기반) * - 데이터 소스에서 데이터 페칭 * - QueryResult를 ChartData로 변환 * - D3 Chart 컴포넌트에 전달 */ export function ChartRenderer({ element, data, width, height = 200 }: ChartRendererProps) { const containerRef = useRef(null); const [containerWidth, setContainerWidth] = useState(width || 250); const [chartData, setChartData] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // 컨테이너 너비 측정 (width가 undefined일 때) useEffect(() => { if (width !== undefined) { setContainerWidth(width); return; } const updateWidth = () => { if (containerRef.current) { const measuredWidth = containerRef.current.offsetWidth; console.log("📏 컨테이너 너비 측정:", measuredWidth); setContainerWidth(measuredWidth || 500); // 기본값 500 } }; // 약간의 지연을 두고 측정 (DOM 렌더링 완료 후) const timer = setTimeout(updateWidth, 100); updateWidth(); window.addEventListener("resize", updateWidth); return () => { clearTimeout(timer); window.removeEventListener("resize", updateWidth); }; }, [width]); // 데이터 페칭 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, String(value)); } }); } const response = await fetch(getApiUrl("/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); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ element.dataSource?.query, element.dataSource?.connectionType, element.dataSource?.externalConnectionId, element.dataSource?.refreshInterval, element.dataSource?.type, element.dataSource?.endpoint, element.dataSource?.jsonPath, 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 차트 렌더링 const actualWidth = width !== undefined ? width : containerWidth; // 최소 크기 제약 완화 (1x1 위젯 지원) const isCircularChart = element.subtype === "pie" || element.subtype === "donut"; const minWidth = 35; // 최소 너비 35px const finalWidth = Math.max(actualWidth - 4, minWidth); // 최소 높이도 35px로 설정 const finalHeight = Math.max(height - (isCircularChart ? 10 : 4), 35); console.log("🎨 ChartRenderer:", { elementSubtype: element.subtype, propWidth: width, containerWidth, actualWidth, finalWidth, finalHeight, hasChartData: !!chartData, chartDataLabels: chartData?.labels, chartDataDatasets: chartData?.datasets?.length, isCircularChart, }); return (
); }