ERP-node/frontend/components/dashboard/widgets/ChartTestWidget.tsx

354 lines
11 KiB
TypeScript

"use client";
import React, { useEffect, useState, useCallback, useMemo } from "react";
import { DashboardElement, ChartDataSource } from "@/components/admin/dashboard/types";
import { Button } from "@/components/ui/button";
import { Loader2, RefreshCw } from "lucide-react";
import {
LineChart,
Line,
BarChart,
Bar,
PieChart,
Pie,
Cell,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";
interface ChartTestWidgetProps {
element: DashboardElement;
}
const COLORS = ["#3b82f6", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6", "#ec4899"];
export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [lastRefreshTime, setLastRefreshTime] = useState<Date | null>(null);
console.log("🧪 ChartTestWidget 렌더링!", element);
const dataSources = useMemo(() => {
return element?.dataSources || element?.chartConfig?.dataSources;
}, [element?.dataSources, element?.chartConfig?.dataSources]);
// 다중 데이터 소스 로딩
const loadMultipleDataSources = useCallback(async () => {
// dataSources는 element.dataSources 또는 chartConfig.dataSources에서 로드
const dataSources = element?.dataSources || element?.chartConfig?.dataSources;
if (!dataSources || dataSources.length === 0) {
console.log("⚠️ 데이터 소스가 없습니다.");
return;
}
console.log(`🔄 \${dataSources.length}개의 데이터 소스 로딩 시작...`);
setLoading(true);
setError(null);
try {
// 모든 데이터 소스를 병렬로 로딩
const results = await Promise.allSettled(
dataSources.map(async (source) => {
try {
console.log(`📡 데이터 소스 "\${source.name || source.id}" 로딩 중...`);
if (source.type === "api") {
return await loadRestApiData(source);
} else if (source.type === "database") {
return await loadDatabaseData(source);
}
return [];
} catch (err: any) {
console.error(`❌ 데이터 소스 "\${source.name || source.id}" 로딩 실패:`, err);
return [];
}
})
);
// 성공한 데이터만 병합
const allData: any[] = [];
results.forEach((result, index) => {
if (result.status === "fulfilled" && Array.isArray(result.value)) {
const sourceData = result.value.map((item: any) => ({
...item,
_source: dataSources[index].name || dataSources[index].id || `소스 \${index + 1}`,
}));
allData.push(...sourceData);
}
});
console.log(`✅ 총 \${allData.length}개의 데이터 로딩 완료`);
setData(allData);
setLastRefreshTime(new Date());
} catch (err: any) {
console.error("❌ 데이터 로딩 중 오류:", err);
setError(err.message);
} finally {
setLoading(false);
}
}, [element?.dataSources]);
// 수동 새로고침 핸들러
const handleManualRefresh = useCallback(() => {
console.log("🔄 수동 새로고침 버튼 클릭");
loadMultipleDataSources();
}, [loadMultipleDataSources]);
// REST API 데이터 로딩
const loadRestApiData = async (source: ChartDataSource): Promise<any[]> => {
if (!source.endpoint) {
throw new Error("API endpoint가 없습니다.");
}
const queryParams: Record<string, string> = {};
if (source.queryParams) {
source.queryParams.forEach((param) => {
if (param.key && param.value) {
queryParams[param.key] = param.value;
}
});
}
const headers: Record<string, string> = {};
if (source.headers) {
source.headers.forEach((header) => {
if (header.key && header.value) {
headers[header.key] = header.value;
}
});
}
const response = await fetch("/api/dashboards/fetch-external-api", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
url: source.endpoint,
method: source.method || "GET",
headers,
queryParams,
}),
});
if (!response.ok) {
throw new Error(`API 호출 실패: \${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || "API 호출 실패");
}
let apiData = result.data;
if (source.jsonPath) {
const pathParts = source.jsonPath.split(".");
for (const part of pathParts) {
apiData = apiData?.[part];
}
}
return Array.isArray(apiData) ? apiData : [apiData];
};
// Database 데이터 로딩
const loadDatabaseData = async (source: ChartDataSource): Promise<any[]> => {
if (!source.query) {
throw new Error("SQL 쿼리가 없습니다.");
}
const response = await fetch("/api/dashboards/query", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
connectionType: source.connectionType || "current",
externalConnectionId: source.externalConnectionId,
query: source.query,
}),
});
if (!response.ok) {
throw new Error(`데이터베이스 쿼리 실패: \${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || "쿼리 실패");
}
return result.data || [];
};
// 초기 로드
useEffect(() => {
if (dataSources && dataSources.length > 0) {
loadMultipleDataSources();
}
}, [dataSources, loadMultipleDataSources]);
// 자동 새로고침
useEffect(() => {
if (!dataSources || dataSources.length === 0) return;
const intervals = dataSources
.map((ds) => ds.refreshInterval)
.filter((interval): interval is number => typeof interval === "number" && interval > 0);
if (intervals.length === 0) return;
const minInterval = Math.min(...intervals);
console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`);
const intervalId = setInterval(() => {
console.log("🔄 자동 새로고침 실행");
loadMultipleDataSources();
}, minInterval * 1000);
return () => {
console.log("⏹️ 자동 새로고침 정리");
clearInterval(intervalId);
};
}, [dataSources, loadMultipleDataSources]);
const chartType = element?.subtype || "line";
const chartConfig = element?.chartConfig || {};
const renderChart = () => {
if (data.length === 0) {
return (
<div className="flex h-full items-center justify-center">
<p className="text-sm text-muted-foreground"> </p>
</div>
);
}
const xAxis = chartConfig.xAxis || Object.keys(data[0])[0];
const yAxis = chartConfig.yAxis || Object.keys(data[0])[1];
switch (chartType) {
case "line":
return (
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={xAxis} />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey={yAxis} stroke="#3b82f6" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
);
case "bar":
return (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={xAxis} />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey={yAxis} fill="#3b82f6" />
</BarChart>
</ResponsiveContainer>
);
case "pie":
return (
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={data}
dataKey={yAxis}
nameKey={xAxis}
cx="50%"
cy="50%"
outerRadius={80}
label
>
{data.map((entry, index) => (
<Cell key={`cell-\${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
);
default:
return (
<div className="flex h-full items-center justify-center">
<p className="text-sm text-muted-foreground">
: {chartType}
</p>
</div>
);
}
};
return (
<div className="flex h-full w-full flex-col bg-background">
<div className="flex items-center justify-between border-b p-4">
<div>
<h3 className="text-lg font-semibold">
{element?.customTitle || "차트 테스트 (다중 데이터 소스)"}
</h3>
<p className="text-xs text-muted-foreground">
{dataSources?.length || 0} {data.length}
{lastRefreshTime && (
<span className="ml-2">
{lastRefreshTime.toLocaleTimeString("ko-KR")}
</span>
)}
</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={handleManualRefresh}
disabled={loading}
className="h-8 gap-2 text-xs"
>
<RefreshCw className={`h-3 w-3 ${loading ? "animate-spin" : ""}`} />
</Button>
{loading && <Loader2 className="h-4 w-4 animate-spin" />}
</div>
</div>
<div className="flex-1 p-4">
{error ? (
<div className="flex h-full items-center justify-center">
<p className="text-sm text-destructive">{error}</p>
</div>
) : !(element?.dataSources || element?.chartConfig?.dataSources) || (element?.dataSources || element?.chartConfig?.dataSources)?.length === 0 ? (
<div className="flex h-full items-center justify-center">
<p className="text-sm text-muted-foreground">
</p>
</div>
) : (
renderChart()
)}
</div>
{data.length > 0 && (
<div className="border-t p-2 text-xs text-muted-foreground">
{data.length}
</div>
)}
</div>
);
}