외부 db노드 설정

This commit is contained in:
kjs
2025-10-02 16:43:40 +09:00
parent 0743786f9b
commit 37e018b33c
6 changed files with 782 additions and 74 deletions

View File

@@ -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<any> | null = null;
let timeoutId: NodeJS.Timeout | null = null;
try {
// 타임아웃과 함께 테스트 실행
testPromise = ExternalDbConnectionService.testConnectionById(
connection.id!
);
const timeoutPromise = new Promise<never>((_, 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;

View File

@@ -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;

View File

@@ -1181,4 +1181,157 @@ export class ExternalDbConnectionService {
};
}
}
/**
* 특정 외부 DB 연결의 테이블 목록 조회
*/
static async getTablesFromConnection(
connectionId: number
): Promise<ApiResponse<TableInfo[]>> {
try {
// 연결 정보 조회
const connection = await queryOne<any>(
`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<ApiResponse<any[]>> {
try {
// 연결 정보 조회
const connection = await queryOne<any>(
`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 : "알 수 없는 오류",
};
}
}
}