2025-09-18 09:32:50 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from "react";
|
2025-09-19 12:15:14 +09:00
|
|
|
import { Eye, EyeOff, ChevronDown, ChevronRight } from "lucide-react";
|
2025-09-18 09:32:50 +09:00
|
|
|
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";
|
2025-09-19 12:15:14 +09:00
|
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
|
|
|
|
import { useToast } from "@/hooks/use-toast";
|
2025-09-18 09:32:50 +09:00
|
|
|
import {
|
|
|
|
|
ExternalDbConnectionAPI,
|
|
|
|
|
ExternalDbConnection,
|
2025-09-19 12:15:14 +09:00
|
|
|
ConnectionTestRequest,
|
|
|
|
|
ConnectionTestResult,
|
2025-09-18 09:32:50 +09:00
|
|
|
} from "@/lib/api/externalDbConnection";
|
|
|
|
|
|
|
|
|
|
interface ExternalDbConnectionModalProps {
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
onSave: () => void;
|
2025-09-19 12:15:14 +09:00
|
|
|
connection?: ExternalDbConnection;
|
|
|
|
|
supportedDbTypes: Array<{ value: string; label: string }>;
|
2025-09-18 09:32:50 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-19 12:15:14 +09:00
|
|
|
// 기본 포트 설정
|
|
|
|
|
const DEFAULT_PORTS: Record<string, number> = {
|
|
|
|
|
mysql: 3306,
|
|
|
|
|
postgresql: 5432,
|
|
|
|
|
oracle: 1521,
|
|
|
|
|
mssql: 1433,
|
|
|
|
|
sqlite: 0, // SQLite는 파일 기반이므로 포트 없음
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const ExternalDbConnectionModal: React.FC<ExternalDbConnectionModalProps> = ({
|
2025-09-18 09:32:50 +09:00
|
|
|
isOpen,
|
|
|
|
|
onClose,
|
|
|
|
|
onSave,
|
2025-09-19 12:15:14 +09:00
|
|
|
connection,
|
|
|
|
|
supportedDbTypes,
|
|
|
|
|
}) => {
|
|
|
|
|
const { toast } = useToast();
|
|
|
|
|
|
|
|
|
|
// 상태 관리
|
|
|
|
|
const [loading, setSaving] = useState(false);
|
|
|
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
|
|
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
|
|
|
const [testingConnection, setTestingConnection] = useState(false);
|
|
|
|
|
const [testResult, setTestResult] = useState<ConnectionTestResult | null>(null);
|
|
|
|
|
|
|
|
|
|
// 폼 데이터
|
|
|
|
|
const [formData, setFormData] = useState<ExternalDbConnection>({
|
2025-09-18 09:32:50 +09:00
|
|
|
connection_name: "",
|
|
|
|
|
description: "",
|
2025-09-19 12:15:14 +09:00
|
|
|
db_type: "postgresql",
|
|
|
|
|
host: "localhost",
|
|
|
|
|
port: DEFAULT_PORTS.postgresql,
|
2025-09-18 09:32:50 +09:00
|
|
|
database_name: "",
|
|
|
|
|
username: "",
|
|
|
|
|
password: "",
|
|
|
|
|
connection_timeout: 30,
|
|
|
|
|
query_timeout: 60,
|
|
|
|
|
max_connections: 10,
|
|
|
|
|
ssl_enabled: "N",
|
|
|
|
|
ssl_cert_path: "",
|
|
|
|
|
company_code: "*",
|
|
|
|
|
is_active: "Y",
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-19 12:15:14 +09:00
|
|
|
// 편집 모드인지 확인
|
|
|
|
|
const isEditMode = !!connection;
|
2025-09-18 09:32:50 +09:00
|
|
|
|
2025-09-19 12:15:14 +09:00
|
|
|
// 연결 정보가 변경될 때 폼 데이터 업데이트
|
2025-09-18 09:32:50 +09:00
|
|
|
useEffect(() => {
|
2025-09-19 12:15:14 +09:00
|
|
|
if (connection) {
|
|
|
|
|
setFormData({
|
|
|
|
|
...connection,
|
|
|
|
|
// 편집 시에는 비밀번호를 빈 문자열로 설정 (보안상 기존 비밀번호는 보여주지 않음)
|
|
|
|
|
password: "",
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// 새 연결 생성 시 기본값 설정
|
|
|
|
|
setFormData({
|
|
|
|
|
connection_name: "",
|
|
|
|
|
description: "",
|
|
|
|
|
db_type: "postgresql",
|
|
|
|
|
host: "localhost",
|
|
|
|
|
port: DEFAULT_PORTS.postgresql,
|
|
|
|
|
database_name: "",
|
|
|
|
|
username: "",
|
|
|
|
|
password: "",
|
|
|
|
|
connection_timeout: 30,
|
|
|
|
|
query_timeout: 60,
|
|
|
|
|
max_connections: 10,
|
|
|
|
|
ssl_enabled: "N",
|
|
|
|
|
ssl_cert_path: "",
|
|
|
|
|
company_code: "*",
|
|
|
|
|
is_active: "Y",
|
|
|
|
|
});
|
2025-09-18 09:32:50 +09:00
|
|
|
}
|
2025-09-19 12:15:14 +09:00
|
|
|
}, [connection]);
|
2025-09-18 09:32:50 +09:00
|
|
|
|
|
|
|
|
// DB 타입 변경 시 기본 포트 설정
|
|
|
|
|
const handleDbTypeChange = (dbType: string) => {
|
2025-09-19 12:15:14 +09:00
|
|
|
setFormData({
|
|
|
|
|
...formData,
|
2025-09-18 09:32:50 +09:00
|
|
|
db_type: dbType as ExternalDbConnection["db_type"],
|
2025-09-19 12:15:14 +09:00
|
|
|
port: DEFAULT_PORTS[dbType] || 5432,
|
|
|
|
|
});
|
2025-09-18 09:32:50 +09:00
|
|
|
};
|
|
|
|
|
|
2025-09-19 12:15:14 +09:00
|
|
|
// 입력값 변경 처리
|
2025-09-18 09:32:50 +09:00
|
|
|
const handleInputChange = (field: keyof ExternalDbConnection, value: string | number) => {
|
2025-09-19 12:15:14 +09:00
|
|
|
setFormData({
|
|
|
|
|
...formData,
|
2025-09-18 09:32:50 +09:00
|
|
|
[field]: value,
|
2025-09-19 12:15:14 +09:00
|
|
|
});
|
2025-09-18 09:32:50 +09:00
|
|
|
};
|
|
|
|
|
|
2025-09-19 12:15:14 +09:00
|
|
|
// 폼 검증
|
|
|
|
|
const validateForm = (): boolean => {
|
|
|
|
|
if (!formData.connection_name.trim()) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "검증 오류",
|
|
|
|
|
description: "연결명을 입력해주세요.",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!formData.host.trim()) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "검증 오류",
|
|
|
|
|
description: "호스트를 입력해주세요.",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!formData.database_name.trim()) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "검증 오류",
|
|
|
|
|
description: "데이터베이스명을 입력해주세요.",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!formData.username.trim()) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "검증 오류",
|
|
|
|
|
description: "사용자명을 입력해주세요.",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isEditMode && !formData.password.trim()) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "검증 오류",
|
|
|
|
|
description: "비밀번호를 입력해주세요.",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 연결 테스트 처리
|
|
|
|
|
const handleTestConnection = async () => {
|
|
|
|
|
// 기본 필수 필드 검증
|
|
|
|
|
if (!formData.host.trim()) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "검증 오류",
|
|
|
|
|
description: "호스트를 입력해주세요.",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!formData.database_name.trim()) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "검증 오류",
|
|
|
|
|
description: "데이터베이스명을 입력해주세요.",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
2025-09-18 09:32:50 +09:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 12:15:14 +09:00
|
|
|
if (!formData.username.trim()) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "검증 오류",
|
|
|
|
|
description: "사용자명을 입력해주세요.",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!formData.password.trim()) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "검증 오류",
|
|
|
|
|
description: "비밀번호를 입력해주세요.",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
2025-09-18 09:32:50 +09:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2025-09-19 12:15:14 +09:00
|
|
|
setTestingConnection(true);
|
|
|
|
|
setTestResult(null);
|
|
|
|
|
|
2025-09-23 12:34:34 +09:00
|
|
|
// 편집 모드일 때만 연결 테스트 실행
|
|
|
|
|
if (!isEditMode || !connection?.id) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "연결 테스트 불가",
|
|
|
|
|
description: "연결을 먼저 저장한 후 테스트할 수 있습니다.",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-23 16:05:30 +09:00
|
|
|
const result = await ExternalDbConnectionAPI.testConnection(connection.id, formData.password);
|
2025-09-19 12:15:14 +09:00
|
|
|
setTestResult(result);
|
2025-09-18 09:32:50 +09:00
|
|
|
|
2025-09-19 12:15:14 +09:00
|
|
|
if (result.success) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "연결 테스트 성공",
|
|
|
|
|
description: result.message,
|
|
|
|
|
});
|
2025-09-18 09:32:50 +09:00
|
|
|
} else {
|
2025-09-19 12:15:14 +09:00
|
|
|
toast({
|
|
|
|
|
title: "연결 테스트 실패",
|
|
|
|
|
description: result.message,
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
2025-09-18 09:32:50 +09:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-09-19 12:15:14 +09:00
|
|
|
console.error("연결 테스트 오류:", error);
|
|
|
|
|
const errorResult: ConnectionTestResult = {
|
|
|
|
|
success: false,
|
|
|
|
|
message: "연결 테스트 중 오류가 발생했습니다.",
|
|
|
|
|
error: {
|
|
|
|
|
code: "TEST_ERROR",
|
|
|
|
|
details: error instanceof Error ? error.message : "알 수 없는 오류",
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
setTestResult(errorResult);
|
|
|
|
|
|
|
|
|
|
toast({
|
|
|
|
|
title: "연결 테스트 오류",
|
|
|
|
|
description: errorResult.message,
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
2025-09-18 09:32:50 +09:00
|
|
|
} finally {
|
2025-09-19 12:15:14 +09:00
|
|
|
setTestingConnection(false);
|
2025-09-18 09:32:50 +09:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-19 12:15:14 +09:00
|
|
|
// 저장 처리
|
|
|
|
|
const handleSave = async () => {
|
|
|
|
|
if (!validateForm()) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
setSaving(true);
|
|
|
|
|
|
|
|
|
|
// 편집 모드에서 비밀번호가 비어있으면 기존 비밀번호 유지
|
|
|
|
|
let dataToSave = { ...formData };
|
|
|
|
|
if (isEditMode && !dataToSave.password.trim()) {
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
|
const { password, ...dataWithoutPassword } = dataToSave;
|
|
|
|
|
dataToSave = dataWithoutPassword as ExternalDbConnection;
|
|
|
|
|
}
|
2025-09-18 09:32:50 +09:00
|
|
|
|
2025-09-19 12:15:14 +09:00
|
|
|
if (isEditMode && connection?.id) {
|
|
|
|
|
await ExternalDbConnectionAPI.updateConnection(connection.id, dataToSave);
|
|
|
|
|
toast({
|
|
|
|
|
title: "성공",
|
|
|
|
|
description: "연결 정보가 수정되었습니다.",
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
await ExternalDbConnectionAPI.createConnection(dataToSave);
|
|
|
|
|
toast({
|
|
|
|
|
title: "성공",
|
|
|
|
|
description: "새 연결이 생성되었습니다.",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onSave();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("연결 저장 오류:", error);
|
|
|
|
|
toast({
|
|
|
|
|
title: "오류",
|
|
|
|
|
description: error instanceof Error ? error.message : "연결 저장에 실패했습니다.",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
setSaving(false);
|
|
|
|
|
}
|
2025-09-18 09:32:50 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2025-09-19 12:15:14 +09:00
|
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
2025-10-22 14:52:13 +09:00
|
|
|
<DialogContent className="max-h-[90vh] max-w-[95vw] overflow-y-auto sm:max-w-2xl">
|
2025-09-18 09:32:50 +09:00
|
|
|
<DialogHeader>
|
2025-10-22 14:52:13 +09:00
|
|
|
<DialogTitle className="text-base sm:text-lg">{isEditMode ? "연결 정보 수정" : "새 외부 DB 연결 추가"}</DialogTitle>
|
2025-09-18 09:32:50 +09:00
|
|
|
</DialogHeader>
|
|
|
|
|
|
2025-10-22 14:52:13 +09:00
|
|
|
<div className="space-y-3 sm:space-y-4">
|
2025-09-18 09:32:50 +09:00
|
|
|
{/* 기본 정보 */}
|
2025-10-22 14:52:13 +09:00
|
|
|
<div className="space-y-3 sm:space-y-4">
|
|
|
|
|
<h3 className="text-sm font-semibold sm:text-base">기본 정보</h3>
|
2025-09-18 09:32:50 +09:00
|
|
|
|
2025-10-22 14:52:13 +09:00
|
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
|
2025-09-18 09:32:50 +09:00
|
|
|
<div>
|
2025-10-22 14:52:13 +09:00
|
|
|
<Label htmlFor="connection_name" className="text-xs sm:text-sm">
|
|
|
|
|
연결명 <span className="text-destructive">*</span>
|
|
|
|
|
</Label>
|
2025-09-18 09:32:50 +09:00
|
|
|
<Input
|
|
|
|
|
id="connection_name"
|
2025-09-19 12:15:14 +09:00
|
|
|
value={formData.connection_name}
|
2025-09-18 09:32:50 +09:00
|
|
|
onChange={(e) => handleInputChange("connection_name", e.target.value)}
|
2025-09-19 12:15:14 +09:00
|
|
|
placeholder="예: 운영 DB"
|
2025-10-22 14:52:13 +09:00
|
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
2025-09-18 09:32:50 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
2025-09-19 12:15:14 +09:00
|
|
|
|
2025-09-18 09:32:50 +09:00
|
|
|
<div>
|
2025-10-22 14:52:13 +09:00
|
|
|
<Label htmlFor="db_type" className="text-xs sm:text-sm">
|
|
|
|
|
DB 타입 <span className="text-destructive">*</span>
|
|
|
|
|
</Label>
|
2025-09-19 12:15:14 +09:00
|
|
|
<Select value={formData.db_type} onValueChange={handleDbTypeChange}>
|
2025-10-22 14:52:13 +09:00
|
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
2025-09-18 09:32:50 +09:00
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
2025-09-19 12:15:14 +09:00
|
|
|
{supportedDbTypes.map((type) => (
|
|
|
|
|
<SelectItem key={type.value} value={type.value}>
|
|
|
|
|
{type.label}
|
2025-09-18 09:32:50 +09:00
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
2025-10-22 14:52:13 +09:00
|
|
|
<Label htmlFor="description" className="text-xs sm:text-sm">
|
|
|
|
|
설명
|
|
|
|
|
</Label>
|
2025-09-18 09:32:50 +09:00
|
|
|
<Textarea
|
|
|
|
|
id="description"
|
|
|
|
|
value={formData.description || ""}
|
|
|
|
|
onChange={(e) => handleInputChange("description", e.target.value)}
|
|
|
|
|
placeholder="연결에 대한 설명을 입력하세요"
|
|
|
|
|
rows={2}
|
2025-10-22 14:52:13 +09:00
|
|
|
className="text-xs sm:text-sm"
|
2025-09-18 09:32:50 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 연결 정보 */}
|
2025-10-22 14:52:13 +09:00
|
|
|
<div className="space-y-3 sm:space-y-4">
|
|
|
|
|
<h3 className="text-sm font-semibold sm:text-base">연결 정보</h3>
|
2025-09-18 09:32:50 +09:00
|
|
|
|
2025-10-22 14:52:13 +09:00
|
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
|
2025-09-19 12:15:14 +09:00
|
|
|
<div>
|
2025-10-22 14:52:13 +09:00
|
|
|
<Label htmlFor="host" className="text-xs sm:text-sm">
|
|
|
|
|
호스트 <span className="text-destructive">*</span>
|
|
|
|
|
</Label>
|
2025-09-18 09:32:50 +09:00
|
|
|
<Input
|
|
|
|
|
id="host"
|
2025-09-19 12:15:14 +09:00
|
|
|
value={formData.host}
|
2025-09-18 09:32:50 +09:00
|
|
|
onChange={(e) => handleInputChange("host", e.target.value)}
|
2025-09-19 12:15:14 +09:00
|
|
|
placeholder="localhost"
|
2025-10-22 14:52:13 +09:00
|
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
2025-09-18 09:32:50 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
2025-09-19 12:15:14 +09:00
|
|
|
|
2025-09-18 09:32:50 +09:00
|
|
|
<div>
|
2025-10-22 14:52:13 +09:00
|
|
|
<Label htmlFor="port" className="text-xs sm:text-sm">
|
|
|
|
|
포트 <span className="text-destructive">*</span>
|
|
|
|
|
</Label>
|
2025-09-18 09:32:50 +09:00
|
|
|
<Input
|
|
|
|
|
id="port"
|
|
|
|
|
type="number"
|
2025-09-19 12:15:14 +09:00
|
|
|
value={formData.port}
|
2025-09-18 09:32:50 +09:00
|
|
|
onChange={(e) => handleInputChange("port", parseInt(e.target.value) || 0)}
|
2025-09-19 12:15:14 +09:00
|
|
|
placeholder="5432"
|
2025-10-22 14:52:13 +09:00
|
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
2025-09-18 09:32:50 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
2025-10-22 14:52:13 +09:00
|
|
|
<Label htmlFor="database_name" className="text-xs sm:text-sm">
|
|
|
|
|
데이터베이스명 <span className="text-destructive">*</span>
|
|
|
|
|
</Label>
|
2025-09-18 09:32:50 +09:00
|
|
|
<Input
|
|
|
|
|
id="database_name"
|
2025-09-19 12:15:14 +09:00
|
|
|
value={formData.database_name}
|
2025-09-18 09:32:50 +09:00
|
|
|
onChange={(e) => handleInputChange("database_name", e.target.value)}
|
2025-09-19 12:15:14 +09:00
|
|
|
placeholder="database_name"
|
2025-10-22 14:52:13 +09:00
|
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
2025-09-18 09:32:50 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-10-22 14:52:13 +09:00
|
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
|
2025-09-18 09:32:50 +09:00
|
|
|
<div>
|
2025-10-22 14:52:13 +09:00
|
|
|
<Label htmlFor="username" className="text-xs sm:text-sm">
|
|
|
|
|
사용자명 <span className="text-destructive">*</span>
|
|
|
|
|
</Label>
|
2025-09-18 09:32:50 +09:00
|
|
|
<Input
|
|
|
|
|
id="username"
|
2025-09-19 12:15:14 +09:00
|
|
|
value={formData.username}
|
2025-09-18 09:32:50 +09:00
|
|
|
onChange={(e) => handleInputChange("username", e.target.value)}
|
2025-09-19 12:15:14 +09:00
|
|
|
placeholder="username"
|
2025-10-22 14:52:13 +09:00
|
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
2025-09-18 09:32:50 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
2025-09-19 12:15:14 +09:00
|
|
|
|
2025-09-18 09:32:50 +09:00
|
|
|
<div>
|
2025-10-22 14:52:13 +09:00
|
|
|
<Label htmlFor="password" className="text-xs sm:text-sm">
|
|
|
|
|
비밀번호 {isEditMode ? "(변경 시에만 입력)" : <span className="text-destructive">*</span>}
|
|
|
|
|
</Label>
|
2025-09-18 09:32:50 +09:00
|
|
|
<div className="relative">
|
|
|
|
|
<Input
|
|
|
|
|
id="password"
|
|
|
|
|
type={showPassword ? "text" : "password"}
|
2025-09-19 12:15:14 +09:00
|
|
|
value={formData.password}
|
2025-09-18 09:32:50 +09:00
|
|
|
onChange={(e) => handleInputChange("password", e.target.value)}
|
2025-09-19 12:15:14 +09:00
|
|
|
placeholder={isEditMode ? "변경하지 않으려면 비워두세요" : "password"}
|
2025-10-22 14:52:13 +09:00
|
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
2025-09-18 09:32:50 +09:00
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
2025-10-22 14:52:13 +09:00
|
|
|
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
2025-09-18 09:32:50 +09:00
|
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
|
|
|
>
|
2025-09-19 12:15:14 +09:00
|
|
|
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
2025-09-18 09:32:50 +09:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-09-19 12:15:14 +09:00
|
|
|
{/* 연결 테스트 */}
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={handleTestConnection}
|
|
|
|
|
disabled={
|
|
|
|
|
testingConnection ||
|
|
|
|
|
!formData.host ||
|
|
|
|
|
!formData.database_name ||
|
|
|
|
|
!formData.username ||
|
|
|
|
|
!formData.password
|
|
|
|
|
}
|
|
|
|
|
className="w-32"
|
|
|
|
|
>
|
|
|
|
|
{testingConnection ? "테스트 중..." : "연결 테스트"}
|
|
|
|
|
</Button>
|
|
|
|
|
{testingConnection && <div className="text-sm text-gray-500">연결을 확인하고 있습니다...</div>}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 테스트 결과 표시 */}
|
|
|
|
|
{testResult && (
|
|
|
|
|
<div
|
|
|
|
|
className={`rounded-md border p-3 text-sm ${
|
|
|
|
|
testResult.success
|
|
|
|
|
? "border-green-200 bg-green-50 text-green-800"
|
2025-10-02 14:34:15 +09:00
|
|
|
: "border-destructive/20 bg-destructive/10 text-red-800"
|
2025-09-19 12:15:14 +09:00
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="font-medium">{testResult.success ? "✅ 연결 성공" : "❌ 연결 실패"}</div>
|
|
|
|
|
<div className="mt-1">{testResult.message}</div>
|
|
|
|
|
|
|
|
|
|
{testResult.success && testResult.details && (
|
|
|
|
|
<div className="mt-2 space-y-1 text-xs">
|
|
|
|
|
{testResult.details.response_time && <div>응답 시간: {testResult.details.response_time}ms</div>}
|
|
|
|
|
{testResult.details.server_version && <div>서버 버전: {testResult.details.server_version}</div>}
|
|
|
|
|
{testResult.details.database_size && (
|
|
|
|
|
<div>데이터베이스 크기: {testResult.details.database_size}</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{!testResult.success && testResult.error && (
|
|
|
|
|
<div className="mt-2 text-xs">
|
|
|
|
|
<div>오류 코드: {testResult.error.code}</div>
|
2025-10-02 14:34:15 +09:00
|
|
|
{testResult.error.details && <div className="mt-1 text-destructive">{testResult.error.details}</div>}
|
2025-09-19 12:15:14 +09:00
|
|
|
</div>
|
|
|
|
|
)}
|
2025-09-18 09:32:50 +09:00
|
|
|
</div>
|
2025-09-19 12:15:14 +09:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 고급 설정 */}
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={() => setShowAdvanced(!showAdvanced)}
|
|
|
|
|
className="flex h-auto items-center gap-2 p-0 font-medium"
|
|
|
|
|
>
|
|
|
|
|
{showAdvanced ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
|
|
|
|
고급 설정
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{showAdvanced && (
|
|
|
|
|
<div className="space-y-4 border-l-2 border-gray-200 pl-6">
|
|
|
|
|
<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)}
|
|
|
|
|
/>
|
|
|
|
|
</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)}
|
|
|
|
|
/>
|
|
|
|
|
</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)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-09-18 09:32:50 +09:00
|
|
|
</div>
|
|
|
|
|
|
2025-09-19 12:15:14 +09:00
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<Label htmlFor="ssl_enabled">SSL 사용</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={formData.ssl_enabled || "N"}
|
|
|
|
|
onValueChange={(value) => handleInputChange("ssl_enabled", value)}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="Y">사용</SelectItem>
|
|
|
|
|
<SelectItem value="N">사용 안함</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<Label htmlFor="is_active">활성 상태</Label>
|
|
|
|
|
<Select value={formData.is_active} onValueChange={(value) => handleInputChange("is_active", value)}>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="Y">활성</SelectItem>
|
|
|
|
|
<SelectItem value="N">비활성</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
2025-09-18 09:32:50 +09:00
|
|
|
</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)}
|
2025-09-19 12:15:14 +09:00
|
|
|
placeholder="/path/to/ssl/cert.pem"
|
2025-09-18 09:32:50 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-09-19 12:15:14 +09:00
|
|
|
)}
|
|
|
|
|
</div>
|
2025-09-18 09:32:50 +09:00
|
|
|
</div>
|
|
|
|
|
|
2025-10-22 14:52:13 +09:00
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
|
|
|
>
|
2025-09-18 09:32:50 +09:00
|
|
|
취소
|
|
|
|
|
</Button>
|
2025-10-22 14:52:13 +09:00
|
|
|
<Button onClick={handleSave} disabled={loading} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
|
2025-09-19 12:15:14 +09:00
|
|
|
{loading ? "저장 중..." : isEditMode ? "수정" : "생성"}
|
2025-09-18 09:32:50 +09:00
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
2025-09-19 12:15:14 +09:00
|
|
|
};
|