플로우 외부db연결
This commit is contained in:
384
frontend/app/(main)/admin/flow-external-db/page.tsx
Normal file
384
frontend/app/(main)/admin/flow-external-db/page.tsx
Normal file
@@ -0,0 +1,384 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { flowExternalDbApi } from "@/lib/api/flowExternalDb";
|
||||
import {
|
||||
FlowExternalDbConnection,
|
||||
CreateFlowExternalDbConnectionRequest,
|
||||
UpdateFlowExternalDbConnectionRequest,
|
||||
DB_TYPE_OPTIONS,
|
||||
getDbTypeLabel,
|
||||
} from "@/types/flowExternalDb";
|
||||
import { Plus, Pencil, Trash2, TestTube, Loader2 } from "lucide-react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
export default function FlowExternalDbPage() {
|
||||
const { toast } = useToast();
|
||||
const [connections, setConnections] = useState<FlowExternalDbConnection[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [editingConnection, setEditingConnection] = useState<FlowExternalDbConnection | null>(null);
|
||||
const [testingId, setTestingId] = useState<number | null>(null);
|
||||
|
||||
// 폼 상태
|
||||
const [formData, setFormData] = useState<
|
||||
CreateFlowExternalDbConnectionRequest | UpdateFlowExternalDbConnectionRequest
|
||||
>({
|
||||
name: "",
|
||||
description: "",
|
||||
dbType: "postgresql",
|
||||
host: "",
|
||||
port: 5432,
|
||||
databaseName: "",
|
||||
username: "",
|
||||
password: "",
|
||||
sslEnabled: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadConnections();
|
||||
}, []);
|
||||
|
||||
const loadConnections = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await flowExternalDbApi.getAll();
|
||||
if (response.success) {
|
||||
setConnections(response.data);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "오류",
|
||||
description: error.message || "외부 DB 연결 목록 조회 실패",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingConnection(null);
|
||||
setFormData({
|
||||
name: "",
|
||||
description: "",
|
||||
dbType: "postgresql",
|
||||
host: "",
|
||||
port: 5432,
|
||||
databaseName: "",
|
||||
username: "",
|
||||
password: "",
|
||||
sslEnabled: false,
|
||||
});
|
||||
setShowDialog(true);
|
||||
};
|
||||
|
||||
const handleEdit = (connection: FlowExternalDbConnection) => {
|
||||
setEditingConnection(connection);
|
||||
setFormData({
|
||||
name: connection.name,
|
||||
description: connection.description,
|
||||
host: connection.host,
|
||||
port: connection.port,
|
||||
databaseName: connection.databaseName,
|
||||
username: connection.username,
|
||||
password: "", // 비밀번호는 비워둠
|
||||
sslEnabled: connection.sslEnabled,
|
||||
isActive: connection.isActive,
|
||||
});
|
||||
setShowDialog(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (editingConnection) {
|
||||
// 수정
|
||||
await flowExternalDbApi.update(editingConnection.id, formData);
|
||||
toast({ title: "성공", description: "외부 DB 연결이 수정되었습니다" });
|
||||
} else {
|
||||
// 생성
|
||||
await flowExternalDbApi.create(formData as CreateFlowExternalDbConnectionRequest);
|
||||
toast({ title: "성공", description: "외부 DB 연결이 생성되었습니다" });
|
||||
}
|
||||
setShowDialog(false);
|
||||
loadConnections();
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "오류",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number, name: string) => {
|
||||
if (!confirm(`"${name}" 연결을 삭제하시겠습니까?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await flowExternalDbApi.delete(id);
|
||||
toast({ title: "성공", description: "외부 DB 연결이 삭제되었습니다" });
|
||||
loadConnections();
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "오류",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestConnection = async (id: number, name: string) => {
|
||||
setTestingId(id);
|
||||
try {
|
||||
const result = await flowExternalDbApi.testConnection(id);
|
||||
toast({
|
||||
title: result.success ? "연결 성공" : "연결 실패",
|
||||
description: result.message,
|
||||
variant: result.success ? "default" : "destructive",
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "오류",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setTestingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">외부 DB 연결 관리</h1>
|
||||
<p className="text-muted-foreground mt-1 text-sm">플로우에서 사용할 외부 데이터베이스 연결을 관리합니다</p>
|
||||
</div>
|
||||
<Button onClick={handleCreate}>
|
||||
<Plus className="mr-2 h-4 w-4" />새 연결 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
) : connections.length === 0 ? (
|
||||
<div className="bg-muted/50 rounded-lg border py-12 text-center">
|
||||
<p className="text-muted-foreground">등록된 외부 DB 연결이 없습니다</p>
|
||||
<Button onClick={handleCreate} className="mt-4">
|
||||
<Plus className="mr-2 h-4 w-4" />첫 연결 추가하기
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead>DB 타입</TableHead>
|
||||
<TableHead>호스트</TableHead>
|
||||
<TableHead>데이터베이스</TableHead>
|
||||
<TableHead>상태</TableHead>
|
||||
<TableHead className="text-right">작업</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{connections.map((conn) => (
|
||||
<TableRow key={conn.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div>
|
||||
<div>{conn.name}</div>
|
||||
{conn.description && <div className="text-muted-foreground text-xs">{conn.description}</div>}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{getDbTypeLabel(conn.dbType)}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-sm">
|
||||
{conn.host}:{conn.port}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-sm">{conn.databaseName}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={conn.isActive ? "default" : "secondary"}>{conn.isActive ? "활성" : "비활성"}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleTestConnection(conn.id, conn.name)}
|
||||
disabled={testingId === conn.id}
|
||||
>
|
||||
{testingId === conn.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<TestTube className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleEdit(conn)}>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleDelete(conn.id, conn.name)}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 생성/수정 다이얼로그 */}
|
||||
<Dialog open={showDialog} onOpenChange={setShowDialog}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingConnection ? "외부 DB 연결 수정" : "새 외부 DB 연결 추가"}</DialogTitle>
|
||||
<DialogDescription>외부 데이터베이스 연결 정보를 입력하세요</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="col-span-2">
|
||||
<Label htmlFor="name">연결 이름 *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="예: 운영_PostgreSQL"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<Label htmlFor="description">설명</Label>
|
||||
<Input
|
||||
id="description"
|
||||
value={formData.description || ""}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="연결에 대한 설명"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="dbType">DB 타입 *</Label>
|
||||
<Select
|
||||
value={formData.dbType}
|
||||
onValueChange={(value: any) => setFormData({ ...formData, dbType: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DB_TYPE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="host">호스트 *</Label>
|
||||
<Input
|
||||
id="host"
|
||||
value={formData.host}
|
||||
onChange={(e) => setFormData({ ...formData, host: e.target.value })}
|
||||
placeholder="localhost"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-24">
|
||||
<Label htmlFor="port">포트 *</Label>
|
||||
<Input
|
||||
id="port"
|
||||
type="number"
|
||||
value={formData.port}
|
||||
onChange={(e) => setFormData({ ...formData, port: parseInt(e.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<Label htmlFor="databaseName">데이터베이스명 *</Label>
|
||||
<Input
|
||||
id="databaseName"
|
||||
value={formData.databaseName}
|
||||
onChange={(e) => setFormData({ ...formData, databaseName: e.target.value })}
|
||||
placeholder="mydb"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="username">사용자명 *</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={formData.username}
|
||||
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
||||
placeholder="dbuser"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="password">비밀번호 {editingConnection && "(변경 시에만 입력)"}</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={formData.password || ""}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
placeholder={editingConnection ? "변경하지 않으려면 비워두세요" : "비밀번호"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 flex items-center gap-2">
|
||||
<Switch
|
||||
id="sslEnabled"
|
||||
checked={formData.sslEnabled}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, sslEnabled: checked })}
|
||||
/>
|
||||
<Label htmlFor="sslEnabled">SSL 사용</Label>
|
||||
</div>
|
||||
|
||||
{editingConnection && (
|
||||
<div className="col-span-2 flex items-center gap-2">
|
||||
<Switch
|
||||
id="isActive"
|
||||
checked={(formData as UpdateFlowExternalDbConnectionRequest).isActive ?? true}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, isActive: checked })}
|
||||
/>
|
||||
<Label htmlFor="isActive">활성화</Label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setShowDialog(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSave}>{editingConnection ? "수정" : "생성"}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user