287 lines
10 KiB
TypeScript
287 lines
10 KiB
TypeScript
/**
|
|
* 커넥션 선택 패널
|
|
* 제어관리에서 FROM/TO 커넥션을 선택할 수 있는 컴포넌트
|
|
*/
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
import { AlertCircle, Database, ExternalLink } from "lucide-react";
|
|
import { getActiveConnections, ConnectionInfo } from "@/lib/api/multiConnection";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
|
|
export interface ConnectionSelectionPanelProps {
|
|
fromConnectionId?: number;
|
|
toConnectionId?: number;
|
|
onFromConnectionChange: (connectionId: number) => void;
|
|
onToConnectionChange: (connectionId: number) => void;
|
|
actionType: "insert" | "update" | "delete";
|
|
// 🆕 자기 자신 테이블 작업 지원
|
|
allowSameConnection?: boolean;
|
|
currentConnectionId?: number; // 현재 메인 DB 커넥션
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export const ConnectionSelectionPanel: React.FC<ConnectionSelectionPanelProps> = ({
|
|
fromConnectionId,
|
|
toConnectionId,
|
|
onFromConnectionChange,
|
|
onToConnectionChange,
|
|
actionType,
|
|
allowSameConnection = true,
|
|
currentConnectionId = 0,
|
|
disabled = false,
|
|
}) => {
|
|
const { toast } = useToast();
|
|
const [availableConnections, setAvailableConnections] = useState<ConnectionInfo[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// 커넥션 목록 로드 (한 번만 실행)
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
|
|
const loadConnections = async () => {
|
|
try {
|
|
setLoading(true);
|
|
console.log("🔍 커넥션 목록 로드 시작...");
|
|
const connections = await getActiveConnections();
|
|
|
|
if (isMounted) {
|
|
console.log("✅ 커넥션 목록 로드 성공:", connections);
|
|
setAvailableConnections(Array.isArray(connections) ? connections : []);
|
|
}
|
|
} catch (error) {
|
|
if (isMounted) {
|
|
console.error("❌ 커넥션 목록 로드 실패:", error);
|
|
console.error("Error details:", {
|
|
message: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
response: error && typeof error === "object" && "response" in error ? error.response : undefined,
|
|
});
|
|
toast({
|
|
title: "커넥션 로드 실패",
|
|
description: `활성 커넥션 목록을 불러오는데 실패했습니다. ${error instanceof Error ? error.message : String(error)}`,
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
} finally {
|
|
if (isMounted) {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
// 컴포넌트가 마운트된 후 1초 딜레이로 API 호출 (Rate Limiting 방지)
|
|
const timeoutId = setTimeout(() => {
|
|
if (isMounted) {
|
|
loadConnections();
|
|
}
|
|
}, 1000);
|
|
|
|
return () => {
|
|
isMounted = false;
|
|
clearTimeout(timeoutId);
|
|
};
|
|
}, []); // 의존성 배열을 빈 배열로 수정
|
|
|
|
// 액션 타입별 라벨 정의
|
|
const getConnectionLabels = () => {
|
|
switch (actionType) {
|
|
case "insert":
|
|
return {
|
|
from: {
|
|
title: "소스 데이터베이스 연결",
|
|
desc: "데이터를 가져올 데이터베이스 연결을 선택하세요",
|
|
},
|
|
to: {
|
|
title: "대상 데이터베이스 연결",
|
|
desc: "데이터를 저장할 데이터베이스 연결을 선택하세요",
|
|
},
|
|
};
|
|
case "update":
|
|
return {
|
|
from: {
|
|
title: "조건 확인 데이터베이스",
|
|
desc: "업데이트 조건을 확인할 데이터베이스 연결을 선택하세요 (자기 자신 가능)",
|
|
},
|
|
to: {
|
|
title: "업데이트 대상 데이터베이스",
|
|
desc: "데이터를 업데이트할 데이터베이스 연결을 선택하세요 (자기 자신 가능)",
|
|
},
|
|
};
|
|
case "delete":
|
|
return {
|
|
from: {
|
|
title: "조건 확인 데이터베이스",
|
|
desc: "삭제 조건을 확인할 데이터베이스 연결을 선택하세요 (자기 자신 가능)",
|
|
},
|
|
to: {
|
|
title: "삭제 대상 데이터베이스",
|
|
desc: "데이터를 삭제할 데이터베이스 연결을 선택하세요 (자기 자신 가능)",
|
|
},
|
|
};
|
|
}
|
|
};
|
|
|
|
// 🆕 자기 자신 테이블 작업 시 경고 메시지
|
|
const getSameConnectionWarning = () => {
|
|
if (fromConnectionId === toConnectionId && fromConnectionId !== undefined) {
|
|
switch (actionType) {
|
|
case "update":
|
|
return "⚠️ 같은 데이터베이스에서 UPDATE 작업을 수행합니다. 조건을 신중히 설정하세요.";
|
|
case "delete":
|
|
return "🚨 같은 데이터베이스에서 DELETE 작업을 수행합니다. 데이터 손실에 주의하세요.";
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const labels = getConnectionLabels();
|
|
const warningMessage = getSameConnectionWarning();
|
|
|
|
// 커넥션 선택 핸들러
|
|
const handleFromConnectionChange = (value: string) => {
|
|
const connectionId = parseInt(value);
|
|
onFromConnectionChange(connectionId);
|
|
};
|
|
|
|
const handleToConnectionChange = (value: string) => {
|
|
const connectionId = parseInt(value);
|
|
onToConnectionChange(connectionId);
|
|
};
|
|
|
|
// 커넥션 아이템 렌더링
|
|
const renderConnectionItem = (connection: ConnectionInfo) => (
|
|
<SelectItem key={connection.id} value={connection.id.toString()}>
|
|
<div className="flex items-center gap-2">
|
|
{connection.id === 0 ? (
|
|
<Badge variant="default" className="text-xs">
|
|
<Database className="mr-1 h-3 w-3" />
|
|
현재 DB
|
|
</Badge>
|
|
) : (
|
|
<Badge variant="outline" className="text-xs">
|
|
<ExternalLink className="mr-1 h-3 w-3" />
|
|
{connection.db_type?.toUpperCase()}
|
|
</Badge>
|
|
)}
|
|
<span className="truncate">{connection.connection_name}</span>
|
|
</div>
|
|
</SelectItem>
|
|
);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<div className="h-4 w-4 animate-pulse rounded bg-gray-300" />
|
|
<div className="h-4 w-32 animate-pulse rounded bg-gray-300" />
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-10 w-full animate-pulse rounded bg-gray-200" />
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<div className="h-4 w-4 animate-pulse rounded bg-gray-300" />
|
|
<div className="h-4 w-32 animate-pulse rounded bg-gray-300" />
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-10 w-full animate-pulse rounded bg-gray-200" />
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-6">
|
|
{/* FROM 커넥션 선택 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Database className="h-4 w-4" />
|
|
{labels.from.title}
|
|
</CardTitle>
|
|
<CardDescription>{labels.from.desc}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Select
|
|
value={fromConnectionId?.toString() || ""}
|
|
onValueChange={handleFromConnectionChange}
|
|
disabled={disabled}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="커넥션을 선택하세요" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Array.isArray(availableConnections) ? availableConnections.map(renderConnectionItem) : []}
|
|
</SelectContent>
|
|
</Select>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* TO 커넥션 선택 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Database className="h-4 w-4" />
|
|
{labels.to.title}
|
|
</CardTitle>
|
|
<CardDescription>{labels.to.desc}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Select
|
|
value={toConnectionId?.toString() || ""}
|
|
onValueChange={handleToConnectionChange}
|
|
disabled={disabled}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="커넥션을 선택하세요" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Array.isArray(availableConnections) ? availableConnections.map(renderConnectionItem) : []}
|
|
</SelectContent>
|
|
</Select>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* 🆕 자기 자신 테이블 작업 시 경고 */}
|
|
{warningMessage && (
|
|
<Alert variant={actionType === "delete" ? "destructive" : "default"}>
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertTitle>주의사항</AlertTitle>
|
|
<AlertDescription>{warningMessage}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{/* 연결 상태 표시 */}
|
|
{fromConnectionId !== undefined && toConnectionId !== undefined && (
|
|
<div className="text-muted-foreground text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<span>연결 상태:</span>
|
|
<Badge variant="secondary">
|
|
{availableConnections.find((c) => c.id === fromConnectionId)?.connection_name || "Unknown"}
|
|
</Badge>
|
|
<span>→</span>
|
|
<Badge variant="secondary">
|
|
{availableConnections.find((c) => c.id === toConnectionId)?.connection_name || "Unknown"}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|