196 lines
5.7 KiB
TypeScript
196 lines
5.7 KiB
TypeScript
|
|
'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 [];
|
|||
|
|
}
|
|||
|
|
}
|