외부 REST API 연결 확장

This commit is contained in:
dohyeons
2025-11-28 11:35:36 +09:00
parent b70ed8aaff
commit 39d327fb45
5 changed files with 308 additions and 154 deletions

View File

@@ -1,4 +1,7 @@
import { Response } from "express";
import https from "https";
import axios, { AxiosRequestConfig } from "axios";
import { logger } from "../utils/logger";
import { AuthenticatedRequest } from "../middleware/authMiddleware";
import { DashboardService } from "../services/DashboardService";
import {
@@ -7,6 +10,7 @@ import {
DashboardListQuery,
} from "../types/dashboard";
import { PostgreSQLService } from "../database/PostgreSQLService";
import { ExternalRestApiConnectionService } from "../services/externalRestApiConnectionService";
/**
* 대시보드 컨트롤러
@@ -590,7 +594,14 @@ export class DashboardController {
res: Response
): Promise<void> {
try {
const { url, method = "GET", headers = {}, queryParams = {} } = req.body;
const {
url,
method = "GET",
headers = {},
queryParams = {},
body,
externalConnectionId, // 프론트엔드에서 선택된 커넥션 ID를 전달받아야 함
} = req.body;
if (!url || typeof url !== "string") {
res.status(400).json({
@@ -608,85 +619,131 @@ export class DashboardController {
}
});
// 외부 API 호출 (타임아웃 30초)
// @ts-ignore - node-fetch dynamic import
const fetch = (await import("node-fetch")).default;
// 타임아웃 설정 (Node.js 글로벌 AbortController 사용)
const controller = new (global as any).AbortController();
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60초 (기상청 API는 느림)
let response;
try {
response = await fetch(urlObj.toString(), {
method: method.toUpperCase(),
headers: {
"Content-Type": "application/json",
...headers,
},
signal: controller.signal,
});
clearTimeout(timeoutId);
} catch (err: any) {
clearTimeout(timeoutId);
if (err.name === 'AbortError') {
throw new Error('외부 API 요청 타임아웃 (30초 초과)');
// Axios 요청 설정
const requestConfig: AxiosRequestConfig = {
url: urlObj.toString(),
method: method.toUpperCase(),
headers: {
"Content-Type": "application/json",
Accept: "application/json",
...headers,
},
timeout: 60000, // 60초 타임아웃
validateStatus: () => true, // 모든 상태 코드 허용 (에러도 응답으로 처리)
};
// 외부 커넥션 ID가 있는 경우, 해당 커넥션의 인증 정보(DB 토큰 등)를 적용
if (externalConnectionId) {
try {
// 사용자 회사 코드가 있으면 사용하고, 없으면 '*' (최고 관리자)로 시도
let companyCode = req.user?.companyCode;
if (!companyCode) {
companyCode = "*";
}
// 커넥션 로드
const connectionResult =
await ExternalRestApiConnectionService.getConnectionById(
Number(externalConnectionId),
companyCode
);
if (connectionResult.success && connectionResult.data) {
const connection = connectionResult.data;
// 인증 헤더 생성 (DB 토큰 등)
const authHeaders =
await ExternalRestApiConnectionService.getAuthHeaders(
connection.auth_type,
connection.auth_config,
connection.company_code
);
// 기존 헤더에 인증 헤더 병합
requestConfig.headers = {
...requestConfig.headers,
...authHeaders,
};
// API Key가 Query Param인 경우 처리
if (
connection.auth_type === "api-key" &&
connection.auth_config?.keyLocation === "query" &&
connection.auth_config?.keyName &&
connection.auth_config?.keyValue
) {
const currentUrl = new URL(requestConfig.url!);
currentUrl.searchParams.append(
connection.auth_config.keyName,
connection.auth_config.keyValue
);
requestConfig.url = currentUrl.toString();
}
}
} catch (connError) {
logger.error(
`외부 커넥션(${externalConnectionId}) 정보 로드 및 인증 적용 실패:`,
connError
);
}
throw err;
}
if (!response.ok) {
// Body 처리
if (body) {
requestConfig.data = body;
}
// TLS 인증서 검증 예외 처리 (thiratis.com 등 내부망/레거시 API 대응)
// ExternalRestApiConnectionService와 동일한 로직 적용
const bypassDomains = ["thiratis.com"];
const hostname = urlObj.hostname;
const shouldBypassTls = bypassDomains.some((domain) =>
hostname.includes(domain)
);
if (shouldBypassTls) {
requestConfig.httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
}
const response = await axios(requestConfig);
if (response.status >= 400) {
throw new Error(
`외부 API 오류: ${response.status} ${response.statusText}`
);
}
// Content-Type에 따라 응답 파싱
const contentType = response.headers.get("content-type");
let data: any;
let data = response.data;
const contentType = response.headers["content-type"];
// 한글 인코딩 처리 (EUC-KR → UTF-8)
const isKoreanApi = urlObj.hostname.includes('kma.go.kr') ||
urlObj.hostname.includes('data.go.kr');
if (isKoreanApi) {
// 한국 정부 API는 EUC-KR 인코딩 사용
const buffer = await response.arrayBuffer();
const decoder = new TextDecoder('euc-kr');
const text = decoder.decode(buffer);
try {
data = JSON.parse(text);
} catch {
data = { text, contentType };
}
} else if (contentType && contentType.includes("application/json")) {
data = await response.json();
} else if (contentType && contentType.includes("text/")) {
// 텍스트 응답 (CSV, 일반 텍스트 등)
const text = await response.text();
data = { text, contentType };
} else {
// 기타 응답 (JSON으로 시도)
try {
data = await response.json();
} catch {
const text = await response.text();
data = { text, contentType };
}
// 텍스트 응답인 경우 포맷팅
if (typeof data === "string") {
data = { text: data, contentType };
}
res.status(200).json({
success: true,
data,
});
} catch (error) {
} catch (error: any) {
const status = error.response?.status || 500;
const message = error.response?.statusText || error.message;
logger.error("외부 API 호출 오류:", {
message,
status,
data: error.response?.data,
});
res.status(500).json({
success: false,
message: "외부 API 호출 중 오류가 발생했습니다.",
error:
process.env.NODE_ENV === "development"
? (error as Error).message
? message
: "외부 API 호출 오류",
});
}