외부 db노드 설정
This commit is contained in:
231
backend-node/src/routes/dataflow/node-external-connections.ts
Normal file
231
backend-node/src/routes/dataflow/node-external-connections.ts
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 : "알 수 없는 오류",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user