db 정보 조회
This commit is contained in:
432
frontend/components/admin/ExternalDbConnectionModal.tsx
Normal file
432
frontend/components/admin/ExternalDbConnectionModal.tsx
Normal file
@@ -0,0 +1,432 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Database, ChevronDown, ChevronRight, Eye, EyeOff } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
ExternalDbConnectionAPI,
|
||||
ExternalDbConnection,
|
||||
DB_TYPE_OPTIONS,
|
||||
DB_TYPE_DEFAULTS,
|
||||
} from "@/lib/api/externalDbConnection";
|
||||
|
||||
interface ExternalDbConnectionModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: () => void;
|
||||
editingConnection?: ExternalDbConnection | null;
|
||||
}
|
||||
|
||||
export function ExternalDbConnectionModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
editingConnection,
|
||||
}: ExternalDbConnectionModalProps) {
|
||||
const [formData, setFormData] = useState<Partial<ExternalDbConnection>>({
|
||||
connection_name: "",
|
||||
description: "",
|
||||
db_type: "mysql",
|
||||
host: "",
|
||||
port: 3306,
|
||||
database_name: "",
|
||||
username: "",
|
||||
password: "",
|
||||
connection_timeout: 30,
|
||||
query_timeout: 60,
|
||||
max_connections: 10,
|
||||
ssl_enabled: "N",
|
||||
ssl_cert_path: "",
|
||||
company_code: "*",
|
||||
is_active: "Y",
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
// 편집 모드일 때 기존 데이터 로드
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (editingConnection) {
|
||||
setFormData({
|
||||
...editingConnection,
|
||||
password: "", // 보안상 비밀번호는 빈 값으로 시작
|
||||
});
|
||||
setShowAdvancedSettings(true); // 편집 시 고급 설정 펼치기
|
||||
} else {
|
||||
// 새 연결 생성 시 기본값 설정
|
||||
setFormData({
|
||||
connection_name: "",
|
||||
description: "",
|
||||
db_type: "mysql",
|
||||
host: "",
|
||||
port: 3306,
|
||||
database_name: "",
|
||||
username: "",
|
||||
password: "",
|
||||
connection_timeout: 30,
|
||||
query_timeout: 60,
|
||||
max_connections: 10,
|
||||
ssl_enabled: "N",
|
||||
ssl_cert_path: "",
|
||||
company_code: "*",
|
||||
is_active: "Y",
|
||||
});
|
||||
setShowAdvancedSettings(false);
|
||||
}
|
||||
setShowPassword(false);
|
||||
}
|
||||
}, [isOpen, editingConnection]);
|
||||
|
||||
// DB 타입 변경 시 기본 포트 설정
|
||||
const handleDbTypeChange = (dbType: string) => {
|
||||
const defaultPort = DB_TYPE_DEFAULTS[dbType as keyof typeof DB_TYPE_DEFAULTS]?.port || 3306;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
db_type: dbType as ExternalDbConnection["db_type"],
|
||||
port: defaultPort,
|
||||
}));
|
||||
};
|
||||
|
||||
// 폼 데이터 변경 핸들러
|
||||
const handleInputChange = (field: keyof ExternalDbConnection, value: string | number) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
// 저장
|
||||
const handleSave = async () => {
|
||||
// 필수 필드 검증
|
||||
if (
|
||||
!formData.connection_name ||
|
||||
!formData.db_type ||
|
||||
!formData.host ||
|
||||
!formData.port ||
|
||||
!formData.database_name ||
|
||||
!formData.username
|
||||
) {
|
||||
toast.error("필수 필드를 모두 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 편집 모드에서 비밀번호가 비어있으면 기존 비밀번호 유지
|
||||
if (editingConnection && !formData.password) {
|
||||
formData.password = "***ENCRYPTED***"; // 서버에서 기존 비밀번호 유지하도록 표시
|
||||
} else if (!editingConnection && !formData.password) {
|
||||
toast.error("새 연결 생성 시 비밀번호는 필수입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const connectionData = {
|
||||
...formData,
|
||||
port: Number(formData.port),
|
||||
connection_timeout: Number(formData.connection_timeout),
|
||||
query_timeout: Number(formData.query_timeout),
|
||||
max_connections: Number(formData.max_connections),
|
||||
} as ExternalDbConnection;
|
||||
|
||||
let response;
|
||||
if (editingConnection?.id) {
|
||||
response = await ExternalDbConnectionAPI.updateConnection(editingConnection.id, connectionData);
|
||||
} else {
|
||||
response = await ExternalDbConnectionAPI.createConnection(connectionData);
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
toast.success(editingConnection ? "연결이 수정되었습니다." : "연결이 생성되었습니다.");
|
||||
onSave();
|
||||
} else {
|
||||
toast.error(response.message || "저장 중 오류가 발생했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("저장 오류:", error);
|
||||
toast.error("저장 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 취소
|
||||
const handleCancel = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
// 저장 버튼 비활성화 조건
|
||||
const isSaveDisabled = () => {
|
||||
return (
|
||||
loading ||
|
||||
!formData.connection_name ||
|
||||
!formData.host ||
|
||||
!formData.port ||
|
||||
!formData.database_name ||
|
||||
!formData.username ||
|
||||
(!editingConnection && !formData.password)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleCancel}>
|
||||
<DialogContent className="max-h-[90vh] max-w-2xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5" />
|
||||
{editingConnection ? "외부 DB 연결 수정" : "새 외부 DB 연결 추가"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 기본 정보 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">기본 정보</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="connection_name">연결명 *</Label>
|
||||
<Input
|
||||
id="connection_name"
|
||||
value={formData.connection_name || ""}
|
||||
onChange={(e) => handleInputChange("connection_name", e.target.value)}
|
||||
placeholder="예: 영업팀 MySQL"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="db_type">DB 타입 *</Label>
|
||||
<Select value={formData.db_type || "mysql"} onValueChange={handleDbTypeChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DB_TYPE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description">설명</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={formData.description || ""}
|
||||
onChange={(e) => handleInputChange("description", e.target.value)}
|
||||
placeholder="연결에 대한 설명을 입력하세요"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 연결 정보 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">연결 정보</h3>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2">
|
||||
<Label htmlFor="host">호스트 주소 *</Label>
|
||||
<Input
|
||||
id="host"
|
||||
value={formData.host || ""}
|
||||
onChange={(e) => handleInputChange("host", e.target.value)}
|
||||
placeholder="예: localhost, db.company.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="port">포트 *</Label>
|
||||
<Input
|
||||
id="port"
|
||||
type="number"
|
||||
value={formData.port || ""}
|
||||
onChange={(e) => handleInputChange("port", parseInt(e.target.value) || 0)}
|
||||
min={1}
|
||||
max={65535}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="database_name">데이터베이스명 *</Label>
|
||||
<Input
|
||||
id="database_name"
|
||||
value={formData.database_name || ""}
|
||||
onChange={(e) => handleInputChange("database_name", e.target.value)}
|
||||
placeholder="예: sales_db, production"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="username">사용자명 *</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={formData.username || ""}
|
||||
onChange={(e) => handleInputChange("username", e.target.value)}
|
||||
placeholder="DB 사용자명"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="password">비밀번호 {!editingConnection && "*"}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={formData.password || ""}
|
||||
onChange={(e) => handleInputChange("password", e.target.value)}
|
||||
placeholder={editingConnection ? "변경하려면 입력하세요" : "DB 비밀번호"}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute top-1/2 right-2 h-8 w-8 -translate-y-1/2 p-0"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 고급 설정 (접기/펼치기) */}
|
||||
<Collapsible open={showAdvancedSettings} onOpenChange={setShowAdvancedSettings}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" className="flex w-full justify-between p-0">
|
||||
<h3 className="text-lg font-medium">고급 설정</h3>
|
||||
{showAdvancedSettings ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-4 pt-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="connection_timeout">연결 타임아웃 (초)</Label>
|
||||
<Input
|
||||
id="connection_timeout"
|
||||
type="number"
|
||||
value={formData.connection_timeout || 30}
|
||||
onChange={(e) => handleInputChange("connection_timeout", parseInt(e.target.value) || 30)}
|
||||
min={1}
|
||||
max={300}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="query_timeout">쿼리 타임아웃 (초)</Label>
|
||||
<Input
|
||||
id="query_timeout"
|
||||
type="number"
|
||||
value={formData.query_timeout || 60}
|
||||
onChange={(e) => handleInputChange("query_timeout", parseInt(e.target.value) || 60)}
|
||||
min={1}
|
||||
max={3600}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="max_connections">최대 연결 수</Label>
|
||||
<Input
|
||||
id="max_connections"
|
||||
type="number"
|
||||
value={formData.max_connections || 10}
|
||||
onChange={(e) => handleInputChange("max_connections", parseInt(e.target.value) || 10)}
|
||||
min={1}
|
||||
max={100}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="ssl_enabled"
|
||||
checked={formData.ssl_enabled === "Y"}
|
||||
onCheckedChange={(checked) => handleInputChange("ssl_enabled", checked ? "Y" : "N")}
|
||||
/>
|
||||
<Label htmlFor="ssl_enabled">SSL 사용</Label>
|
||||
</div>
|
||||
|
||||
{formData.ssl_enabled === "Y" && (
|
||||
<div>
|
||||
<Label htmlFor="ssl_cert_path">SSL 인증서 경로</Label>
|
||||
<Input
|
||||
id="ssl_cert_path"
|
||||
value={formData.ssl_cert_path || ""}
|
||||
onChange={(e) => handleInputChange("ssl_cert_path", e.target.value)}
|
||||
placeholder="/path/to/certificate.pem"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="company_code">회사 코드</Label>
|
||||
<Input
|
||||
id="company_code"
|
||||
value={formData.company_code || "*"}
|
||||
onChange={(e) => handleInputChange("company_code", e.target.value)}
|
||||
placeholder="회사 코드 (기본: *)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="is_active">상태</Label>
|
||||
<Select
|
||||
value={formData.is_active || "Y"}
|
||||
onValueChange={(value) => handleInputChange("is_active", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Y">활성</SelectItem>
|
||||
<SelectItem value="N">비활성</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* 편집 모드일 때 정보 표시 */}
|
||||
{editingConnection && (
|
||||
<div className="bg-muted rounded-lg p-4 text-sm">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Badge variant="secondary">편집 모드</Badge>
|
||||
<span className="text-muted-foreground">
|
||||
생성일:{" "}
|
||||
{editingConnection.created_date ? new Date(editingConnection.created_date).toLocaleString() : "-"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
비밀번호를 변경하지 않으려면 비워두세요. 기존 비밀번호가 유지됩니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleCancel} disabled={loading}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isSaveDisabled()}>
|
||||
{loading ? "저장 중..." : editingConnection ? "수정" : "생성"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user