차량위치 위젯 기존꺼 분할 완료
This commit is contained in:
439
frontend/lib/registry/components/map/MapConfigPanel.tsx
Normal file
439
frontend/lib/registry/components/map/MapConfigPanel.tsx
Normal file
@@ -0,0 +1,439 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
|
||||
interface MapConfigPanelProps {
|
||||
config: any;
|
||||
onChange: (config: any) => void;
|
||||
}
|
||||
|
||||
interface DbConnection {
|
||||
id: number;
|
||||
name: string;
|
||||
db_type: string;
|
||||
}
|
||||
|
||||
interface TableInfo {
|
||||
table_name: string;
|
||||
}
|
||||
|
||||
interface ColumnInfo {
|
||||
column_name: string;
|
||||
data_type: string;
|
||||
}
|
||||
|
||||
export default function MapConfigPanel({ config, onChange }: MapConfigPanelProps) {
|
||||
const [connections, setConnections] = useState<DbConnection[]>([]);
|
||||
const [tables, setTables] = useState<TableInfo[]>([]);
|
||||
const [columns, setColumns] = useState<ColumnInfo[]>([]);
|
||||
const [isLoadingConnections, setIsLoadingConnections] = useState(false);
|
||||
const [isLoadingTables, setIsLoadingTables] = useState(false);
|
||||
const [isLoadingColumns, setIsLoadingColumns] = useState(false);
|
||||
|
||||
// DB 연결 목록 로드
|
||||
useEffect(() => {
|
||||
loadConnections();
|
||||
}, []);
|
||||
|
||||
// 테이블 목록 로드
|
||||
useEffect(() => {
|
||||
if (config.dataSource?.type === "external" && config.dataSource?.connectionId) {
|
||||
loadTables(config.dataSource.connectionId);
|
||||
} else if (config.dataSource?.type === "internal") {
|
||||
loadInternalTables();
|
||||
}
|
||||
}, [config.dataSource?.type, config.dataSource?.connectionId]);
|
||||
|
||||
// 컬럼 목록 로드
|
||||
useEffect(() => {
|
||||
if (config.dataSource?.tableName) {
|
||||
if (config.dataSource.type === "external" && config.dataSource.connectionId) {
|
||||
loadColumns(config.dataSource.connectionId, config.dataSource.tableName);
|
||||
} else if (config.dataSource.type === "internal") {
|
||||
loadInternalColumns(config.dataSource.tableName);
|
||||
}
|
||||
}
|
||||
}, [config.dataSource?.tableName]);
|
||||
|
||||
const loadConnections = async () => {
|
||||
setIsLoadingConnections(true);
|
||||
try {
|
||||
const response = await fetch("/api/external-db-connections");
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setConnections(data.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("DB 연결 목록 로드 실패:", error);
|
||||
} finally {
|
||||
setIsLoadingConnections(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadTables = async (connectionId: number) => {
|
||||
setIsLoadingTables(true);
|
||||
try {
|
||||
const response = await fetch(`/api/external-db-connections/${connectionId}/tables`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setTables(data.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("테이블 목록 로드 실패:", error);
|
||||
} finally {
|
||||
setIsLoadingTables(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadInternalTables = async () => {
|
||||
setIsLoadingTables(true);
|
||||
try {
|
||||
const response = await fetch("/api/table-management/tables");
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setTables(data.data.map((t: any) => ({ table_name: t.tableName })) || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("내부 테이블 목록 로드 실패:", error);
|
||||
} finally {
|
||||
setIsLoadingTables(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadColumns = async (connectionId: number, tableName: string) => {
|
||||
setIsLoadingColumns(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/external-db-connections/${connectionId}/tables/${encodeURIComponent(tableName)}/columns`
|
||||
);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setColumns(data.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("컬럼 목록 로드 실패:", error);
|
||||
} finally {
|
||||
setIsLoadingColumns(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadInternalColumns = async (tableName: string) => {
|
||||
setIsLoadingColumns(true);
|
||||
try {
|
||||
const response = await fetch(`/api/table-management/tables/${encodeURIComponent(tableName)}/columns`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setColumns(data.data.map((c: any) => ({ column_name: c.columnName, data_type: c.dataType })) || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("내부 컬럼 목록 로드 실패:", error);
|
||||
} finally {
|
||||
setIsLoadingColumns(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateConfig = (path: string, value: any) => {
|
||||
const keys = path.split(".");
|
||||
const newConfig = { ...config };
|
||||
let current: any = newConfig;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
if (!current[keys[i]]) {
|
||||
current[keys[i]] = {};
|
||||
}
|
||||
current = current[keys[i]];
|
||||
}
|
||||
|
||||
current[keys[keys.length - 1]] = value;
|
||||
onChange(newConfig);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-3">📊 데이터 소스</h3>
|
||||
|
||||
{/* DB 타입 선택 */}
|
||||
<div className="space-y-2 mb-3">
|
||||
<Label>DB 타입</Label>
|
||||
<Select
|
||||
value={config.dataSource?.type || "internal"}
|
||||
onValueChange={(value) => {
|
||||
updateConfig("dataSource.type", value);
|
||||
updateConfig("dataSource.tableName", "");
|
||||
updateConfig("dataSource.connectionId", null);
|
||||
setTables([]);
|
||||
setColumns([]);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="DB 타입 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="internal">내부 DB (PostgreSQL)</SelectItem>
|
||||
<SelectItem value="external">외부 DB 연결</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 외부 DB 연결 선택 */}
|
||||
{config.dataSource?.type === "external" && (
|
||||
<div className="space-y-2 mb-3">
|
||||
<Label>외부 DB 연결</Label>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={config.dataSource?.connectionId?.toString() || ""}
|
||||
onValueChange={(value) => {
|
||||
updateConfig("dataSource.connectionId", parseInt(value));
|
||||
updateConfig("dataSource.tableName", "");
|
||||
setTables([]);
|
||||
setColumns([]);
|
||||
}}
|
||||
disabled={isLoadingConnections}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="DB 연결 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{connections.map((conn) => (
|
||||
<SelectItem key={conn.id} value={conn.id.toString()}>
|
||||
{conn.name} ({conn.db_type})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
onClick={loadConnections}
|
||||
size="icon"
|
||||
variant="outline"
|
||||
disabled={isLoadingConnections}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isLoadingConnections ? "animate-spin" : ""}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 테이블 선택 */}
|
||||
<div className="space-y-2 mb-3">
|
||||
<Label>테이블</Label>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={config.dataSource?.tableName || ""}
|
||||
onValueChange={(value) => {
|
||||
updateConfig("dataSource.tableName", value);
|
||||
setColumns([]);
|
||||
}}
|
||||
disabled={
|
||||
isLoadingTables ||
|
||||
(config.dataSource?.type === "external" && !config.dataSource?.connectionId)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="테이블 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{tables.map((table) => (
|
||||
<SelectItem key={table.table_name} value={table.table_name}>
|
||||
{table.table_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (config.dataSource?.type === "external" && config.dataSource?.connectionId) {
|
||||
loadTables(config.dataSource.connectionId);
|
||||
} else if (config.dataSource?.type === "internal") {
|
||||
loadInternalTables();
|
||||
}
|
||||
}}
|
||||
size="icon"
|
||||
variant="outline"
|
||||
disabled={isLoadingTables}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isLoadingTables ? "animate-spin" : ""}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 위도 컬럼 */}
|
||||
<div className="space-y-2 mb-3">
|
||||
<Label>위도 컬럼 *</Label>
|
||||
<Select
|
||||
value={config.dataSource?.latColumn || ""}
|
||||
onValueChange={(value) => updateConfig("dataSource.latColumn", value)}
|
||||
disabled={isLoadingColumns || !config.dataSource?.tableName}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="위도 컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{columns.map((col) => (
|
||||
<SelectItem key={col.column_name} value={col.column_name}>
|
||||
{col.column_name} ({col.data_type})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 경도 컬럼 */}
|
||||
<div className="space-y-2 mb-3">
|
||||
<Label>경도 컬럼 *</Label>
|
||||
<Select
|
||||
value={config.dataSource?.lngColumn || ""}
|
||||
onValueChange={(value) => updateConfig("dataSource.lngColumn", value)}
|
||||
disabled={isLoadingColumns || !config.dataSource?.tableName}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="경도 컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{columns.map((col) => (
|
||||
<SelectItem key={col.column_name} value={col.column_name}>
|
||||
{col.column_name} ({col.data_type})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 라벨 컬럼 (선택) */}
|
||||
<div className="space-y-2 mb-3">
|
||||
<Label>라벨 컬럼 (선택사항)</Label>
|
||||
<Select
|
||||
value={config.dataSource?.labelColumn || ""}
|
||||
onValueChange={(value) => updateConfig("dataSource.labelColumn", value)}
|
||||
disabled={isLoadingColumns || !config.dataSource?.tableName}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="라벨 컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">선택 안 함</SelectItem>
|
||||
{columns.map((col) => (
|
||||
<SelectItem key={col.column_name} value={col.column_name}>
|
||||
{col.column_name} ({col.data_type})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 상태 컬럼 (선택) */}
|
||||
<div className="space-y-2 mb-3">
|
||||
<Label>상태 컬럼 (선택사항)</Label>
|
||||
<Select
|
||||
value={config.dataSource?.statusColumn || ""}
|
||||
onValueChange={(value) => updateConfig("dataSource.statusColumn", value)}
|
||||
disabled={isLoadingColumns || !config.dataSource?.tableName}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="상태 컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">선택 안 함</SelectItem>
|
||||
{columns.map((col) => (
|
||||
<SelectItem key={col.column_name} value={col.column_name}>
|
||||
{col.column_name} ({col.data_type})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* WHERE 조건 (선택) */}
|
||||
<div className="space-y-2 mb-3">
|
||||
<Label>WHERE 조건 (선택사항)</Label>
|
||||
<Textarea
|
||||
value={config.dataSource?.whereClause || ""}
|
||||
onChange={(e) => updateConfig("dataSource.whereClause", e.target.value)}
|
||||
placeholder="예: status = 'active' AND city = 'Seoul'"
|
||||
rows={2}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">SQL WHERE 절 (WHERE 키워드 제외)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-3">🗺️ 지도 설정</h3>
|
||||
|
||||
{/* 중심 좌표 */}
|
||||
<div className="grid grid-cols-2 gap-2 mb-3">
|
||||
<div className="space-y-2">
|
||||
<Label>중심 위도</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.0001"
|
||||
value={config.mapConfig?.center?.lat || 36.5}
|
||||
onChange={(e) =>
|
||||
updateConfig("mapConfig.center.lat", parseFloat(e.target.value) || 36.5)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>중심 경도</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.0001"
|
||||
value={config.mapConfig?.center?.lng || 127.5}
|
||||
onChange={(e) =>
|
||||
updateConfig("mapConfig.center.lng", parseFloat(e.target.value) || 127.5)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 줌 레벨 */}
|
||||
<div className="space-y-2 mb-3">
|
||||
<Label>기본 줌 레벨</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max="18"
|
||||
value={config.mapConfig?.zoom || 7}
|
||||
onChange={(e) => updateConfig("mapConfig.zoom", parseInt(e.target.value) || 7)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-3">🔄 새로고침 설정</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>자동 새로고침 (초)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
value={(config.refreshInterval || 0) / 1000}
|
||||
onChange={(e) =>
|
||||
updateConfig("refreshInterval", parseInt(e.target.value) * 1000 || 0)
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">0이면 자동 새로고침 없음</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user