155 lines
5.0 KiB
TypeScript
155 lines
5.0 KiB
TypeScript
"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;
|
||
}
|
||
|
||
// 데이터 소스가 설정되어 있으면 페칭
|
||
if (element.dataSource && element.dataSource.query && element.chartConfig) {
|
||
setIsLoading(true);
|
||
setError(null);
|
||
|
||
try {
|
||
let queryResult: QueryResult;
|
||
|
||
// 현재 DB vs 외부 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,
|
||
};
|
||
}
|
||
|
||
// 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>
|
||
);
|
||
}
|