331 lines
12 KiB
TypeScript
331 lines
12 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState, useEffect } from "react";
|
||
import { ChartConfig, ChartDataSource } from "./types";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Switch } from "@/components/ui/switch";
|
||
import { Trash2 } from "lucide-react";
|
||
|
||
interface MultiChartConfigPanelProps {
|
||
config: ChartConfig;
|
||
dataSources: ChartDataSource[];
|
||
testResults: Map<string, { columns: string[]; rows: Record<string, unknown>[] }>; // 각 데이터 소스의 테스트 결과
|
||
onConfigChange: (config: ChartConfig) => void;
|
||
}
|
||
|
||
export function MultiChartConfigPanel({
|
||
config,
|
||
dataSources,
|
||
testResults,
|
||
onConfigChange,
|
||
}: MultiChartConfigPanelProps) {
|
||
const [chartType, setChartType] = useState<string>(config.chartType || "line");
|
||
const [mergeMode, setMergeMode] = useState<boolean>(config.mergeMode || false);
|
||
const [dataSourceConfigs, setDataSourceConfigs] = useState<
|
||
Array<{
|
||
dataSourceId: string;
|
||
xAxis: string;
|
||
yAxis: string[];
|
||
label?: string;
|
||
}>
|
||
>(config.dataSourceConfigs || []);
|
||
|
||
// 데이터 소스별 사용 가능한 컬럼
|
||
const getColumnsForDataSource = (dataSourceId: string): string[] => {
|
||
const result = testResults.get(dataSourceId);
|
||
return result?.columns || [];
|
||
};
|
||
|
||
// 데이터 소스별 숫자 컬럼
|
||
const getNumericColumnsForDataSource = (dataSourceId: string): string[] => {
|
||
const result = testResults.get(dataSourceId);
|
||
if (!result || !result.rows || result.rows.length === 0) return [];
|
||
|
||
const firstRow = result.rows[0];
|
||
return Object.keys(firstRow).filter((key) => {
|
||
const value = firstRow[key];
|
||
return typeof value === "number" || !isNaN(Number(value));
|
||
});
|
||
};
|
||
|
||
// 차트 타입 변경
|
||
const handleChartTypeChange = (type: string) => {
|
||
setChartType(type);
|
||
onConfigChange({
|
||
...config,
|
||
chartType: type,
|
||
mergeMode,
|
||
dataSourceConfigs,
|
||
});
|
||
};
|
||
|
||
// 병합 모드 변경
|
||
const handleMergeModeChange = (checked: boolean) => {
|
||
setMergeMode(checked);
|
||
onConfigChange({
|
||
...config,
|
||
chartType,
|
||
mergeMode: checked,
|
||
dataSourceConfigs,
|
||
});
|
||
};
|
||
|
||
// 데이터 소스 설정 추가
|
||
const handleAddDataSourceConfig = (dataSourceId: string) => {
|
||
const columns = getColumnsForDataSource(dataSourceId);
|
||
const numericColumns = getNumericColumnsForDataSource(dataSourceId);
|
||
|
||
const newConfig = {
|
||
dataSourceId,
|
||
xAxis: columns[0] || "",
|
||
yAxis: numericColumns.length > 0 ? [numericColumns[0]] : [],
|
||
label: dataSources.find((ds) => ds.id === dataSourceId)?.name || "",
|
||
};
|
||
|
||
const updated = [...dataSourceConfigs, newConfig];
|
||
setDataSourceConfigs(updated);
|
||
onConfigChange({
|
||
...config,
|
||
chartType,
|
||
mergeMode,
|
||
dataSourceConfigs: updated,
|
||
});
|
||
};
|
||
|
||
// 데이터 소스 설정 삭제
|
||
const handleRemoveDataSourceConfig = (dataSourceId: string) => {
|
||
const updated = dataSourceConfigs.filter((c) => c.dataSourceId !== dataSourceId);
|
||
setDataSourceConfigs(updated);
|
||
onConfigChange({
|
||
...config,
|
||
chartType,
|
||
mergeMode,
|
||
dataSourceConfigs: updated,
|
||
});
|
||
};
|
||
|
||
// X축 변경
|
||
const handleXAxisChange = (dataSourceId: string, xAxis: string) => {
|
||
const updated = dataSourceConfigs.map((c) => (c.dataSourceId === dataSourceId ? { ...c, xAxis } : c));
|
||
setDataSourceConfigs(updated);
|
||
onConfigChange({
|
||
...config,
|
||
chartType,
|
||
mergeMode,
|
||
dataSourceConfigs: updated,
|
||
});
|
||
};
|
||
|
||
// Y축 변경
|
||
const handleYAxisChange = (dataSourceId: string, yAxis: string) => {
|
||
const updated = dataSourceConfigs.map((c) => (c.dataSourceId === dataSourceId ? { ...c, yAxis: [yAxis] } : c));
|
||
setDataSourceConfigs(updated);
|
||
onConfigChange({
|
||
...config,
|
||
chartType,
|
||
mergeMode,
|
||
dataSourceConfigs: updated,
|
||
});
|
||
};
|
||
|
||
// 🆕 개별 차트 타입 변경
|
||
const handleIndividualChartTypeChange = (dataSourceId: string, chartType: "bar" | "line" | "area") => {
|
||
const updated = dataSourceConfigs.map((c) => (c.dataSourceId === dataSourceId ? { ...c, chartType } : c));
|
||
setDataSourceConfigs(updated);
|
||
onConfigChange({
|
||
...config,
|
||
chartType: "mixed", // 혼합 모드로 설정
|
||
mergeMode,
|
||
dataSourceConfigs: updated,
|
||
});
|
||
};
|
||
|
||
// 설정되지 않은 데이터 소스 (테스트 완료된 것만)
|
||
const availableDataSources = dataSources.filter(
|
||
(ds) => testResults.has(ds.id!) && !dataSourceConfigs.some((c) => c.dataSourceId === ds.id),
|
||
);
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
{/* 차트 타입 선택 */}
|
||
<div className="space-y-2">
|
||
<Label className="text-xs font-medium">차트 타입</Label>
|
||
<Select value={chartType} onValueChange={handleChartTypeChange}>
|
||
<SelectTrigger className="h-8 text-xs">
|
||
<SelectValue placeholder="차트 타입 선택" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="line">📈 라인 차트</SelectItem>
|
||
<SelectItem value="bar">📊 바 차트</SelectItem>
|
||
<SelectItem value="horizontal-bar">📊 수평 바 차트</SelectItem>
|
||
<SelectItem value="stacked-bar">📊 누적 바 차트</SelectItem>
|
||
<SelectItem value="area">📉 영역 차트</SelectItem>
|
||
<SelectItem value="pie">🥧 파이 차트</SelectItem>
|
||
<SelectItem value="donut">🍩 도넛 차트</SelectItem>
|
||
<SelectItem value="combo">🎨 콤보 차트</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
{/* 데이터 병합 모드 */}
|
||
{dataSourceConfigs.length > 1 && (
|
||
<div className="bg-muted/50 flex items-center justify-between rounded-lg border p-3">
|
||
<div className="space-y-0.5">
|
||
<Label className="text-xs font-medium">데이터 병합 모드</Label>
|
||
<p className="text-muted-foreground text-[10px]">여러 데이터 소스를 하나의 라인/바로 합쳐서 표시</p>
|
||
</div>
|
||
<Switch checked={mergeMode} onCheckedChange={handleMergeModeChange} aria-label="데이터 병합 모드" />
|
||
</div>
|
||
)}
|
||
|
||
{/* 데이터 소스별 설정 */}
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<Label className="text-xs font-medium">데이터 소스별 축 설정</Label>
|
||
{availableDataSources.length > 0 && (
|
||
<Select onValueChange={handleAddDataSourceConfig}>
|
||
<SelectTrigger className="h-7 w-32 text-xs">
|
||
<SelectValue placeholder="추가" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{availableDataSources.map((ds) => (
|
||
<SelectItem key={ds.id} value={ds.id!} className="text-xs">
|
||
{ds.name || ds.id}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
)}
|
||
</div>
|
||
|
||
{dataSourceConfigs.length === 0 ? (
|
||
<div className="rounded-lg border border-dashed p-4 text-center">
|
||
<p className="text-muted-foreground text-xs">
|
||
데이터 소스를 추가하고 API 테스트를 실행한 후<br />위 드롭다운에서 차트에 표시할 데이터를 선택하세요
|
||
</p>
|
||
</div>
|
||
) : (
|
||
dataSourceConfigs.map((dsConfig) => {
|
||
const dataSource = dataSources.find((ds) => ds.id === dsConfig.dataSourceId);
|
||
const columns = getColumnsForDataSource(dsConfig.dataSourceId);
|
||
const numericColumns = getNumericColumnsForDataSource(dsConfig.dataSourceId);
|
||
|
||
return (
|
||
<div key={dsConfig.dataSourceId} className="bg-muted/50 space-y-3 rounded-lg border p-3">
|
||
{/* 헤더 */}
|
||
<div className="flex items-center justify-between">
|
||
<h5 className="text-xs font-semibold">{dataSource?.name || dsConfig.dataSourceId}</h5>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleRemoveDataSourceConfig(dsConfig.dataSourceId)}
|
||
className="h-6 w-6 p-0"
|
||
>
|
||
<Trash2 className="h-3 w-3" />
|
||
</Button>
|
||
</div>
|
||
|
||
{/* X축 */}
|
||
<div className="space-y-1.5">
|
||
<Label className="text-xs">X축 (카테고리/시간)</Label>
|
||
<Select
|
||
value={dsConfig.xAxis}
|
||
onValueChange={(value) => handleXAxisChange(dsConfig.dataSourceId, value)}
|
||
>
|
||
<SelectTrigger className="h-8 text-xs">
|
||
<SelectValue placeholder="X축 선택" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{columns.map((col) => (
|
||
<SelectItem key={col} value={col} className="text-xs">
|
||
{col}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
{/* Y축 */}
|
||
<div className="space-y-1.5">
|
||
<Label className="text-xs">Y축 (값)</Label>
|
||
<Select
|
||
value={dsConfig.yAxis[0] || ""}
|
||
onValueChange={(value) => handleYAxisChange(dsConfig.dataSourceId, value)}
|
||
>
|
||
<SelectTrigger className="h-8 text-xs">
|
||
<SelectValue placeholder="Y축 선택" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{numericColumns.map((col) => (
|
||
<SelectItem key={col} value={col} className="text-xs">
|
||
{col}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
{/* 🆕 개별 차트 타입 (병합 모드가 아닐 때만) */}
|
||
{!mergeMode && (
|
||
<div className="space-y-1.5">
|
||
<Label className="text-xs">차트 타입</Label>
|
||
<Select
|
||
value={dsConfig.chartType || "line"}
|
||
onValueChange={(value) =>
|
||
handleIndividualChartTypeChange(dsConfig.dataSourceId, value as "bar" | "line" | "area")
|
||
}
|
||
>
|
||
<SelectTrigger className="h-8 text-xs">
|
||
<SelectValue placeholder="차트 타입" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="bar" className="text-xs">
|
||
📊 바 차트
|
||
</SelectItem>
|
||
<SelectItem value="line" className="text-xs">
|
||
📈 라인 차트
|
||
</SelectItem>
|
||
<SelectItem value="area" className="text-xs">
|
||
📉 영역 차트
|
||
</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})
|
||
)}
|
||
</div>
|
||
|
||
{/* 안내 메시지 */}
|
||
{dataSourceConfigs.length > 0 && (
|
||
<div className="rounded-lg bg-primary/10 p-3">
|
||
<p className="text-xs text-primary">
|
||
{mergeMode ? (
|
||
<>
|
||
🔗 {dataSourceConfigs.length}개의 데이터 소스가 하나의 라인/바로 병합되어 표시됩니다.
|
||
<br />
|
||
<span className="text-[10px]">
|
||
⚠️ 중요: 첫 번째 데이터 소스의 X축/Y축 컬럼명이 기준이 됩니다.
|
||
<br />
|
||
다른 데이터 소스에 동일한 컬럼명이 없으면 해당 데이터는 표시되지 않습니다.
|
||
<br />
|
||
💡 컬럼명이 다르면 "컬럼 매핑" 기능을 사용하여 통일하세요.
|
||
</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
💡 {dataSourceConfigs.length}개의 데이터 소스가 하나의 차트에 표시됩니다.
|
||
<br />각 데이터 소스마다 다른 차트 타입(바/라인/영역)을 선택할 수 있습니다.
|
||
</>
|
||
)}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|