From 37e018b33c0f31dc9be2c553b92853dfb4322320 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 2 Oct 2025 16:43:40 +0900 Subject: [PATCH] =?UTF-8?q?=EC=99=B8=EB=B6=80=20db=EB=85=B8=EB=93=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dataflow/node-external-connections.ts | 231 ++++++++++++ backend-node/src/routes/dataflowRoutes.ts | 7 + .../services/externalDbConnectionService.ts | 153 ++++++++ .../properties/ExternalDBSourceProperties.tsx | 339 ++++++++++++++---- frontend/lib/api/nodeExternalConnections.ts | 75 ++++ frontend/lib/stores/flowEditorStore.ts | 51 +++ 6 files changed, 782 insertions(+), 74 deletions(-) create mode 100644 backend-node/src/routes/dataflow/node-external-connections.ts create mode 100644 frontend/lib/api/nodeExternalConnections.ts diff --git a/backend-node/src/routes/dataflow/node-external-connections.ts b/backend-node/src/routes/dataflow/node-external-connections.ts new file mode 100644 index 00000000..c15ca7dc --- /dev/null +++ b/backend-node/src/routes/dataflow/node-external-connections.ts @@ -0,0 +1,231 @@ +import { Router, Request, Response } from "express"; +import { + authenticateToken, + AuthenticatedRequest, +} from "../../middleware/authMiddleware"; +import { ExternalDbConnectionService } from "../../services/externalDbConnectionService"; +import { ExternalDbConnectionFilter } from "../../types/externalDbTypes"; +import logger from "../../utils/logger"; + +const router = Router(); + +/** + * GET /api/dataflow/node-external-connections/tested + * 노드 플로우용: 테스트에 성공한 외부 DB 커넥션 목록 조회 + */ +router.get( + "/tested", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + try { + logger.info("🔍 노드 플로우용 테스트 완료된 커넥션 조회 요청"); + + // 활성 상태의 외부 커넥션 조회 + const filter: ExternalDbConnectionFilter = { + is_active: "Y", + }; + + const externalConnections = + await ExternalDbConnectionService.getConnections(filter); + + if (!externalConnections.success) { + return res.status(400).json(externalConnections); + } + + // 외부 커넥션들에 대해 연결 테스트 수행 (제한된 병렬 처리 + 타임아웃 관리) + const validExternalConnections: any[] = []; + const connections = externalConnections.data || []; + const MAX_CONCURRENT = 3; // 최대 동시 연결 수 + const TIMEOUT_MS = 3000; // 타임아웃 3초 + + // 청크 단위로 처리 (최대 3개씩) + for (let i = 0; i < connections.length; i += MAX_CONCURRENT) { + const chunk = connections.slice(i, i + MAX_CONCURRENT); + + const chunkResults = await Promise.allSettled( + chunk.map(async (connection) => { + let testPromise: Promise | null = null; + let timeoutId: NodeJS.Timeout | null = null; + + try { + // 타임아웃과 함께 테스트 실행 + testPromise = ExternalDbConnectionService.testConnectionById( + connection.id! + ); + + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + reject(new Error("연결 테스트 타임아웃")); + }, TIMEOUT_MS); + }); + + const testResult = await Promise.race([ + testPromise, + timeoutPromise, + ]); + + // 타임아웃 정리 + if (timeoutId) clearTimeout(timeoutId); + + if (testResult.success) { + return { + id: connection.id, + connection_name: connection.connection_name, + description: connection.description, + db_type: connection.db_type, + host: connection.host, + port: connection.port, + database_name: connection.database_name, + }; + } + return null; + } catch (error) { + // 타임아웃 정리 + if (timeoutId) clearTimeout(timeoutId); + + // 🔥 타임아웃 시 연결 강제 해제 + try { + const { DatabaseConnectorFactory } = await import( + "../../database/DatabaseConnectorFactory" + ); + await DatabaseConnectorFactory.closeConnector( + connection.id!, + connection.db_type + ); + logger.info( + `🧹 타임아웃/실패로 인한 커넥션 정리 완료: ${connection.connection_name}` + ); + } catch (cleanupError) { + logger.warn( + `커넥션 정리 실패 (ID: ${connection.id}):`, + cleanupError instanceof Error + ? cleanupError.message + : cleanupError + ); + } + + logger.warn( + `커넥션 테스트 실패 (ID: ${connection.id}):`, + error instanceof Error ? error.message : error + ); + return null; + } + }) + ); + + // fulfilled 결과만 수집 + chunkResults.forEach((result) => { + if (result.status === "fulfilled" && result.value !== null) { + validExternalConnections.push(result.value); + } + }); + + // 다음 청크 처리 전 짧은 대기 (연결 풀 안정화) + if (i + MAX_CONCURRENT < connections.length) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } + + logger.info( + `✅ 테스트 성공한 커넥션: ${validExternalConnections.length}/${externalConnections.data?.length || 0}개` + ); + + return res.status(200).json({ + success: true, + data: validExternalConnections, + message: `테스트에 성공한 ${validExternalConnections.length}개의 커넥션을 조회했습니다.`, + }); + } catch (error) { + logger.error("노드 플로우용 커넥션 조회 오류:", error); + return res.status(500).json({ + success: false, + message: "서버 내부 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류", + }); + } + } +); + +/** + * GET /api/dataflow/node-external-connections/:id/tables + * 특정 외부 DB의 테이블 목록 조회 + */ +router.get( + "/:id/tables", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + try { + const id = parseInt(req.params.id); + + if (isNaN(id)) { + return res.status(400).json({ + success: false, + message: "유효하지 않은 연결 ID입니다.", + }); + } + + logger.info(`🔍 외부 DB 테이블 목록 조회: connectionId=${id}`); + + const result = + await ExternalDbConnectionService.getTablesFromConnection(id); + + return res.status(200).json(result); + } catch (error) { + logger.error("외부 DB 테이블 목록 조회 오류:", error); + return res.status(500).json({ + success: false, + message: "서버 내부 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류", + }); + } + } +); + +/** + * GET /api/dataflow/node-external-connections/:id/tables/:tableName/columns + * 특정 외부 DB 테이블의 컬럼 목록 조회 + */ +router.get( + "/:id/tables/:tableName/columns", + authenticateToken, + async (req: AuthenticatedRequest, res: Response) => { + try { + const id = parseInt(req.params.id); + const { tableName } = req.params; + + if (isNaN(id)) { + return res.status(400).json({ + success: false, + message: "유효하지 않은 연결 ID입니다.", + }); + } + + if (!tableName) { + return res.status(400).json({ + success: false, + message: "테이블명이 필요합니다.", + }); + } + + logger.info( + `🔍 외부 DB 컬럼 목록 조회: connectionId=${id}, table=${tableName}` + ); + + const result = await ExternalDbConnectionService.getColumnsFromConnection( + id, + tableName + ); + + return res.status(200).json(result); + } catch (error) { + logger.error("외부 DB 컬럼 목록 조회 오류:", error); + return res.status(500).json({ + success: false, + message: "서버 내부 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류", + }); + } + } +); + +export default router; diff --git a/backend-node/src/routes/dataflowRoutes.ts b/backend-node/src/routes/dataflowRoutes.ts index 90e07075..b61d0c48 100644 --- a/backend-node/src/routes/dataflowRoutes.ts +++ b/backend-node/src/routes/dataflowRoutes.ts @@ -22,6 +22,7 @@ import { executeConditionalActions, } from "../controllers/conditionalConnectionController"; import nodeFlowsRouter from "./dataflow/node-flows"; +import nodeExternalConnectionsRouter from "./dataflow/node-external-connections"; const router = express.Router(); @@ -153,4 +154,10 @@ router.post("/diagrams/:diagramId/execute-actions", executeConditionalActions); */ router.use("/node-flows", nodeFlowsRouter); +/** + * 노드 플로우용 외부 DB 커넥션 관리 + * /api/dataflow/node-external-connections/* + */ +router.use("/node-external-connections", nodeExternalConnectionsRouter); + export default router; diff --git a/backend-node/src/services/externalDbConnectionService.ts b/backend-node/src/services/externalDbConnectionService.ts index 4140f352..8a64da96 100644 --- a/backend-node/src/services/externalDbConnectionService.ts +++ b/backend-node/src/services/externalDbConnectionService.ts @@ -1181,4 +1181,157 @@ export class ExternalDbConnectionService { }; } } + + /** + * 특정 외부 DB 연결의 테이블 목록 조회 + */ + static async getTablesFromConnection( + connectionId: number + ): Promise> { + try { + // 연결 정보 조회 + const connection = await queryOne( + `SELECT * FROM external_db_connections WHERE id = $1`, + [connectionId] + ); + + if (!connection) { + return { + success: false, + message: `연결 ID ${connectionId}를 찾을 수 없습니다.`, + }; + } + + // 비밀번호 복호화 + const password = connection.password + ? PasswordEncryption.decrypt(connection.password) + : ""; + + // 연결 설정 준비 + const config = { + host: connection.host, + port: connection.port, + database: connection.database_name, + user: connection.username, + password: password, + connectionTimeoutMillis: + connection.connection_timeout != null + ? connection.connection_timeout * 1000 + : undefined, + queryTimeoutMillis: + connection.query_timeout != null + ? connection.query_timeout * 1000 + : undefined, + ssl: + connection.ssl_enabled === "Y" + ? { rejectUnauthorized: false } + : false, + }; + + // 커넥터 생성 + const connector = await DatabaseConnectorFactory.createConnector( + connection.db_type, + config, + connectionId + ); + + try { + const tables = await connector.getTables(); + return { + success: true, + data: tables, + message: `${tables.length}개의 테이블을 조회했습니다.`, + }; + } finally { + await DatabaseConnectorFactory.closeConnector( + connectionId, + connection.db_type + ); + } + } catch (error) { + logger.error("테이블 목록 조회 실패:", error); + return { + success: false, + message: "테이블 목록 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류", + }; + } + } + + /** + * 특정 외부 DB 테이블의 컬럼 목록 조회 + */ + static async getColumnsFromConnection( + connectionId: number, + tableName: string + ): Promise> { + try { + // 연결 정보 조회 + const connection = await queryOne( + `SELECT * FROM external_db_connections WHERE id = $1`, + [connectionId] + ); + + if (!connection) { + return { + success: false, + message: `연결 ID ${connectionId}를 찾을 수 없습니다.`, + }; + } + + // 비밀번호 복호화 + const password = connection.password + ? PasswordEncryption.decrypt(connection.password) + : ""; + + // 연결 설정 준비 + const config = { + host: connection.host, + port: connection.port, + database: connection.database_name, + user: connection.username, + password: password, + connectionTimeoutMillis: + connection.connection_timeout != null + ? connection.connection_timeout * 1000 + : undefined, + queryTimeoutMillis: + connection.query_timeout != null + ? connection.query_timeout * 1000 + : undefined, + ssl: + connection.ssl_enabled === "Y" + ? { rejectUnauthorized: false } + : false, + }; + + // 커넥터 생성 + const connector = await DatabaseConnectorFactory.createConnector( + connection.db_type, + config, + connectionId + ); + + try { + const columns = await connector.getColumns(tableName); + return { + success: true, + data: columns, + message: `${columns.length}개의 컬럼을 조회했습니다.`, + }; + } finally { + await DatabaseConnectorFactory.closeConnector( + connectionId, + connection.db_type + ); + } + } catch (error) { + logger.error("컬럼 목록 조회 실패:", error); + return { + success: false, + message: "컬럼 목록 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류", + }; + } + } } diff --git a/frontend/components/dataflow/node-editor/panels/properties/ExternalDBSourceProperties.tsx b/frontend/components/dataflow/node-editor/panels/properties/ExternalDBSourceProperties.tsx index ee8cb408..b3f36238 100644 --- a/frontend/components/dataflow/node-editor/panels/properties/ExternalDBSourceProperties.tsx +++ b/frontend/components/dataflow/node-editor/panels/properties/ExternalDBSourceProperties.tsx @@ -5,12 +5,22 @@ */ import { useEffect, useState } from "react"; -import { Database } from "lucide-react"; +import { Database, RefreshCw } 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 { @@ -27,36 +37,179 @@ const DB_TYPE_INFO: Record(data.connectionId); const [tableName, setTableName] = useState(data.tableName); const [schema, setSchema] = useState(data.schema || ""); + 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 = - data.dbType && DB_TYPE_INFO[data.dbType] - ? DB_TYPE_INFO[data.dbType] + selectedConnection && DB_TYPE_INFO[selectedConnection.db_type] + ? DB_TYPE_INFO[selectedConnection.db_type] : { - label: data.dbType ? data.dbType.toUpperCase() : "알 수 없음", + label: selectedConnection ? selectedConnection.db_type.toUpperCase() : "알 수 없음", color: "#666", icon: "💾", }; + // 🔥 첫 로드 시에만 커넥션 목록 로드 (전역 캐싱) useEffect(() => { - setDisplayName(data.displayName || data.connectionName); - setConnectionName(data.connectionName); - setTableName(data.tableName); - setSchema(data.schema || ""); - }, [data]); + 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 handleSave = () => { updateNode(nodeId, { displayName, - connectionName, + connectionId: selectedConnectionId, + connectionName: selectedConnection?.connection_name || "", tableName, schema, + dbType: selectedConnection?.db_type, }); + toast.success("설정이 저장되었습니다."); }; return ( @@ -86,11 +239,62 @@ export function ExternalDBSourceProperties({ nodeId, data }: ExternalDBSourcePro - {/* 기본 정보 */} + {/* 연결 선택 */}
-

연결 정보

+
+

외부 DB 연결

+ +
+
+ + + {loadingConnections &&

테스트 중... ⏳

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

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

+ )} +
+
- -
- - setConnectionName(e.target.value)} - className="mt-1" - placeholder="외부 DB 연결명" - /> -

외부 DB 연결 관리에서 설정한 연결명입니다.

-
- {/* 테이블 정보 */} -
-

테이블 정보

- -
-
- - setTableName(e.target.value)} - className="mt-1" - placeholder="데이터를 가져올 테이블" - /> -
- -
- - setSchema(e.target.value)} - className="mt-1" - placeholder="스키마명" - /> -
-
-
- - {/* 출력 필드 */} - {data.outputFields && data.outputFields.length > 0 && ( + {/* 테이블 선택 */} + {selectedConnectionId && (
-

출력 필드

-
- {data.outputFields.map((field, index) => ( -
- {field.label || field.name} - {field.type} -
- ))} +

테이블 선택

+ +
+
+ + + {loadingTables &&

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

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

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

+ {loadingColumns ? ( +

컬럼 목록 로딩 중... ⏳

+ ) : ( +
+ {columns.map((col, index) => ( +
+ {col.column_name} + {col.data_type} +
+ ))} +
+ )} +
+ )} + diff --git a/frontend/lib/api/nodeExternalConnections.ts b/frontend/lib/api/nodeExternalConnections.ts new file mode 100644 index 00000000..9aa99713 --- /dev/null +++ b/frontend/lib/api/nodeExternalConnections.ts @@ -0,0 +1,75 @@ +import { apiClient } from "./client"; + +export interface ExternalConnection { + id: number; + connection_name: string; + description?: string; + db_type: string; + host: string; + port: number; + database_name: string; +} + +export interface ExternalTable { + table_name: string; + table_type?: string; + schema?: string; +} + +export interface ExternalColumn { + column_name: string; + data_type: string; + is_nullable?: string; + column_default?: string; +} + +/** + * 테스트에 성공한 외부 DB 커넥션 목록 조회 + */ +export async function getTestedExternalConnections(): Promise { + const response = await apiClient.get<{ + success: boolean; + data: ExternalConnection[]; + message?: string; + }>("/dataflow/node-external-connections/tested"); + + if (response.data.success && response.data.data) { + return response.data.data; + } + + throw new Error(response.data.message || "커넥션 목록을 조회할 수 없습니다."); +} + +/** + * 특정 외부 DB의 테이블 목록 조회 + */ +export async function getExternalTables(connectionId: number): Promise { + const response = await apiClient.get<{ + success: boolean; + data: ExternalTable[]; + message?: string; + }>(`/dataflow/node-external-connections/${connectionId}/tables`); + + if (response.data.success && response.data.data) { + return response.data.data; + } + + throw new Error(response.data.message || "테이블 목록을 조회할 수 없습니다."); +} + +/** + * 특정 외부 DB 테이블의 컬럼 목록 조회 + */ +export async function getExternalColumns(connectionId: number, tableName: string): Promise { + const response = await apiClient.get<{ + success: boolean; + data: ExternalColumn[]; + message?: string; + }>(`/dataflow/node-external-connections/${connectionId}/tables/${tableName}/columns`); + + if (response.data.success && response.data.data) { + return response.data.data; + } + + throw new Error(response.data.message || "컬럼 목록을 조회할 수 없습니다."); +} diff --git a/frontend/lib/stores/flowEditorStore.ts b/frontend/lib/stores/flowEditorStore.ts index 4b61581b..7fb928fa 100644 --- a/frontend/lib/stores/flowEditorStore.ts +++ b/frontend/lib/stores/flowEditorStore.ts @@ -7,6 +7,12 @@ import { Connection, Edge, EdgeChange, Node, NodeChange, addEdge, applyNodeChang import type { FlowNode, FlowEdge, NodeType, ValidationResult } from "@/types/node-editor"; import { createNodeFlow, updateNodeFlow } from "../api/nodeFlows"; +// 🔥 외부 커넥션 캐시 타입 +interface ExternalConnectionCache { + data: any[]; + timestamp: number; +} + interface FlowEditorState { // 노드 및 엣지 nodes: FlowNode[]; @@ -30,6 +36,9 @@ interface FlowEditorState { // 검증 결과 validationResult: ValidationResult | null; + // 🔥 외부 커넥션 캐시 (전역 캐싱) + externalConnectionsCache: ExternalConnectionCache | null; + // ======================================================================== // 노드 관리 // ======================================================================== @@ -41,6 +50,14 @@ interface FlowEditorState { removeNode: (id: string) => void; removeNodes: (ids: string[]) => void; + // ======================================================================== + // 🔥 외부 커넥션 캐시 관리 + // ======================================================================== + + setExternalConnectionsCache: (data: any[]) => void; + clearExternalConnectionsCache: () => void; + getExternalConnectionsCache: () => any[] | null; + // ======================================================================== // 엣지 관리 // ======================================================================== @@ -110,6 +127,7 @@ export const useFlowEditorStore = create((set, get) => ({ showValidationPanel: false, showPropertiesPanel: true, validationResult: null, + externalConnectionsCache: null, // 🔥 캐시 초기화 // ======================================================================== // 노드 관리 @@ -380,6 +398,39 @@ export const useFlowEditorStore = create((set, get) => ({ return { incoming, outgoing }; }, + + // ======================================================================== + // 🔥 외부 커넥션 캐시 관리 + // ======================================================================== + + setExternalConnectionsCache: (data) => { + set({ + externalConnectionsCache: { + data, + timestamp: Date.now(), + }, + }); + }, + + clearExternalConnectionsCache: () => { + set({ externalConnectionsCache: null }); + }, + + getExternalConnectionsCache: () => { + const cache = get().externalConnectionsCache; + if (!cache) return null; + + // 🔥 5분 후 캐시 만료 + const CACHE_DURATION = 5 * 60 * 1000; // 5분 + const isExpired = Date.now() - cache.timestamp > CACHE_DURATION; + + if (isExpired) { + set({ externalConnectionsCache: null }); + return null; + } + + return cache.data; + }, })); // ============================================================================