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

196 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React from 'react';
import { DashboardElement, QueryResult } from '../types';
import { BarChartComponent } from './BarChartComponent';
import { PieChartComponent } from './PieChartComponent';
import { LineChartComponent } from './LineChartComponent';
interface ChartRendererProps {
element: DashboardElement;
data?: QueryResult;
width?: number;
height?: number;
}
/**
* 차트 렌더러 컴포넌트
* - 차트 타입에 따라 적절한 차트 컴포넌트를 렌더링
* - 데이터 변환 및 에러 처리
*/
export function ChartRenderer({ element, data, width = 250, height = 200 }: ChartRendererProps) {
// 데이터가 없거나 설정이 불완전한 경우
if (!data || !element.chartConfig?.xAxis || !element.chartConfig?.yAxis) {
return (
<div className="w-full h-full flex items-center justify-center text-gray-500 text-sm">
<div className="text-center">
<div className="text-2xl mb-2">📊</div>
<div> </div>
<div className="text-xs mt-1"> </div>
</div>
</div>
);
}
// 데이터 변환
const chartData = transformData(data, element.chartConfig);
// 에러가 있는 경우
if (chartData.length === 0) {
return (
<div className="w-full h-full flex items-center justify-center text-red-500 text-sm">
<div className="text-center">
<div className="text-2xl mb-2"></div>
<div> </div>
</div>
</div>
);
}
// 차트 공통 props
const chartProps = {
data: chartData,
config: element.chartConfig,
width: width - 20, // 패딩 고려
height: height - 60, // 헤더 높이 고려
};
// 차트 타입에 따른 렌더링
switch (element.subtype) {
case 'bar':
return <BarChartComponent {...chartProps} />;
case 'pie':
return <PieChartComponent {...chartProps} />;
case 'line':
return <LineChartComponent {...chartProps} />;
default:
return (
<div className="w-full h-full flex items-center justify-center text-gray-500 text-sm">
<div className="text-center">
<div className="text-2xl mb-2"></div>
<div> </div>
</div>
</div>
);
}
}
/**
* 쿼리 결과를 차트 데이터로 변환
*/
function transformData(queryResult: QueryResult, config: any) {
try {
const { xAxis, yAxis, groupBy, aggregation = 'sum' } = config;
if (!queryResult.rows || queryResult.rows.length === 0) {
return [];
}
// 그룹핑이 있는 경우
if (groupBy && groupBy !== xAxis) {
const grouped = queryResult.rows.reduce((acc, row) => {
const xValue = String(row[xAxis] || '');
const groupValue = String(row[groupBy] || '');
const yValue = Number(row[yAxis]) || 0;
if (!acc[xValue]) {
acc[xValue] = { [xAxis]: xValue };
}
if (!acc[xValue][groupValue]) {
acc[xValue][groupValue] = 0;
}
// 집계 함수 적용
switch (aggregation) {
case 'sum':
acc[xValue][groupValue] += yValue;
break;
case 'avg':
// 평균 계산을 위해 임시로 배열 저장
if (!acc[xValue][`${groupValue}_values`]) {
acc[xValue][`${groupValue}_values`] = [];
}
acc[xValue][`${groupValue}_values`].push(yValue);
break;
case 'count':
acc[xValue][groupValue] += 1;
break;
case 'max':
acc[xValue][groupValue] = Math.max(acc[xValue][groupValue], yValue);
break;
case 'min':
acc[xValue][groupValue] = Math.min(acc[xValue][groupValue], yValue);
break;
}
return acc;
}, {} as any);
// 평균 계산 후처리
if (aggregation === 'avg') {
Object.keys(grouped).forEach(xValue => {
Object.keys(grouped[xValue]).forEach(key => {
if (key.endsWith('_values')) {
const baseKey = key.replace('_values', '');
const values = grouped[xValue][key];
grouped[xValue][baseKey] = values.reduce((sum: number, val: number) => sum + val, 0) / values.length;
delete grouped[xValue][key];
}
});
});
}
return Object.values(grouped);
}
// 단순 변환 (그룹핑 없음)
const dataMap = new Map();
queryResult.rows.forEach(row => {
const xValue = String(row[xAxis] || '');
const yValue = Number(row[yAxis]) || 0;
if (!dataMap.has(xValue)) {
dataMap.set(xValue, { [xAxis]: xValue, [yAxis]: 0, count: 0 });
}
const existing = dataMap.get(xValue);
switch (aggregation) {
case 'sum':
existing[yAxis] += yValue;
break;
case 'avg':
existing[yAxis] += yValue;
existing.count += 1;
break;
case 'count':
existing[yAxis] += 1;
break;
case 'max':
existing[yAxis] = Math.max(existing[yAxis], yValue);
break;
case 'min':
existing[yAxis] = existing[yAxis] === 0 ? yValue : Math.min(existing[yAxis], yValue);
break;
}
});
// 평균 계산 후처리
if (aggregation === 'avg') {
dataMap.forEach(item => {
if (item.count > 0) {
item[yAxis] = item[yAxis] / item.count;
}
delete item.count;
});
}
return Array.from(dataMap.values()).slice(0, 50); // 최대 50개 데이터포인트
} catch (error) {
console.error('데이터 변환 오류:', error);
return [];
}
}