ERP-node/frontend/lib/registry/components/map/MapConfigPanel.tsx

442 lines
15 KiB
TypeScript
Raw Normal View History

"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>
{/* Radix UI Select v2.x: 빈 문자열 value="" 금지 → "__none__" 사용 */}
<Select
value={config.dataSource?.labelColumn || "__none__"}
onValueChange={(value) => updateConfig("dataSource.labelColumn", value === "__none__" ? "" : value)}
disabled={isLoadingColumns || !config.dataSource?.tableName}
>
<SelectTrigger>
<SelectValue placeholder="라벨 컬럼 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__"> </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>
{/* Radix UI Select v2.x: 빈 문자열 value="" 금지 → "__none__" 사용 */}
<Select
value={config.dataSource?.statusColumn || "__none__"}
onValueChange={(value) => updateConfig("dataSource.statusColumn", value === "__none__" ? "" : value)}
disabled={isLoadingColumns || !config.dataSource?.tableName}
>
<SelectTrigger>
<SelectValue placeholder="상태 컬럼 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__"> </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>
);
}