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

440 lines
14 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}