ERP-node/frontend/components/admin/dashboard/charts/ChartRenderer.tsx

215 lines
7.2 KiB
TypeScript
Raw Normal View History

"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";
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<ChartData | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// 데이터 페칭
useEffect(() => {
const fetchData = async () => {
// 이미 data가 전달된 경우 사용
if (data) {
const transformed = transformQueryResultToChartData(data, element.chartConfig || {});
setChartData(transformed);
return;
}
// 데이터 소스가 설정되어 있으면 페칭
2025-10-14 17:54:28 +09:00
if (element.dataSource && element.chartConfig) {
setIsLoading(true);
setError(null);
try {
let queryResult: QueryResult;
2025-10-14 17:54:28 +09:00
// REST API vs Database 분기
if (element.dataSource.type === "api" && element.dataSource.endpoint) {
// REST API
const params = new URLSearchParams();
if (element.dataSource.queryParams) {
Object.entries(element.dataSource.queryParams).forEach(([key, value]) => {
if (key && value) {
params.append(key, value);
}
});
}
2025-10-14 17:54:28 +09:00
let url = element.dataSource.endpoint;
const queryString = params.toString();
if (queryString) {
url += (url.includes("?") ? "&" : "?") + queryString;
}
2025-10-14 17:54:28 +09:00
const headers: Record<string, string> = {
"Content-Type": "application/json",
...element.dataSource.headers,
};
2025-10-14 17:54:28 +09:00
const response = await fetch(url, {
method: "GET",
headers,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const apiData = await response.json();
// 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 = {
2025-10-14 17:54:28 +09:00
columns,
rows,
totalRows: rows.length,
executionTime: 0,
};
2025-10-14 17:54:28 +09:00
} else if (element.dataSource.query) {
// Database (현재 DB 또는 외부 DB)
if (element.dataSource.connectionType === "external" && element.dataSource.externalConnectionId) {
// 외부 DB
const result = await ExternalDbConnectionAPI.executeQuery(
parseInt(element.dataSource.externalConnectionId),
element.dataSource.query,
);
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 result = await dashboardApi.executeQuery(element.dataSource.query);
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 (
<div className="flex h-full w-full items-center justify-center text-gray-500">
<div className="text-center">
<div className="mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-blue-600 border-t-transparent" />
<div className="text-sm"> ...</div>
</div>
</div>
);
}
// 에러
if (error) {
return (
<div className="flex h-full w-full items-center justify-center text-red-500">
<div className="text-center">
<div className="mb-2 text-2xl"></div>
<div className="text-sm font-medium"> </div>
<div className="mt-1 text-xs">{error}</div>
</div>
</div>
);
}
// 데이터나 설정이 없으면
if (!chartData || !element.chartConfig?.xAxis || !element.chartConfig?.yAxis) {
return (
<div className="flex h-full w-full items-center justify-center text-gray-500">
<div className="text-center">
<div className="mb-2 text-2xl">📊</div>
<div className="text-sm"> </div>
<div className="mt-1 text-xs"> </div>
</div>
</div>
);
}
// D3 차트 렌더링
return (
<div className="flex h-full w-full items-center justify-center bg-white p-2">
<Chart
chartType={element.subtype}
data={chartData}
config={element.chartConfig}
width={width - 20}
height={height - 20}
/>
</div>
);
}