ERP-node/frontend/components/admin/dashboard/data-sources/MultiDataSourceConfig.tsx

364 lines
14 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useState } from "react";
import { ChartDataSource } from "@/components/admin/dashboard/types";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
2025-10-28 09:32:03 +09:00
import { Plus, Trash2, Database, Globe } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
2025-10-28 09:32:03 +09:00
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import MultiApiConfig from "./MultiApiConfig";
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"
);
const [previewData, setPreviewData] = useState<any[]>([]);
const [showPreview, setShowPreview] = useState(false);
2025-10-28 09:32:03 +09:00
const [showAddMenu, setShowAddMenu] = useState(false);
2025-10-28 09:32:03 +09:00
// 새 데이터 소스 추가 (타입 지정)
const handleAddDataSource = (type: "api" | "database") => {
const newId = Date.now().toString();
const newSource: ChartDataSource = {
id: newId,
2025-10-28 09:32:03 +09:00
name: `${type === "api" ? "REST API" : "Database"} ${dataSources.length + 1}`,
type,
};
onChange([...dataSources, newSource]);
setActiveTab(newId);
2025-10-28 09:32:03 +09:00
setShowAddMenu(false);
};
// 데이터 소스 삭제
const handleDeleteDataSource = (id: string) => {
const filtered = dataSources.filter((ds) => ds.id !== id);
onChange(filtered);
// 삭제 후 첫 번째 탭으로 이동
if (filtered.length > 0) {
setActiveTab(filtered[0].id || "0");
} else {
setActiveTab("new");
}
};
// 데이터 소스 업데이트
const handleUpdateDataSource = (id: string, updates: Partial<ChartDataSource>) => {
const updated = dataSources.map((ds) =>
ds.id === id ? { ...ds, ...updates } : ds
);
onChange(updated);
};
return (
<div className="space-y-4">
{/* 헤더 */}
<div className="flex items-center justify-between">
<div>
<h4 className="text-sm font-semibold"> </h4>
<p className="text-xs text-muted-foreground">
</p>
</div>
2025-10-28 13:40:17 +09:00
<DropdownMenu open={showAddMenu} onOpenChange={setShowAddMenu}>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-8 gap-2 text-xs"
>
<Plus className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleAddDataSource("api")}>
<Globe className="mr-2 h-4 w-4" />
REST API
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleAddDataSource("database")}>
<Database className="mr-2 h-4 w-4" />
Database
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* 데이터 소스가 없는 경우 */}
{dataSources.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-8">
<p className="mb-4 text-sm text-muted-foreground">
</p>
2025-10-28 13:40:17 +09:00
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="default"
size="sm"
className="h-8 gap-2 text-xs"
>
<Plus className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="center">
<DropdownMenuItem onClick={() => handleAddDataSource("api")}>
<Globe className="mr-2 h-4 w-4" />
REST API
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleAddDataSource("database")}>
<Database className="mr-2 h-4 w-4" />
Database
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
) : (
/* 탭 UI */
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="w-full justify-start overflow-x-auto">
{dataSources.map((ds, index) => (
<TabsTrigger
key={ds.id}
value={ds.id || index.toString()}
className="text-xs"
>
{ds.name || `소스 ${index + 1}`}
</TabsTrigger>
))}
</TabsList>
{dataSources.map((ds, index) => (
<TabsContent
key={ds.id}
value={ds.id || index.toString()}
className="space-y-4"
>
{/* 데이터 소스 기본 정보 */}
<div className="space-y-3 rounded-lg border p-4">
{/* 이름 */}
<div className="space-y-2">
<Label htmlFor={`name-${ds.id}`} className="text-xs">
</Label>
<Input
id={`name-${ds.id}`}
value={ds.name || ""}
onChange={(e) =>
handleUpdateDataSource(ds.id!, { name: e.target.value })
}
placeholder="예: 기상특보, 교통정보"
className="h-8 text-xs"
/>
</div>
{/* 타입 선택 */}
<div className="space-y-2">
<Label className="text-xs"> </Label>
<RadioGroup
value={ds.type}
onValueChange={(value: "database" | "api") =>
handleUpdateDataSource(ds.id!, { type: value })
}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="api" id={`api-${ds.id}`} />
<Label
htmlFor={`api-${ds.id}`}
className="text-xs font-normal"
>
REST API
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="database" id={`db-${ds.id}`} />
<Label
htmlFor={`db-${ds.id}`}
className="text-xs font-normal"
>
Database
</Label>
</div>
</RadioGroup>
</div>
{/* 삭제 버튼 */}
<div className="border-t pt-3">
<Button
variant="destructive"
size="sm"
onClick={() => handleDeleteDataSource(ds.id!)}
className="h-8 gap-2 text-xs"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
{/* 지도 표시 방식 선택 (지도 위젯만) */}
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<RadioGroup
value={ds.mapDisplayType || "auto"}
onValueChange={(value) =>
handleUpdateDataSource(ds.id!, { mapDisplayType: value as "auto" | "marker" | "polygon" })
}
className="flex gap-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="auto" id={`auto-${ds.id}`} />
<Label htmlFor={`auto-${ds.id}`} className="text-xs font-normal cursor-pointer">
( )
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="marker" id={`marker-${ds.id}`} />
<Label htmlFor={`marker-${ds.id}`} className="text-xs font-normal cursor-pointer">
📍
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="polygon" id={`polygon-${ds.id}`} />
<Label htmlFor={`polygon-${ds.id}`} className="text-xs font-normal cursor-pointer">
🔷
</Label>
</div>
</RadioGroup>
<p className="text-xs text-muted-foreground">
{ds.mapDisplayType === "marker" && "모든 데이터를 마커로 표시합니다"}
{ds.mapDisplayType === "polygon" && "모든 데이터를 영역(폴리곤)으로 표시합니다"}
{(!ds.mapDisplayType || ds.mapDisplayType === "auto") && "데이터에 coordinates가 있으면 영역, 없으면 마커로 자동 표시"}
</p>
</div>
{/* 타입별 설정 */}
{ds.type === "api" ? (
<MultiApiConfig
dataSource={ds}
onChange={(updates) => handleUpdateDataSource(ds.id!, updates)}
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>
))}
</Tabs>
)}
{/* 지도 미리보기 */}
{showPreview && previewData.length > 0 && (
<div className="rounded-lg border bg-card p-4 shadow-sm">
<div className="mb-3 flex items-center justify-between">
<div>
<h5 className="text-sm font-semibold">
({previewData.length})
</h5>
<p className="text-xs text-muted-foreground">
"적용"
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setShowPreview(false)}
className="h-7 text-xs"
>
</Button>
</div>
<div className="max-h-[400px] space-y-2 overflow-y-auto">
{previewData.map((item, index) => {
const hasLatLng = (item.lat || item.latitude) && (item.lng || item.longitude);
const hasCoordinates = item.coordinates && Array.isArray(item.coordinates);
return (
<div
key={index}
className="rounded border bg-background p-3 text-xs"
>
<div className="mb-1 flex items-center justify-between">
<div className="font-medium">
{item.name || item.title || item.area || item.region || `항목 ${index + 1}`}
</div>
{(item.status || item.level) && (
<div className={`rounded px-2 py-0.5 text-[10px] font-medium ${
(item.status || item.level)?.includes('경보') || (item.status || item.level)?.includes('위험')
2025-10-29 17:53:03 +09:00
? 'bg-destructive/10 text-destructive'
: (item.status || item.level)?.includes('주의')
2025-10-29 17:53:03 +09:00
? 'bg-warning/10 text-warning'
: 'bg-primary/10 text-primary'
}`}>
{item.status || item.level}
</div>
)}
</div>
{hasLatLng && (
<div className="text-muted-foreground">
📍 : ({item.lat || item.latitude}, {item.lng || item.longitude})
</div>
)}
{hasCoordinates && (
<div className="text-muted-foreground">
🔷 : {item.coordinates.length}
</div>
)}
{(item.type || item.description) && (
<div className="mt-1 text-muted-foreground">
{item.type && `${item.type} `}
{item.description && item.description !== item.type && `- ${item.description}`}
</div>
)}
</div>
);
})}
</div>
</div>
)}
</div>
);
}