232 lines
7.4 KiB
TypeScript
232 lines
7.4 KiB
TypeScript
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;
|