ERP-node/frontend/app/(main)/admin/flow-external-db/page.tsx

385 lines
14 KiB
TypeScript

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