365 lines
14 KiB
TypeScript
365 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { Plus, Search, Pencil, Trash2, Database } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import {
|
|
ExternalDbConnectionAPI,
|
|
ExternalDbConnection,
|
|
ExternalDbConnectionFilter,
|
|
} from "@/lib/api/externalDbConnection";
|
|
import { ExternalDbConnectionModal } from "@/components/admin/ExternalDbConnectionModal";
|
|
|
|
// DB 타입 매핑
|
|
const DB_TYPE_LABELS: Record<string, string> = {
|
|
mysql: "MySQL",
|
|
postgresql: "PostgreSQL",
|
|
oracle: "Oracle",
|
|
mssql: "SQL Server",
|
|
sqlite: "SQLite",
|
|
};
|
|
|
|
// 활성 상태 옵션
|
|
const ACTIVE_STATUS_OPTIONS = [
|
|
{ value: "ALL", label: "전체" },
|
|
{ value: "Y", label: "활성" },
|
|
{ value: "N", label: "비활성" },
|
|
];
|
|
|
|
export default function ExternalConnectionsPage() {
|
|
const { toast } = useToast();
|
|
|
|
// 상태 관리
|
|
const [connections, setConnections] = useState<ExternalDbConnection[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [dbTypeFilter, setDbTypeFilter] = useState("ALL");
|
|
const [activeStatusFilter, setActiveStatusFilter] = useState("ALL");
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [editingConnection, setEditingConnection] = useState<ExternalDbConnection | undefined>();
|
|
const [supportedDbTypes, setSupportedDbTypes] = useState<Array<{ value: string; label: string }>>([]);
|
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
const [connectionToDelete, setConnectionToDelete] = useState<ExternalDbConnection | null>(null);
|
|
|
|
// 데이터 로딩
|
|
const loadConnections = async () => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
const filter: ExternalDbConnectionFilter = {
|
|
search: searchTerm.trim() || undefined,
|
|
db_type: dbTypeFilter === "ALL" ? undefined : dbTypeFilter,
|
|
is_active: activeStatusFilter === "ALL" ? undefined : activeStatusFilter,
|
|
};
|
|
|
|
const data = await ExternalDbConnectionAPI.getConnections(filter);
|
|
setConnections(data);
|
|
} catch (error) {
|
|
console.error("연결 목록 로딩 오류:", error);
|
|
toast({
|
|
title: "오류",
|
|
description: "연결 목록을 불러오는데 실패했습니다.",
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// 지원되는 DB 타입 로딩
|
|
const loadSupportedDbTypes = async () => {
|
|
try {
|
|
const types = await ExternalDbConnectionAPI.getSupportedTypes();
|
|
setSupportedDbTypes([{ value: "ALL", label: "전체" }, ...types]);
|
|
} catch (error) {
|
|
console.error("지원 DB 타입 로딩 오류:", error);
|
|
// 실패 시 기본값 사용
|
|
setSupportedDbTypes([
|
|
{ value: "ALL", label: "전체" },
|
|
{ value: "mysql", label: "MySQL" },
|
|
{ value: "postgresql", label: "PostgreSQL" },
|
|
{ value: "oracle", label: "Oracle" },
|
|
{ value: "mssql", label: "SQL Server" },
|
|
{ value: "sqlite", label: "SQLite" },
|
|
]);
|
|
}
|
|
};
|
|
|
|
// 초기 데이터 로딩
|
|
useEffect(() => {
|
|
loadConnections();
|
|
loadSupportedDbTypes();
|
|
}, []);
|
|
|
|
// 필터 변경 시 데이터 재로딩
|
|
useEffect(() => {
|
|
loadConnections();
|
|
}, [searchTerm, dbTypeFilter, activeStatusFilter]);
|
|
|
|
// 새 연결 추가
|
|
const handleAddConnection = () => {
|
|
setEditingConnection(undefined);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
// 연결 편집
|
|
const handleEditConnection = (connection: ExternalDbConnection) => {
|
|
setEditingConnection(connection);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
// 연결 삭제 확인 다이얼로그 열기
|
|
const handleDeleteConnection = (connection: ExternalDbConnection) => {
|
|
setConnectionToDelete(connection);
|
|
setDeleteDialogOpen(true);
|
|
};
|
|
|
|
// 연결 삭제 실행
|
|
const confirmDeleteConnection = async () => {
|
|
if (!connectionToDelete?.id) return;
|
|
|
|
try {
|
|
await ExternalDbConnectionAPI.deleteConnection(connectionToDelete.id);
|
|
toast({
|
|
title: "성공",
|
|
description: "연결이 삭제되었습니다.",
|
|
});
|
|
loadConnections();
|
|
} catch (error) {
|
|
console.error("연결 삭제 오류:", error);
|
|
toast({
|
|
title: "오류",
|
|
description: error instanceof Error ? error.message : "연결 삭제에 실패했습니다.",
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
setDeleteDialogOpen(false);
|
|
setConnectionToDelete(null);
|
|
}
|
|
};
|
|
|
|
// 연결 삭제 취소
|
|
const cancelDeleteConnection = () => {
|
|
setDeleteDialogOpen(false);
|
|
setConnectionToDelete(null);
|
|
};
|
|
|
|
// 모달 저장 처리
|
|
const handleModalSave = () => {
|
|
setIsModalOpen(false);
|
|
setEditingConnection(undefined);
|
|
loadConnections();
|
|
};
|
|
|
|
// 모달 취소 처리
|
|
const handleModalCancel = () => {
|
|
setIsModalOpen(false);
|
|
setEditingConnection(undefined);
|
|
};
|
|
|
|
return (
|
|
<div className="container mx-auto p-6">
|
|
<div className="mb-6">
|
|
<h1 className="mb-2 text-2xl font-bold text-gray-900">외부 커넥션 관리</h1>
|
|
<p className="text-gray-600">외부 데이터베이스 연결 정보를 관리합니다.</p>
|
|
</div>
|
|
|
|
{/* 검색 및 필터 */}
|
|
<Card className="mb-6">
|
|
<CardContent className="pt-6">
|
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
<div className="flex flex-col gap-4 md:flex-row md:items-center">
|
|
{/* 검색 */}
|
|
<div className="relative">
|
|
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
|
<Input
|
|
placeholder="연결명 또는 설명으로 검색..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="w-64 pl-10"
|
|
/>
|
|
</div>
|
|
|
|
{/* DB 타입 필터 */}
|
|
<Select value={dbTypeFilter} onValueChange={setDbTypeFilter}>
|
|
<SelectTrigger className="w-40">
|
|
<SelectValue placeholder="DB 타입" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{supportedDbTypes.map((type) => (
|
|
<SelectItem key={type.value} value={type.value}>
|
|
{type.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{/* 활성 상태 필터 */}
|
|
<Select value={activeStatusFilter} onValueChange={setActiveStatusFilter}>
|
|
<SelectTrigger className="w-32">
|
|
<SelectValue placeholder="상태" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{ACTIVE_STATUS_OPTIONS.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 추가 버튼 */}
|
|
<Button onClick={handleAddConnection} className="shrink-0">
|
|
<Plus className="mr-2 h-4 w-4" />새 연결 추가
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 연결 목록 */}
|
|
{loading ? (
|
|
<div className="flex h-64 items-center justify-center">
|
|
<div className="text-gray-500">로딩 중...</div>
|
|
</div>
|
|
) : connections.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="py-8 text-center text-gray-500">
|
|
<Database className="mx-auto mb-4 h-12 w-12 text-gray-400" />
|
|
<p className="mb-2 text-lg font-medium">등록된 연결이 없습니다</p>
|
|
<p className="mb-4 text-sm text-gray-400">새 외부 데이터베이스 연결을 추가해보세요.</p>
|
|
<Button onClick={handleAddConnection}>
|
|
<Plus className="mr-2 h-4 w-4" />첫 번째 연결 추가
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<Card>
|
|
<CardContent className="p-0">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-[200px]">연결명</TableHead>
|
|
<TableHead className="w-[120px]">DB 타입</TableHead>
|
|
<TableHead className="w-[200px]">호스트:포트</TableHead>
|
|
<TableHead className="w-[150px]">데이터베이스</TableHead>
|
|
<TableHead className="w-[120px]">사용자</TableHead>
|
|
<TableHead className="w-[80px]">상태</TableHead>
|
|
<TableHead className="w-[100px]">생성일</TableHead>
|
|
<TableHead className="w-[120px] text-right">작업</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{connections.map((connection) => (
|
|
<TableRow key={connection.id} className="hover:bg-gray-50">
|
|
<TableCell>
|
|
<div>
|
|
<div className="font-medium">{connection.connection_name}</div>
|
|
{connection.description && (
|
|
<div className="max-w-[180px] truncate text-sm text-gray-500" title={connection.description}>
|
|
{connection.description}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant="outline" className="text-xs">
|
|
{DB_TYPE_LABELS[connection.db_type] || connection.db_type}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="font-mono text-sm">
|
|
{connection.host}:{connection.port}
|
|
</TableCell>
|
|
<TableCell className="font-mono text-sm">{connection.database_name}</TableCell>
|
|
<TableCell className="font-mono text-sm">{connection.username}</TableCell>
|
|
<TableCell>
|
|
<Badge variant={connection.is_active === "Y" ? "default" : "secondary"} className="text-xs">
|
|
{connection.is_active === "Y" ? "활성" : "비활성"}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-sm">
|
|
{connection.created_date ? new Date(connection.created_date).toLocaleDateString() : "N/A"}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<div className="flex justify-end gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleEditConnection(connection)}
|
|
className="h-8 w-8 p-0"
|
|
>
|
|
<Pencil className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleDeleteConnection(connection)}
|
|
className="h-8 w-8 p-0 text-red-600 hover:bg-red-50 hover:text-red-700"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* 연결 설정 모달 */}
|
|
{isModalOpen && (
|
|
<ExternalDbConnectionModal
|
|
isOpen={isModalOpen}
|
|
onClose={handleModalCancel}
|
|
onSave={handleModalSave}
|
|
connection={editingConnection}
|
|
supportedDbTypes={supportedDbTypes.filter((type) => type.value !== "ALL")}
|
|
/>
|
|
)}
|
|
|
|
{/* 삭제 확인 다이얼로그 */}
|
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>연결 삭제 확인</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
"{connectionToDelete?.connection_name}" 연결을 삭제하시겠습니까?
|
|
<br />
|
|
<span className="font-medium text-red-600">이 작업은 되돌릴 수 없습니다.</span>
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel onClick={cancelDeleteConnection}>취소</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={confirmDeleteConnection}
|
|
className="bg-red-600 text-white hover:bg-red-700 focus:ring-red-600"
|
|
>
|
|
삭제
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
);
|
|
}
|