테스트 위젯 원본 승격 전 세이브
This commit is contained in:
@@ -908,7 +908,7 @@ export function CanvasElement({
|
||||
<ListTestWidget element={element} />
|
||||
</div>
|
||||
) : element.type === "widget" && element.subtype === "custom-metric-test" ? (
|
||||
// 🧪 테스트용 커스텀 메트릭 위젯 (다중 데이터 소스)
|
||||
// 🧪 통계 카드 (다중 데이터 소스)
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
<CustomMetricTestWidget element={element} />
|
||||
</div>
|
||||
|
||||
@@ -181,15 +181,15 @@ export function DashboardTopMenu({
|
||||
<SelectValue placeholder="위젯 추가" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[99999]">
|
||||
<SelectGroup>
|
||||
<SelectLabel>🧪 테스트 위젯 (다중 데이터 소스)</SelectLabel>
|
||||
<SelectItem value="map-test-v2">🧪 지도 테스트 V2</SelectItem>
|
||||
<SelectItem value="chart-test">🧪 차트 테스트</SelectItem>
|
||||
<SelectItem value="list-test">🧪 리스트 테스트</SelectItem>
|
||||
<SelectItem value="custom-metric-test">🧪 커스텀 메트릭 테스트</SelectItem>
|
||||
<SelectItem value="status-summary-test">🧪 상태 요약 테스트</SelectItem>
|
||||
<SelectItem value="risk-alert-test">🧪 리스크/알림 테스트</SelectItem>
|
||||
</SelectGroup>
|
||||
<SelectGroup>
|
||||
<SelectLabel>🧪 테스트 위젯 (다중 데이터 소스)</SelectLabel>
|
||||
<SelectItem value="map-test-v2">🧪 지도 테스트 V2</SelectItem>
|
||||
<SelectItem value="chart-test">🧪 차트 테스트</SelectItem>
|
||||
<SelectItem value="list-test">🧪 리스트 테스트</SelectItem>
|
||||
<SelectItem value="custom-metric-test">통계 카드</SelectItem>
|
||||
{/* <SelectItem value="status-summary-test">🧪 상태 요약 테스트</SelectItem> */}
|
||||
<SelectItem value="risk-alert-test">🧪 리스크/알림 테스트</SelectItem>
|
||||
</SelectGroup>
|
||||
<SelectGroup>
|
||||
<SelectLabel>데이터 위젯</SelectLabel>
|
||||
<SelectItem value="list">리스트 위젯</SelectItem>
|
||||
@@ -197,7 +197,7 @@ export function DashboardTopMenu({
|
||||
<SelectItem value="yard-management-3d">야드 관리 3D</SelectItem>
|
||||
{/* <SelectItem value="transport-stats">커스텀 통계 카드</SelectItem> */}
|
||||
<SelectItem value="map-summary">커스텀 지도 카드</SelectItem>
|
||||
<SelectItem value="map-test">🧪 지도 테스트 (REST API)</SelectItem>
|
||||
{/* <SelectItem value="map-test">🧪 지도 테스트 (REST API)</SelectItem> */}
|
||||
{/* <SelectItem value="status-summary">커스텀 상태 카드</SelectItem> */}
|
||||
</SelectGroup>
|
||||
<SelectGroup>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { QueryEditor } from "./QueryEditor";
|
||||
import { ChartConfigPanel } from "./ChartConfigPanel";
|
||||
import { VehicleMapConfigPanel } from "./VehicleMapConfigPanel";
|
||||
import { MapTestConfigPanel } from "./MapTestConfigPanel";
|
||||
import { MultiChartConfigPanel } from "./MultiChartConfigPanel";
|
||||
import { DatabaseConfig } from "./data-sources/DatabaseConfig";
|
||||
import { ApiConfig } from "./data-sources/ApiConfig";
|
||||
import MultiDataSourceConfig from "./data-sources/MultiDataSourceConfig";
|
||||
@@ -41,16 +42,41 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
||||
const [customTitle, setCustomTitle] = useState<string>("");
|
||||
const [showHeader, setShowHeader] = useState<boolean>(true);
|
||||
|
||||
// 멀티 데이터 소스의 테스트 결과 저장 (ChartTestWidget용)
|
||||
const [testResults, setTestResults] = useState<Map<string, { columns: string[]; rows: Record<string, unknown>[] }>>(
|
||||
new Map(),
|
||||
);
|
||||
|
||||
// 사이드바가 열릴 때 초기화
|
||||
useEffect(() => {
|
||||
if (isOpen && element) {
|
||||
console.log("🔄 ElementConfigSidebar 초기화 - element.id:", element.id);
|
||||
console.log("🔄 element.dataSources:", element.dataSources);
|
||||
console.log("🔄 element.chartConfig?.dataSources:", element.chartConfig?.dataSources);
|
||||
|
||||
setDataSource(element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 });
|
||||
|
||||
// dataSources는 element.dataSources 또는 chartConfig.dataSources에서 로드
|
||||
setDataSources(element.dataSources || element.chartConfig?.dataSources || []);
|
||||
// ⚠️ 중요: 없으면 반드시 빈 배열로 초기화
|
||||
const initialDataSources = element.dataSources || element.chartConfig?.dataSources || [];
|
||||
console.log("🔄 초기화된 dataSources:", initialDataSources);
|
||||
setDataSources(initialDataSources);
|
||||
|
||||
setChartConfig(element.chartConfig || {});
|
||||
setQueryResult(null);
|
||||
setTestResults(new Map()); // 테스트 결과도 초기화
|
||||
setCustomTitle(element.customTitle || "");
|
||||
setShowHeader(element.showHeader !== false);
|
||||
} else if (!isOpen) {
|
||||
// 사이드바가 닫힐 때 모든 상태 초기화
|
||||
console.log("🧹 ElementConfigSidebar 닫힘 - 상태 초기화");
|
||||
setDataSource({ type: "database", connectionType: "current", refreshInterval: 0 });
|
||||
setDataSources([]);
|
||||
setChartConfig({});
|
||||
setQueryResult(null);
|
||||
setTestResults(new Map());
|
||||
setCustomTitle("");
|
||||
setShowHeader(true);
|
||||
}
|
||||
}, [isOpen, element]);
|
||||
|
||||
@@ -127,10 +153,10 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
||||
console.log("🔧 적용 버튼 클릭 - chartConfig:", chartConfig);
|
||||
|
||||
// 다중 데이터 소스 위젯 체크
|
||||
const isMultiDS =
|
||||
element.subtype === "map-test-v2" ||
|
||||
element.subtype === "chart-test" ||
|
||||
element.subtype === "list-test" ||
|
||||
const isMultiDS =
|
||||
element.subtype === "map-test-v2" ||
|
||||
element.subtype === "chart-test" ||
|
||||
element.subtype === "list-test" ||
|
||||
element.subtype === "custom-metric-test" ||
|
||||
element.subtype === "risk-alert-test";
|
||||
|
||||
@@ -227,10 +253,10 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
||||
(element.subtype === "clock" || element.subtype === "calendar" || isSelfContainedWidget);
|
||||
|
||||
// 다중 데이터 소스 테스트 위젯
|
||||
const isMultiDataSourceWidget =
|
||||
element.subtype === "map-test-v2" ||
|
||||
element.subtype === "chart-test" ||
|
||||
element.subtype === "list-test" ||
|
||||
const isMultiDataSourceWidget =
|
||||
element.subtype === "map-test-v2" ||
|
||||
element.subtype === "chart-test" ||
|
||||
element.subtype === "list-test" ||
|
||||
element.subtype === "custom-metric-test" ||
|
||||
element.subtype === "status-summary-test" ||
|
||||
element.subtype === "risk-alert-test";
|
||||
@@ -321,7 +347,27 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
||||
{isMultiDataSourceWidget && (
|
||||
<>
|
||||
<div className="rounded-lg bg-white p-3 shadow-sm">
|
||||
<MultiDataSourceConfig dataSources={dataSources} onChange={setDataSources} />
|
||||
<MultiDataSourceConfig
|
||||
dataSources={dataSources}
|
||||
onChange={setDataSources}
|
||||
onTestResult={(result, dataSourceId) => {
|
||||
// API 테스트 결과를 queryResult로 설정 (차트 설정용)
|
||||
setQueryResult({
|
||||
...result,
|
||||
totalRows: result.rows.length,
|
||||
executionTime: 0,
|
||||
});
|
||||
console.log("📊 API 테스트 결과 수신:", result, "데이터 소스 ID:", dataSourceId);
|
||||
|
||||
// ChartTestWidget용: 각 데이터 소스의 테스트 결과 저장
|
||||
setTestResults((prev) => {
|
||||
const updated = new Map(prev);
|
||||
updated.set(dataSourceId, result);
|
||||
console.log("📊 테스트 결과 저장:", dataSourceId, result);
|
||||
return updated;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 지도 테스트 V2: 타일맵 URL 설정 */}
|
||||
@@ -354,6 +400,40 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
||||
</details>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 차트 테스트: 차트 설정 */}
|
||||
{element.subtype === "chart-test" && (
|
||||
<div className="rounded-lg bg-white shadow-sm">
|
||||
<details className="group" open>
|
||||
<summary className="flex cursor-pointer items-center justify-between p-3 hover:bg-gray-50">
|
||||
<div>
|
||||
<div className="text-xs font-semibold tracking-wide text-gray-500 uppercase">차트 설정</div>
|
||||
<div className="text-muted-foreground mt-0.5 text-[10px]">
|
||||
{testResults.size > 0
|
||||
? `${testResults.size}개 데이터 소스 • X축, Y축, 차트 타입 설정`
|
||||
: "먼저 데이터 소스를 추가하고 API 테스트를 실행하세요"}
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
className="h-4 w-4 transition-transform group-open:rotate-180"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</summary>
|
||||
<div className="border-t p-3">
|
||||
<MultiChartConfigPanel
|
||||
config={chartConfig}
|
||||
dataSources={dataSources}
|
||||
testResults={testResults}
|
||||
onConfigChange={handleChartConfigChange}
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
327
frontend/components/admin/dashboard/MultiChartConfigPanel.tsx
Normal file
327
frontend/components/admin/dashboard/MultiChartConfigPanel.tsx
Normal file
@@ -0,0 +1,327 @@
|
||||
"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="area">영역 차트</SelectItem>
|
||||
<SelectItem value="pie">파이 차트</SelectItem>
|
||||
<SelectItem value="donut">도넛 차트</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-blue-50 p-3">
|
||||
<p className="text-xs text-blue-900">
|
||||
{mergeMode ? (
|
||||
<>
|
||||
🔗 {dataSourceConfigs.length}개의 데이터 소스가 하나의 라인/바로 병합되어 표시됩니다.
|
||||
<br />
|
||||
<span className="text-[10px]">
|
||||
⚠️ 중요: 첫 번째 데이터 소스의 X축/Y축 컬럼명이 기준이 됩니다.
|
||||
<br />
|
||||
다른 데이터 소스에 동일한 컬럼명이 없으면 해당 데이터는 표시되지 않습니다.
|
||||
<br />
|
||||
💡 컬럼명이 다르면 "컬럼 매핑" 기능을 사용하여 통일하세요.
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
💡 {dataSourceConfigs.length}개의 데이터 소스가 하나의 차트에 표시됩니다.
|
||||
<br />각 데이터 소스마다 다른 차트 타입(바/라인/영역)을 선택할 수 있습니다.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -557,6 +557,55 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 지도 색상 설정 (MapTestWidgetV2 전용) */}
|
||||
<div className="space-y-3 rounded-lg border bg-muted/30 p-3">
|
||||
<h5 className="text-xs font-semibold">🎨 지도 색상 선택</h5>
|
||||
|
||||
{/* 색상 팔레트 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">색상</Label>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{[
|
||||
{ name: "파랑", marker: "#3b82f6", polygon: "#3b82f6" },
|
||||
{ name: "빨강", marker: "#ef4444", polygon: "#ef4444" },
|
||||
{ name: "초록", marker: "#10b981", polygon: "#10b981" },
|
||||
{ name: "노랑", marker: "#f59e0b", polygon: "#f59e0b" },
|
||||
{ name: "보라", marker: "#8b5cf6", polygon: "#8b5cf6" },
|
||||
{ name: "주황", marker: "#f97316", polygon: "#f97316" },
|
||||
{ name: "청록", marker: "#06b6d4", polygon: "#06b6d4" },
|
||||
{ name: "분홍", marker: "#ec4899", polygon: "#ec4899" },
|
||||
].map((color) => {
|
||||
const isSelected = dataSource.markerColor === color.marker;
|
||||
return (
|
||||
<button
|
||||
key={color.name}
|
||||
type="button"
|
||||
onClick={() => onChange({
|
||||
markerColor: color.marker,
|
||||
polygonColor: color.polygon,
|
||||
polygonOpacity: 0.5,
|
||||
})}
|
||||
className={`flex h-16 flex-col items-center justify-center gap-1 rounded-md border-2 transition-all hover:scale-105 ${
|
||||
isSelected
|
||||
? "border-primary bg-primary/10 shadow-md"
|
||||
: "border-border bg-background hover:border-primary/50"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="h-6 w-6 rounded-full border-2 border-white shadow-sm"
|
||||
style={{ backgroundColor: color.marker }}
|
||||
/>
|
||||
<span className="text-[10px] font-medium">{color.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
선택한 색상이 마커와 폴리곤에 모두 적용됩니다
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 테스트 버튼 */}
|
||||
<div className="space-y-2 border-t pt-4">
|
||||
<Button
|
||||
@@ -745,6 +794,103 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 컬럼 매핑 (API 테스트 성공 후에만 표시) */}
|
||||
{testResult?.success && availableColumns.length > 0 && (
|
||||
<div className="space-y-3 rounded-lg border bg-muted/30 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h5 className="text-xs font-semibold">🔄 컬럼 매핑 (선택사항)</h5>
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">
|
||||
다른 데이터 소스와 통합할 때 컬럼명을 통일할 수 있습니다
|
||||
</p>
|
||||
</div>
|
||||
{dataSource.columnMapping && Object.keys(dataSource.columnMapping).length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onChange({ columnMapping: {} })}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 매핑 목록 */}
|
||||
{dataSource.columnMapping && Object.keys(dataSource.columnMapping).length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{Object.entries(dataSource.columnMapping).map(([original, mapped]) => (
|
||||
<div key={original} className="flex items-center gap-2">
|
||||
{/* 원본 컬럼 (읽기 전용) */}
|
||||
<Input
|
||||
value={original}
|
||||
disabled
|
||||
className="h-8 flex-1 text-xs bg-muted"
|
||||
/>
|
||||
|
||||
{/* 화살표 */}
|
||||
<span className="text-muted-foreground text-xs">→</span>
|
||||
|
||||
{/* 표시 이름 (편집 가능) */}
|
||||
<Input
|
||||
value={mapped}
|
||||
onChange={(e) => {
|
||||
const newMapping = { ...dataSource.columnMapping };
|
||||
newMapping[original] = e.target.value;
|
||||
onChange({ columnMapping: newMapping });
|
||||
}}
|
||||
placeholder="표시 이름"
|
||||
className="h-8 flex-1 text-xs"
|
||||
/>
|
||||
|
||||
{/* 삭제 버튼 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newMapping = { ...dataSource.columnMapping };
|
||||
delete newMapping[original];
|
||||
onChange({ columnMapping: newMapping });
|
||||
}}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<XCircle className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 매핑 추가 */}
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={(col) => {
|
||||
const newMapping = { ...dataSource.columnMapping } || {};
|
||||
newMapping[col] = col; // 기본값은 원본과 동일
|
||||
onChange({ columnMapping: newMapping });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="컬럼 선택하여 매핑 추가" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableColumns
|
||||
.filter(col => !dataSource.columnMapping || !dataSource.columnMapping[col])
|
||||
.map(col => (
|
||||
<SelectItem key={col} value={col} className="text-xs">
|
||||
{col}
|
||||
</SelectItem>
|
||||
))
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
💡 매핑하지 않은 컬럼은 원본 이름 그대로 사용됩니다
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,11 +20,13 @@ import MultiDatabaseConfig from "./MultiDatabaseConfig";
|
||||
interface MultiDataSourceConfigProps {
|
||||
dataSources: ChartDataSource[];
|
||||
onChange: (dataSources: ChartDataSource[]) => void;
|
||||
onTestResult?: (result: { columns: string[]; rows: any[] }, dataSourceId: string) => void;
|
||||
}
|
||||
|
||||
export default function MultiDataSourceConfig({
|
||||
dataSources = [],
|
||||
onChange,
|
||||
onTestResult,
|
||||
}: MultiDataSourceConfigProps) {
|
||||
const [activeTab, setActiveTab] = useState<string>(
|
||||
dataSources.length > 0 ? dataSources[0].id || "0" : "new"
|
||||
@@ -258,12 +260,24 @@ export default function MultiDataSourceConfig({
|
||||
onTestResult={(data) => {
|
||||
setPreviewData(data);
|
||||
setShowPreview(true);
|
||||
// 부모로 테스트 결과 전달 (차트 설정용)
|
||||
if (onTestResult && data.length > 0 && ds.id) {
|
||||
const columns = Object.keys(data[0]);
|
||||
onTestResult({ columns, rows: data }, ds.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<MultiDatabaseConfig
|
||||
dataSource={ds}
|
||||
onChange={(updates) => handleUpdateDataSource(ds.id!, updates)}
|
||||
onTestResult={(data) => {
|
||||
// 부모로 테스트 결과 전달 (차트 설정용)
|
||||
if (onTestResult && data.length > 0 && ds.id) {
|
||||
const columns = Object.keys(data[0]);
|
||||
onTestResult({ columns, rows: data }, ds.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Loader2, CheckCircle, XCircle } from "lucide-react";
|
||||
interface MultiDatabaseConfigProps {
|
||||
dataSource: ChartDataSource;
|
||||
onChange: (updates: Partial<ChartDataSource>) => void;
|
||||
onTestResult?: (data: any[]) => void;
|
||||
}
|
||||
|
||||
interface ExternalConnection {
|
||||
@@ -21,7 +22,7 @@ interface ExternalConnection {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export default function MultiDatabaseConfig({ dataSource, onChange }: MultiDatabaseConfigProps) {
|
||||
export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult }: MultiDatabaseConfigProps) {
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; message: string; rowCount?: number } | null>(null);
|
||||
const [externalConnections, setExternalConnections] = useState<ExternalConnection[]>([]);
|
||||
@@ -122,6 +123,11 @@ export default function MultiDatabaseConfig({ dataSource, onChange }: MultiDatab
|
||||
message: "쿼리 실행 성공",
|
||||
rowCount,
|
||||
});
|
||||
|
||||
// 부모로 테스트 결과 전달 (차트 설정용)
|
||||
if (onTestResult && rows && rows.length > 0) {
|
||||
onTestResult(rows);
|
||||
}
|
||||
} else {
|
||||
setTestResult({ success: false, message: result.message || "쿼리 실행 실패" });
|
||||
}
|
||||
@@ -166,6 +172,11 @@ export default function MultiDatabaseConfig({ dataSource, onChange }: MultiDatab
|
||||
message: "쿼리 실행 성공",
|
||||
rowCount: result.rowCount || 0,
|
||||
});
|
||||
|
||||
// 부모로 테스트 결과 전달 (차트 설정용)
|
||||
if (onTestResult && result.rows && result.rows.length > 0) {
|
||||
onTestResult(result.rows);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
setTestResult({ success: false, message: error.message || "네트워크 오류" });
|
||||
@@ -240,9 +251,61 @@ export default function MultiDatabaseConfig({ dataSource, onChange }: MultiDatab
|
||||
|
||||
{/* SQL 쿼리 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`query-\${dataSource.id}`} className="text-xs">
|
||||
SQL 쿼리 *
|
||||
</Label>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor={`query-\${dataSource.id}`} className="text-xs">
|
||||
SQL 쿼리 *
|
||||
</Label>
|
||||
<Select onValueChange={(value) => {
|
||||
const samples = {
|
||||
users: `SELECT
|
||||
dept_name as 부서명,
|
||||
COUNT(*) as 회원수
|
||||
FROM user_info
|
||||
WHERE dept_name IS NOT NULL
|
||||
GROUP BY dept_name
|
||||
ORDER BY 회원수 DESC`,
|
||||
dept: `SELECT
|
||||
dept_code as 부서코드,
|
||||
dept_name as 부서명,
|
||||
location_name as 위치,
|
||||
TO_CHAR(regdate, 'YYYY-MM-DD') as 등록일
|
||||
FROM dept_info
|
||||
ORDER BY dept_code`,
|
||||
usersByDate: `SELECT
|
||||
DATE_TRUNC('month', regdate)::date as 월,
|
||||
COUNT(*) as 신규사용자수
|
||||
FROM user_info
|
||||
WHERE regdate >= CURRENT_DATE - INTERVAL '12 months'
|
||||
GROUP BY DATE_TRUNC('month', regdate)
|
||||
ORDER BY 월`,
|
||||
usersByPosition: `SELECT
|
||||
position_name as 직급,
|
||||
COUNT(*) as 인원수
|
||||
FROM user_info
|
||||
WHERE position_name IS NOT NULL
|
||||
GROUP BY position_name
|
||||
ORDER BY 인원수 DESC`,
|
||||
deptHierarchy: `SELECT
|
||||
COALESCE(parent_dept_code, '최상위') as 상위부서코드,
|
||||
COUNT(*) as 하위부서수
|
||||
FROM dept_info
|
||||
GROUP BY parent_dept_code
|
||||
ORDER BY 하위부서수 DESC`,
|
||||
};
|
||||
onChange({ query: samples[value as keyof typeof samples] || "" });
|
||||
}}>
|
||||
<SelectTrigger className="h-7 w-32 text-xs">
|
||||
<SelectValue placeholder="샘플 쿼리" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="users" className="text-xs">부서별 회원수</SelectItem>
|
||||
<SelectItem value="dept" className="text-xs">부서 목록</SelectItem>
|
||||
<SelectItem value="usersByDate" className="text-xs">월별 신규사용자</SelectItem>
|
||||
<SelectItem value="usersByPosition" className="text-xs">직급별 인원수</SelectItem>
|
||||
<SelectItem value="deptHierarchy" className="text-xs">부서 계층구조</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Textarea
|
||||
id={`query-\${dataSource.id}`}
|
||||
value={dataSource.query || ""}
|
||||
@@ -251,7 +314,7 @@ export default function MultiDatabaseConfig({ dataSource, onChange }: MultiDatab
|
||||
className="min-h-[120px] font-mono text-xs"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
SELECT 쿼리만 허용됩니다
|
||||
SELECT 쿼리만 허용됩니다. 샘플 쿼리를 선택하여 빠르게 시작할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -283,6 +346,55 @@ export default function MultiDatabaseConfig({ dataSource, onChange }: MultiDatab
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 지도 색상 설정 (MapTestWidgetV2 전용) */}
|
||||
<div className="space-y-3 rounded-lg border bg-muted/30 p-3">
|
||||
<h5 className="text-xs font-semibold">🎨 지도 색상 선택</h5>
|
||||
|
||||
{/* 색상 팔레트 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">색상</Label>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{[
|
||||
{ name: "파랑", marker: "#3b82f6", polygon: "#3b82f6" },
|
||||
{ name: "빨강", marker: "#ef4444", polygon: "#ef4444" },
|
||||
{ name: "초록", marker: "#10b981", polygon: "#10b981" },
|
||||
{ name: "노랑", marker: "#f59e0b", polygon: "#f59e0b" },
|
||||
{ name: "보라", marker: "#8b5cf6", polygon: "#8b5cf6" },
|
||||
{ name: "주황", marker: "#f97316", polygon: "#f97316" },
|
||||
{ name: "청록", marker: "#06b6d4", polygon: "#06b6d4" },
|
||||
{ name: "분홍", marker: "#ec4899", polygon: "#ec4899" },
|
||||
].map((color) => {
|
||||
const isSelected = dataSource.markerColor === color.marker;
|
||||
return (
|
||||
<button
|
||||
key={color.name}
|
||||
type="button"
|
||||
onClick={() => onChange({
|
||||
markerColor: color.marker,
|
||||
polygonColor: color.polygon,
|
||||
polygonOpacity: 0.5,
|
||||
})}
|
||||
className={`flex h-16 flex-col items-center justify-center gap-1 rounded-md border-2 transition-all hover:scale-105 ${
|
||||
isSelected
|
||||
? "border-primary bg-primary/10 shadow-md"
|
||||
: "border-border bg-background hover:border-primary/50"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="h-6 w-6 rounded-full border-2 border-white shadow-sm"
|
||||
style={{ backgroundColor: color.marker }}
|
||||
/>
|
||||
<span className="text-[10px] font-medium">{color.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
선택한 색상이 마커와 폴리곤에 모두 적용됩니다
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 테스트 버튼 */}
|
||||
<div className="space-y-2 border-t pt-4">
|
||||
<Button
|
||||
@@ -476,6 +588,103 @@ export default function MultiDatabaseConfig({ dataSource, onChange }: MultiDatab
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 컬럼 매핑 (쿼리 테스트 성공 후에만 표시) */}
|
||||
{testResult?.success && availableColumns.length > 0 && (
|
||||
<div className="space-y-3 rounded-lg border bg-muted/30 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h5 className="text-xs font-semibold">🔄 컬럼 매핑 (선택사항)</h5>
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">
|
||||
다른 데이터 소스와 통합할 때 컬럼명을 통일할 수 있습니다
|
||||
</p>
|
||||
</div>
|
||||
{dataSource.columnMapping && Object.keys(dataSource.columnMapping).length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onChange({ columnMapping: {} })}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 매핑 목록 */}
|
||||
{dataSource.columnMapping && Object.keys(dataSource.columnMapping).length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{Object.entries(dataSource.columnMapping).map(([original, mapped]) => (
|
||||
<div key={original} className="flex items-center gap-2">
|
||||
{/* 원본 컬럼 (읽기 전용) */}
|
||||
<Input
|
||||
value={original}
|
||||
disabled
|
||||
className="h-8 flex-1 text-xs bg-muted"
|
||||
/>
|
||||
|
||||
{/* 화살표 */}
|
||||
<span className="text-muted-foreground text-xs">→</span>
|
||||
|
||||
{/* 표시 이름 (편집 가능) */}
|
||||
<Input
|
||||
value={mapped}
|
||||
onChange={(e) => {
|
||||
const newMapping = { ...dataSource.columnMapping };
|
||||
newMapping[original] = e.target.value;
|
||||
onChange({ columnMapping: newMapping });
|
||||
}}
|
||||
placeholder="표시 이름"
|
||||
className="h-8 flex-1 text-xs"
|
||||
/>
|
||||
|
||||
{/* 삭제 버튼 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newMapping = { ...dataSource.columnMapping };
|
||||
delete newMapping[original];
|
||||
onChange({ columnMapping: newMapping });
|
||||
}}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<XCircle className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 매핑 추가 */}
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={(col) => {
|
||||
const newMapping = { ...dataSource.columnMapping } || {};
|
||||
newMapping[col] = col; // 기본값은 원본과 동일
|
||||
onChange({ columnMapping: newMapping });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="컬럼 선택하여 매핑 추가" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableColumns
|
||||
.filter(col => !dataSource.columnMapping || !dataSource.columnMapping[col])
|
||||
.map(col => (
|
||||
<SelectItem key={col} value={col} className="text-xs">
|
||||
{col}
|
||||
</SelectItem>
|
||||
))
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
💡 매핑하지 않은 컬럼은 원본 이름 그대로 사용됩니다
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ export type ElementSubtype =
|
||||
| "vehicle-list" // (구버전 - 호환용)
|
||||
| "vehicle-map" // (구버전 - 호환용)
|
||||
| "map-summary" // 범용 지도 카드 (통합)
|
||||
| "map-test" // 🧪 지도 테스트 위젯 (REST API 지원)
|
||||
// | "map-test" // 🧪 지도 테스트 위젯 (REST API 지원) - V2로 대체
|
||||
| "map-test-v2" // 🧪 지도 테스트 V2 (다중 데이터 소스)
|
||||
| "chart-test" // 🧪 차트 테스트 (다중 데이터 소스)
|
||||
| "list-test" // 🧪 리스트 테스트 (다중 데이터 소스)
|
||||
| "custom-metric-test" // 🧪 커스텀 메트릭 테스트 (다중 데이터 소스)
|
||||
| "status-summary-test" // 🧪 상태 요약 테스트 (다중 데이터 소스)
|
||||
| "custom-metric-test" // 🧪 통계 카드 (다중 데이터 소스)
|
||||
// | "status-summary-test" // 🧪 상태 요약 테스트 (CustomMetricTest로 대체 가능)
|
||||
| "risk-alert-test" // 🧪 리스크/알림 테스트 (다중 데이터 소스)
|
||||
| "delivery-status"
|
||||
| "status-summary" // 범용 상태 카드 (통합)
|
||||
@@ -154,7 +154,15 @@ export interface ChartDataSource {
|
||||
lastExecuted?: string; // 마지막 실행 시간
|
||||
lastError?: string; // 마지막 오류 메시지
|
||||
mapDisplayType?: "auto" | "marker" | "polygon"; // 지도 표시 방식 (auto: 자동, marker: 마커, polygon: 영역)
|
||||
|
||||
|
||||
// 지도 색상 설정 (MapTestWidgetV2용)
|
||||
markerColor?: string; // 마커 색상 (예: "#ff0000")
|
||||
polygonColor?: string; // 폴리곤 색상 (예: "#0000ff")
|
||||
polygonOpacity?: number; // 폴리곤 투명도 (0.0 ~ 1.0, 기본값: 0.5)
|
||||
|
||||
// 컬럼 매핑 (다중 데이터 소스 통합용)
|
||||
columnMapping?: Record<string, string>; // { 원본컬럼: 표시이름 } (예: { "name": "product" })
|
||||
|
||||
// 메트릭 설정 (CustomMetricTestWidget용)
|
||||
selectedColumns?: string[]; // 표시할 컬럼 선택 (빈 배열이면 전체 표시)
|
||||
}
|
||||
@@ -163,7 +171,18 @@ export interface ChartConfig {
|
||||
// 다중 데이터 소스 (테스트 위젯용)
|
||||
dataSources?: ChartDataSource[]; // 여러 데이터 소스 (REST API + Database 혼합 가능)
|
||||
|
||||
// 축 매핑
|
||||
// 멀티 차트 설정 (ChartTestWidget용)
|
||||
chartType?: string; // 차트 타입 (line, bar, pie, etc.)
|
||||
mergeMode?: boolean; // 데이터 병합 모드 (여러 데이터 소스를 하나의 라인/바로 합침)
|
||||
dataSourceConfigs?: Array<{
|
||||
dataSourceId: string; // 데이터 소스 ID
|
||||
xAxis: string; // X축 필드명
|
||||
yAxis: string[]; // Y축 필드명 배열
|
||||
label?: string; // 데이터 소스 라벨
|
||||
chartType?: "bar" | "line" | "area"; // 🆕 각 데이터 소스별 차트 타입 (바/라인/영역 혼합 가능)
|
||||
}>;
|
||||
|
||||
// 축 매핑 (단일 데이터 소스용)
|
||||
xAxis?: string; // X축 필드명
|
||||
yAxis?: string | string[]; // Y축 필드명 (다중 가능)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user