647 lines
22 KiB
TypeScript
647 lines
22 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useCallback, useEffect } from "react";
|
|
import {
|
|
DashboardElement,
|
|
ChartDataSource,
|
|
ElementSubtype,
|
|
QueryResult,
|
|
ListWidgetConfig,
|
|
ChartConfig,
|
|
CustomMetricConfig,
|
|
} from "./types";
|
|
import { X } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { DatabaseConfig } from "./data-sources/DatabaseConfig";
|
|
import { ApiConfig } from "./data-sources/ApiConfig";
|
|
import { QueryEditor } from "./QueryEditor";
|
|
import { ListWidgetSection } from "./widget-sections/ListWidgetSection";
|
|
import { ChartConfigSection } from "./widget-sections/ChartConfigSection";
|
|
import { CustomMetricSection } from "./widget-sections/CustomMetricSection";
|
|
import { MapConfigSection } from "./widget-sections/MapConfigSection";
|
|
import { RiskAlertSection } from "./widget-sections/RiskAlertSection";
|
|
import MultiDataSourceConfig from "@/components/admin/dashboard/data-sources/MultiDataSourceConfig";
|
|
|
|
interface WidgetConfigSidebarProps {
|
|
element: DashboardElement | null;
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onApply: (element: DashboardElement) => void;
|
|
}
|
|
|
|
// 위젯 분류 헬퍼 함수
|
|
const needsDataSource = (subtype: ElementSubtype): boolean => {
|
|
// 차트 타입들
|
|
const chartTypes = ["bar", "horizontal-bar", "pie", "line", "area", "stacked-bar", "donut", "combo"];
|
|
|
|
const dataWidgets = [
|
|
"list-v2",
|
|
"custom-metric-v2",
|
|
"chart",
|
|
"map-summary-v2",
|
|
"risk-alert-v2",
|
|
// "yard-management-3d", // 데이터 탭 불필요 (레이아웃 선택만 사용)
|
|
"todo",
|
|
"document",
|
|
"work-history",
|
|
"transport-stats",
|
|
"booking-alert",
|
|
"maintenance",
|
|
"vehicle-status",
|
|
"vehicle-list",
|
|
"status-summary",
|
|
"delivery-status",
|
|
"delivery-status-summary",
|
|
"delivery-today-stats",
|
|
"cargo-list",
|
|
"customer-issues",
|
|
"driver-management",
|
|
];
|
|
|
|
return chartTypes.includes(subtype) || dataWidgets.includes(subtype);
|
|
};
|
|
|
|
const getWidgetIcon = (subtype: ElementSubtype): string => {
|
|
const iconMap: Record<string, string> = {
|
|
"list-v2": "📋",
|
|
"custom-metric-v2": "📊",
|
|
chart: "📈",
|
|
"map-summary-v2": "🗺️",
|
|
"risk-alert-v2": "⚠️",
|
|
"yard-management-3d": "🏗️",
|
|
weather: "🌤️",
|
|
exchange: "💱",
|
|
calculator: "🧮",
|
|
clock: "🕐",
|
|
calendar: "📅",
|
|
todo: "✅",
|
|
document: "📄",
|
|
};
|
|
return iconMap[subtype] || "🔧";
|
|
};
|
|
|
|
const getWidgetTitle = (subtype: ElementSubtype): string => {
|
|
const titleMap: Record<string, string> = {
|
|
"list-v2": "리스트 위젯",
|
|
"custom-metric-v2": "통계 카드",
|
|
chart: "차트",
|
|
"map-summary-v2": "지도",
|
|
"risk-alert-v2": "리스크 알림",
|
|
"yard-management-3d": "3D 필드",
|
|
weather: "날씨 위젯",
|
|
exchange: "환율 위젯",
|
|
calculator: "계산기",
|
|
clock: "시계",
|
|
calendar: "달력",
|
|
todo: "할 일",
|
|
document: "문서",
|
|
};
|
|
return titleMap[subtype] || "위젯";
|
|
};
|
|
|
|
/**
|
|
* 통합 위젯 설정 사이드바
|
|
* - 모든 위젯 타입에 대한 일관된 설정 UI 제공
|
|
* - 일반 탭: 제목, 헤더 표시 설정
|
|
* - 데이터 탭: 데이터 소스 및 위젯별 커스텀 설정
|
|
*/
|
|
export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: WidgetConfigSidebarProps) {
|
|
// 일반 설정 state
|
|
const [customTitle, setCustomTitle] = useState<string>("");
|
|
const [showHeader, setShowHeader] = useState<boolean>(true);
|
|
|
|
// 데이터 소스 state
|
|
const [dataSource, setDataSource] = useState<ChartDataSource>({
|
|
type: "database",
|
|
connectionType: "current",
|
|
refreshInterval: 0,
|
|
});
|
|
|
|
// 다중 데이터 소스 상태 추가
|
|
const [dataSources, setDataSources] = useState<ChartDataSource[]>(element?.dataSources || []);
|
|
|
|
// 쿼리 결과
|
|
const [queryResult, setQueryResult] = useState<QueryResult | null>(null);
|
|
|
|
// 리스트 위젯 설정
|
|
const [listConfig, setListConfig] = useState<ListWidgetConfig>({
|
|
viewMode: "table",
|
|
columns: [],
|
|
pageSize: 10,
|
|
enablePagination: true,
|
|
showHeader: true,
|
|
stripedRows: true,
|
|
compactMode: false,
|
|
cardColumns: 3,
|
|
});
|
|
|
|
// 차트 설정
|
|
const [chartConfig, setChartConfig] = useState<ChartConfig>({});
|
|
|
|
// 커스텀 메트릭 설정
|
|
const [customMetricConfig, setCustomMetricConfig] = useState<CustomMetricConfig>({});
|
|
|
|
// 자동 새로고침 간격 (지도 위젯용)
|
|
const [refreshInterval, setRefreshInterval] = useState<number>(5);
|
|
|
|
// 사이드바 열릴 때 초기화
|
|
useEffect(() => {
|
|
if (isOpen && element) {
|
|
setCustomTitle(element.customTitle || "");
|
|
setShowHeader(element.showHeader !== false);
|
|
setDataSource(element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 });
|
|
// dataSources는 element.dataSources 또는 chartConfig.dataSources에서 가져옴
|
|
setDataSources(element.dataSources || element.chartConfig?.dataSources || []);
|
|
setQueryResult(null);
|
|
// 자동 새로고침 간격 초기화
|
|
setRefreshInterval(element.chartConfig?.refreshInterval ?? 5);
|
|
|
|
// 리스트 위젯 설정 초기화
|
|
if (element.subtype === "list-v2" && element.listConfig) {
|
|
setListConfig(element.listConfig);
|
|
} else {
|
|
setListConfig({
|
|
viewMode: "table",
|
|
columns: [],
|
|
pageSize: 10,
|
|
enablePagination: true,
|
|
showHeader: true,
|
|
stripedRows: true,
|
|
compactMode: false,
|
|
cardColumns: 3,
|
|
});
|
|
}
|
|
|
|
// 차트 설정 초기화
|
|
setChartConfig(element.chartConfig || {});
|
|
|
|
// 커스텀 메트릭 설정 초기화
|
|
setCustomMetricConfig(element.customMetricConfig || {});
|
|
} else if (!isOpen) {
|
|
// 사이드바 닫힐 때 초기화
|
|
setCustomTitle("");
|
|
setShowHeader(true);
|
|
setDataSource({ type: "database", connectionType: "current", refreshInterval: 0 });
|
|
setDataSources([]);
|
|
setQueryResult(null);
|
|
setListConfig({
|
|
viewMode: "table",
|
|
columns: [],
|
|
pageSize: 10,
|
|
enablePagination: true,
|
|
showHeader: true,
|
|
stripedRows: true,
|
|
compactMode: false,
|
|
cardColumns: 3,
|
|
});
|
|
setChartConfig({});
|
|
setCustomMetricConfig({});
|
|
}
|
|
}, [isOpen, element]);
|
|
|
|
// Esc 키로 닫기
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
|
|
const handleEsc = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") {
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
window.addEventListener("keydown", handleEsc);
|
|
return () => window.removeEventListener("keydown", handleEsc);
|
|
}, [isOpen, onClose]);
|
|
|
|
// 데이터 소스 타입 변경
|
|
const handleDataSourceTypeChange = useCallback((type: "database" | "api") => {
|
|
if (type === "database") {
|
|
setDataSource({
|
|
type: "database",
|
|
connectionType: "current",
|
|
refreshInterval: 0,
|
|
});
|
|
} else {
|
|
setDataSource({
|
|
type: "api",
|
|
method: "GET",
|
|
refreshInterval: 0,
|
|
});
|
|
}
|
|
}, []);
|
|
|
|
// 데이터 소스 업데이트
|
|
const handleDataSourceUpdate = useCallback((updates: Partial<ChartDataSource>) => {
|
|
setDataSource((prev) => ({ ...prev, ...updates }));
|
|
}, []);
|
|
|
|
// 다중 데이터 소스 변경 핸들러
|
|
const handleDataSourcesChange = useCallback((updatedSources: ChartDataSource[]) => {
|
|
setDataSources(updatedSources);
|
|
}, []);
|
|
|
|
// 쿼리 테스트 결과 처리
|
|
const handleQueryTest = useCallback(
|
|
(result: QueryResult) => {
|
|
setQueryResult(result);
|
|
|
|
// 리스트 위젯: 쿼리 결과로 컬럼 자동 생성
|
|
if (element?.subtype === "list-v2" && result.columns && result.columns.length > 0) {
|
|
const newColumns = result.columns.map((col: string, idx: number) => ({
|
|
id: `col_${Date.now()}_${idx}`,
|
|
field: col,
|
|
label: col,
|
|
visible: true,
|
|
sortable: true,
|
|
filterable: false,
|
|
align: "left" as const,
|
|
}));
|
|
|
|
setListConfig((prev) => ({ ...prev, columns: newColumns }));
|
|
}
|
|
},
|
|
[element],
|
|
);
|
|
|
|
// 리스트 설정 변경
|
|
const handleListConfigChange = useCallback((updates: Partial<ListWidgetConfig>) => {
|
|
setListConfig((prev) => ({ ...prev, ...updates }));
|
|
}, []);
|
|
|
|
// 차트 설정 변경
|
|
const handleChartConfigChange = useCallback((config: ChartConfig) => {
|
|
setChartConfig(config);
|
|
}, []);
|
|
|
|
// 커스텀 메트릭 설정 변경
|
|
const handleCustomMetricConfigChange = useCallback((updates: Partial<CustomMetricConfig>) => {
|
|
setCustomMetricConfig((prev) => ({ ...prev, ...updates }));
|
|
}, []);
|
|
|
|
// 적용
|
|
const handleApply = useCallback(() => {
|
|
if (!element) return;
|
|
|
|
// 다중 데이터 소스를 사용하는 위젯 체크
|
|
const isMultiDataSourceWidget =
|
|
element.subtype === "map-summary-v2" ||
|
|
element.subtype === "chart" ||
|
|
element.subtype === "list-v2" ||
|
|
element.subtype === "custom-metric-v2" ||
|
|
element.subtype === "risk-alert-v2";
|
|
|
|
// chartConfig 구성 (위젯 타입별로 다르게 처리)
|
|
let finalChartConfig = { ...chartConfig };
|
|
|
|
if (isMultiDataSourceWidget) {
|
|
finalChartConfig = {
|
|
...finalChartConfig,
|
|
dataSources: dataSources,
|
|
};
|
|
}
|
|
|
|
// 지도 위젯인 경우 refreshInterval 추가
|
|
if (element.subtype === "map-summary-v2") {
|
|
finalChartConfig = {
|
|
...finalChartConfig,
|
|
refreshInterval,
|
|
};
|
|
}
|
|
|
|
const updatedElement: DashboardElement = {
|
|
...element,
|
|
customTitle: customTitle.trim() || undefined,
|
|
showHeader,
|
|
// 데이터 소스 처리
|
|
...(needsDataSource(element.subtype)
|
|
? {
|
|
dataSource,
|
|
// 다중 데이터 소스 위젯은 dataSources도 포함 (빈 배열도 허용 - 연결 해제)
|
|
...(isMultiDataSourceWidget
|
|
? {
|
|
dataSources: dataSources,
|
|
}
|
|
: {}),
|
|
}
|
|
: {}),
|
|
// 리스트 위젯 설정
|
|
...(element.subtype === "list-v2"
|
|
? {
|
|
listConfig,
|
|
}
|
|
: {}),
|
|
// 차트 설정 (모든 위젯 공통)
|
|
...(needsDataSource(element.subtype)
|
|
? {
|
|
chartConfig: finalChartConfig,
|
|
}
|
|
: {}),
|
|
// 커스텀 메트릭 설정
|
|
...(element.subtype === "custom-metric-v2"
|
|
? {
|
|
customMetricConfig,
|
|
}
|
|
: {}),
|
|
};
|
|
|
|
console.log("🔧 [WidgetConfigSidebar] handleApply 호출:", {
|
|
subtype: element.subtype,
|
|
isMultiDataSourceWidget,
|
|
dataSources,
|
|
listConfig,
|
|
finalChartConfig,
|
|
customMetricConfig,
|
|
updatedElement,
|
|
});
|
|
|
|
onApply(updatedElement);
|
|
onClose();
|
|
}, [
|
|
element,
|
|
customTitle,
|
|
showHeader,
|
|
dataSource,
|
|
dataSources,
|
|
listConfig,
|
|
chartConfig,
|
|
customMetricConfig,
|
|
refreshInterval,
|
|
onApply,
|
|
onClose,
|
|
]);
|
|
|
|
if (!element) return null;
|
|
|
|
const hasDataTab = needsDataSource(element.subtype);
|
|
const widgetIcon = getWidgetIcon(element.subtype);
|
|
const widgetTitle = getWidgetTitle(element.subtype);
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"bg-muted fixed top-14 left-0 z-[100] flex h-[calc(100vh-3.5rem)] w-80 flex-col transition-transform duration-300 ease-in-out",
|
|
isOpen ? "translate-x-0" : "translate-x-[-100%]",
|
|
)}
|
|
>
|
|
{/* 헤더 */}
|
|
<div className="bg-background flex items-center justify-between px-3 py-2 shadow-sm">
|
|
<div className="flex items-center gap-2">
|
|
<div className="bg-primary/10 flex h-6 w-6 items-center justify-center rounded">
|
|
<span className="text-primary text-xs font-bold">{widgetIcon}</span>
|
|
</div>
|
|
<span className="text-foreground text-xs font-semibold">{widgetTitle} 설정</span>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="hover:bg-muted flex h-6 w-6 items-center justify-center rounded transition-colors"
|
|
>
|
|
<X className="text-muted-foreground h-3.5 w-3.5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* 탭 영역 */}
|
|
<Tabs defaultValue="general" className="flex flex-1 flex-col overflow-hidden">
|
|
<TabsList className="bg-background mx-3 mt-3 grid h-9 w-auto grid-cols-2">
|
|
<TabsTrigger value="general" className="text-xs">
|
|
일반
|
|
</TabsTrigger>
|
|
{hasDataTab && (
|
|
<TabsTrigger value="data" className="text-xs">
|
|
데이터
|
|
</TabsTrigger>
|
|
)}
|
|
</TabsList>
|
|
|
|
{/* 일반 탭 */}
|
|
<TabsContent value="general" className="mt-0 flex-1 overflow-y-auto p-3">
|
|
<div className="space-y-3">
|
|
{/* 위젯 제목 */}
|
|
<div className="bg-background rounded-lg p-3 shadow-sm">
|
|
<Label htmlFor="widget-title" className="mb-2 block text-xs font-semibold">
|
|
위젯 제목
|
|
</Label>
|
|
<Input
|
|
id="widget-title"
|
|
value={customTitle}
|
|
onChange={(e) => setCustomTitle(e.target.value)}
|
|
placeholder={`기본 제목: ${element.title}`}
|
|
className="h-9 text-sm"
|
|
/>
|
|
<p className="text-muted-foreground mt-1.5 text-xs">비워두면 기본 제목이 표시됩니다</p>
|
|
</div>
|
|
|
|
{/* 헤더 표시 */}
|
|
<div className="bg-background rounded-lg p-3 shadow-sm">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex-1">
|
|
<Label htmlFor="show-header" className="text-xs font-semibold">
|
|
헤더 표시
|
|
</Label>
|
|
<p className="text-muted-foreground mt-1 text-xs">위젯 상단 헤더를 표시합니다</p>
|
|
</div>
|
|
<Switch id="show-header" checked={showHeader} onCheckedChange={setShowHeader} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* 레이아웃 선택 (3D 필드 위젯 전용) */}
|
|
{element.subtype === "yard-management-3d" && (
|
|
<div className="bg-background rounded-lg p-3 shadow-sm">
|
|
<Label htmlFor="layout-id" className="mb-2 block text-xs font-semibold">
|
|
레이아웃 선택
|
|
</Label>
|
|
<p className="text-muted-foreground mb-2 text-xs">표시할 디지털 트윈 레이아웃을 선택하세요</p>
|
|
<div className="text-muted-foreground text-xs">
|
|
<p>위젯 내부에서 레이아웃을 선택할 수 있습니다.</p>
|
|
<p className="mt-1">편집 모드에서 레이아웃 목록을 확인하고 선택하세요.</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 자동 새로고침 설정 (지도 위젯 전용) */}
|
|
{element.subtype === "map-summary-v2" && (
|
|
<div className="bg-background rounded-lg p-3 shadow-sm">
|
|
<Label htmlFor="refresh-interval" className="mb-2 block text-xs font-semibold">
|
|
자동 새로고침 간격
|
|
</Label>
|
|
<Select
|
|
value={refreshInterval.toString()}
|
|
onValueChange={(value) => setRefreshInterval(parseInt(value))}
|
|
>
|
|
<SelectTrigger id="refresh-interval" className="h-9 text-sm">
|
|
<SelectValue placeholder="간격 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="0" className="text-sm">
|
|
없음
|
|
</SelectItem>
|
|
<SelectItem value="5" className="text-sm">
|
|
5초
|
|
</SelectItem>
|
|
<SelectItem value="10" className="text-sm">
|
|
10초
|
|
</SelectItem>
|
|
<SelectItem value="30" className="text-sm">
|
|
30초
|
|
</SelectItem>
|
|
<SelectItem value="60" className="text-sm">
|
|
1분
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-muted-foreground mt-1.5 text-xs">
|
|
위젯의 모든 데이터를 자동으로 갱신하는 주기를 설정합니다
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* 데이터 탭 */}
|
|
{hasDataTab && (
|
|
<TabsContent value="data" className="mt-0 flex-1 overflow-y-auto p-3">
|
|
<div className="space-y-3">
|
|
{/* 데이터 소스 선택 - 단일 데이터 소스 위젯에만 표시 */}
|
|
{!["map-summary-v2", "chart", "risk-alert-v2"].includes(element.subtype) && (
|
|
<div className="bg-background rounded-lg p-3 shadow-sm">
|
|
<Label className="mb-2 block text-xs font-semibold">데이터 소스</Label>
|
|
|
|
<Tabs
|
|
value={dataSource.type}
|
|
onValueChange={(value) => handleDataSourceTypeChange(value as "database" | "api")}
|
|
className="w-full"
|
|
>
|
|
<TabsList className="bg-muted grid h-8 w-full grid-cols-2 p-0.5">
|
|
<TabsTrigger
|
|
value="database"
|
|
className="data-[state=active]:bg-background h-7 rounded text-xs data-[state=active]:shadow-sm"
|
|
>
|
|
데이터베이스
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="api"
|
|
className="data-[state=active]:bg-background h-7 rounded text-xs data-[state=active]:shadow-sm"
|
|
>
|
|
REST API
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="database" className="mt-2 space-y-2">
|
|
<DatabaseConfig dataSource={dataSource} onChange={handleDataSourceUpdate} />
|
|
<QueryEditor
|
|
dataSource={dataSource}
|
|
onDataSourceChange={handleDataSourceUpdate}
|
|
onQueryTest={handleQueryTest}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="api" className="mt-2 space-y-2">
|
|
<ApiConfig
|
|
dataSource={dataSource}
|
|
onChange={handleDataSourceUpdate}
|
|
onTestResult={handleQueryTest}
|
|
/>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
)}
|
|
|
|
{/* 다중 데이터 소스 설정 */}
|
|
{["map-summary-v2", "chart", "risk-alert-v2"].includes(element.subtype) && (
|
|
<MultiDataSourceConfig dataSources={dataSources} onChange={handleDataSourcesChange} />
|
|
)}
|
|
|
|
{/* 위젯별 커스텀 섹션 */}
|
|
{element.subtype === "list-v2" && (
|
|
<ListWidgetSection
|
|
queryResult={queryResult}
|
|
config={listConfig}
|
|
onConfigChange={handleListConfigChange}
|
|
/>
|
|
)}
|
|
|
|
{/* 차트 설정 */}
|
|
{(element.type === "chart" ||
|
|
element.subtype === "chart" ||
|
|
["bar", "horizontal-bar", "pie", "line", "area", "stacked-bar", "donut", "combo"].includes(
|
|
element.subtype,
|
|
)) && (
|
|
<ChartConfigSection
|
|
queryResult={queryResult}
|
|
dataSource={dataSource}
|
|
config={chartConfig}
|
|
chartType={element.subtype}
|
|
onConfigChange={handleChartConfigChange}
|
|
/>
|
|
)}
|
|
|
|
{/* 커스텀 메트릭 설정 */}
|
|
{element.subtype === "custom-metric-v2" && (
|
|
<CustomMetricSection
|
|
queryResult={queryResult}
|
|
config={customMetricConfig}
|
|
onConfigChange={handleCustomMetricConfigChange}
|
|
/>
|
|
)}
|
|
|
|
{/* 지도 설정 */}
|
|
{element.subtype === "map-summary-v2" && (
|
|
<MapConfigSection
|
|
queryResult={queryResult}
|
|
refreshInterval={element.chartConfig?.refreshInterval || 5}
|
|
markerType={element.chartConfig?.markerType || "circle"}
|
|
onRefreshIntervalChange={(interval) => {
|
|
setChartConfig((prev) => ({
|
|
...prev,
|
|
refreshInterval: interval,
|
|
}));
|
|
}}
|
|
onMarkerTypeChange={(type) => {
|
|
setChartConfig((prev) => ({
|
|
...prev,
|
|
markerType: type,
|
|
}));
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* 리스크 알림 설정 */}
|
|
{element.subtype === "risk-alert-v2" && <RiskAlertSection queryResult={queryResult} />}
|
|
</div>
|
|
</TabsContent>
|
|
)}
|
|
</Tabs>
|
|
|
|
{/* 푸터 */}
|
|
<div className="bg-background flex gap-2 border-t p-3">
|
|
<Button variant="outline" onClick={onClose} className="h-9 flex-1 text-sm">
|
|
취소
|
|
</Button>
|
|
<Button
|
|
onClick={handleApply}
|
|
className="h-9 flex-1 text-sm"
|
|
disabled={
|
|
// 다중 데이터 소스 위젯: 데이터 소스가 있는데 endpoint가 비어있으면 비활성화
|
|
// (데이터 소스가 없는 건 OK - 연결 해제하는 경우)
|
|
(element?.subtype === "map-summary-v2" ||
|
|
element?.subtype === "chart" ||
|
|
element?.subtype === "list-v2" ||
|
|
element?.subtype === "custom-metric-v2" ||
|
|
element?.subtype === "risk-alert-v2") &&
|
|
dataSources &&
|
|
dataSources.length > 0 &&
|
|
dataSources.some((ds) => ds.type === "api" && !ds.endpoint)
|
|
}
|
|
>
|
|
적용
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|