"use client"; /** * 외부 DB 소스 노드 속성 편집 */ import { useEffect, useState } from "react"; import { Database, RefreshCw, Table, FileText } from "lucide-react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useFlowEditorStore } from "@/lib/stores/flowEditorStore"; import { getTestedExternalConnections, getExternalTables, getExternalColumns, type ExternalConnection, type ExternalTable, type ExternalColumn, } from "@/lib/api/nodeExternalConnections"; import { toast } from "sonner"; import type { ExternalDBSourceNodeData } from "@/types/node-editor"; interface ExternalDBSourcePropertiesProps { nodeId: string; data: ExternalDBSourceNodeData; } const DB_TYPE_INFO: Record = { postgresql: { label: "PostgreSQL", color: "#336791", icon: "🐘" }, mysql: { label: "MySQL", color: "#4479A1", icon: "🐬" }, oracle: { label: "Oracle", color: "#F80000", icon: "🔴" }, mssql: { label: "MS SQL Server", color: "#CC2927", icon: "🏢" }, mariadb: { label: "MariaDB", color: "#003545", icon: "🌊" }, }; export function ExternalDBSourceProperties({ nodeId, data }: ExternalDBSourcePropertiesProps) { const { updateNode, getExternalConnectionsCache, setExternalConnectionsCache } = useFlowEditorStore(); const [displayName, setDisplayName] = useState(data.displayName || data.connectionName); const [selectedConnectionId, setSelectedConnectionId] = useState(data.connectionId); const [tableName, setTableName] = useState(data.tableName); const [schema, setSchema] = useState(data.schema || ""); // 🆕 데이터 소스 타입 (기본값: context-data) const [dataSourceType, setDataSourceType] = useState<"context-data" | "table-all">( (data as any).dataSourceType || "context-data" ); const [connections, setConnections] = useState([]); const [tables, setTables] = useState([]); const [columns, setColumns] = useState([]); const [loadingConnections, setLoadingConnections] = useState(false); const [loadingTables, setLoadingTables] = useState(false); const [loadingColumns, setLoadingColumns] = useState(false); const [lastRefreshTime, setLastRefreshTime] = useState(0); // 🔥 마지막 새로고침 시간 const [remainingCooldown, setRemainingCooldown] = useState(0); // 🔥 남은 쿨다운 시간 const selectedConnection = connections.find((conn) => conn.id === selectedConnectionId); const dbInfo = selectedConnection && DB_TYPE_INFO[selectedConnection.db_type] ? DB_TYPE_INFO[selectedConnection.db_type] : { label: selectedConnection ? selectedConnection.db_type.toUpperCase() : "알 수 없음", color: "#666", icon: "💾", }; // 🔥 첫 로드 시에만 커넥션 목록 로드 (전역 캐싱) useEffect(() => { const cachedData = getExternalConnectionsCache(); if (cachedData) { console.log("✅ 캐시된 커넥션 사용:", cachedData.length); setConnections(cachedData); } else { console.log("🔄 API 호출하여 커넥션 로드"); loadConnections(); } }, []); // 커넥션 변경 시 테이블 목록 로드 useEffect(() => { if (selectedConnectionId) { loadTables(); } }, [selectedConnectionId]); // 테이블 변경 시 컬럼 목록 로드 useEffect(() => { if (selectedConnectionId && tableName) { loadColumns(); } }, [selectedConnectionId, tableName]); // 🔥 쿨다운 타이머 (1초마다 업데이트) useEffect(() => { const THROTTLE_DURATION = 10000; // 10초 const timer = setInterval(() => { if (lastRefreshTime > 0) { const elapsed = Date.now() - lastRefreshTime; const remaining = Math.max(0, THROTTLE_DURATION - elapsed); setRemainingCooldown(Math.ceil(remaining / 1000)); } }, 1000); return () => clearInterval(timer); }, [lastRefreshTime]); const loadConnections = async () => { // 🔥 쓰로틀링: 10초 이내 재요청 차단 const THROTTLE_DURATION = 10000; // 10초 const now = Date.now(); if (now - lastRefreshTime < THROTTLE_DURATION) { const remainingSeconds = Math.ceil((THROTTLE_DURATION - (now - lastRefreshTime)) / 1000); toast.warning(`잠시 후 다시 시도해주세요 (${remainingSeconds}초 후)`); return; } setLoadingConnections(true); setLastRefreshTime(now); // 🔥 마지막 실행 시간 기록 try { const data = await getTestedExternalConnections(); setConnections(data); setExternalConnectionsCache(data); // 🔥 전역 캐시에 저장 console.log("✅ 테스트 성공한 커넥션 로드 및 캐싱:", data.length); toast.success(`${data.length}개의 커넥션을 불러왔습니다.`); } catch (error) { console.error("❌ 커넥션 로드 실패:", error); toast.error("외부 DB 연결 목록을 불러올 수 없습니다."); } finally { setLoadingConnections(false); } }; const loadTables = async () => { if (!selectedConnectionId) return; setLoadingTables(true); try { const data = await getExternalTables(selectedConnectionId); setTables(data); console.log("✅ 테이블 목록 로드:", data.length); } catch (error) { console.error("❌ 테이블 로드 실패:", error); toast.error("테이블 목록을 불러올 수 없습니다."); } finally { setLoadingTables(false); } }; const loadColumns = async () => { if (!selectedConnectionId || !tableName) return; setLoadingColumns(true); try { const data = await getExternalColumns(selectedConnectionId, tableName); setColumns(data); console.log("✅ 컬럼 목록 로드:", data.length); // 노드에 outputFields 업데이트 updateNode(nodeId, { outputFields: data.map((col) => ({ name: col.column_name, type: col.data_type, label: col.column_name, })), }); } catch (error) { console.error("❌ 컬럼 로드 실패:", error); toast.error("컬럼 목록을 불러올 수 없습니다."); } finally { setLoadingColumns(false); } }; const handleConnectionChange = (connectionId: string) => { const id = parseInt(connectionId); setSelectedConnectionId(id); setTableName(""); setTables([]); setColumns([]); const connection = connections.find((conn) => conn.id === id); if (connection) { updateNode(nodeId, { connectionId: id, connectionName: connection.connection_name, dbType: connection.db_type, displayName: connection.connection_name, }); } }; const handleTableChange = (newTableName: string) => { setTableName(newTableName); setColumns([]); updateNode(nodeId, { tableName: newTableName, }); }; const handleDisplayNameChange = (newDisplayName: string) => { setDisplayName(newDisplayName); updateNode(nodeId, { displayName: newDisplayName, }); }; /** * 🆕 데이터 소스 타입 변경 핸들러 */ const handleDataSourceTypeChange = (newType: "context-data" | "table-all") => { setDataSourceType(newType); updateNode(nodeId, { dataSourceType: newType, }); console.log(`✅ 데이터 소스 타입 변경: ${newType}`); }; return (
{/* DB 타입 정보 */}
{dbInfo.icon}

{dbInfo.label}

외부 데이터베이스

{/* 연결 선택 */}

외부 DB 연결

{loadingConnections &&

테스트 중... ⏳

} {connections.length === 0 && !loadingConnections && (

⚠️ 테스트에 성공한 커넥션이 없습니다.

)}
handleDisplayNameChange(e.target.value)} className="mt-1" placeholder="노드 표시 이름" />
{/* 테이블 선택 */} {selectedConnectionId && (

테이블 선택

{loadingTables &&

테이블 목록 로딩 중... ⏳

}
)} {/* 🆕 데이터 소스 설정 */} {tableName && (

데이터 소스 설정

{/* 설명 텍스트 */}
{dataSourceType === "context-data" ? ( <>

💡 컨텍스트 데이터 모드

버튼 실행 시 전달된 데이터를 사용합니다.

) : ( <>

📊 테이블 전체 데이터 모드

외부 DB의 **모든 행**을 직접 조회합니다.

⚠️ 대량 데이터 시 성능 주의

)}
)} {/* 컬럼 정보 */} {columns.length > 0 && (

출력 필드 ({columns.length}개)

{loadingColumns ? (

컬럼 목록 로딩 중... ⏳

) : (
{columns.map((col, index) => (
{col.column_name} {col.data_type}
))}
)}
)}
💡 외부 DB 연결은 "외부 DB 연결 관리" 메뉴에서 미리 설정해야 합니다.
); }