ERP-node/backend-node/src/services/externalRestApiConnectionSe...

1080 lines
34 KiB
TypeScript

import { Pool, QueryResult } from "pg";
import axios, { AxiosResponse } from "axios";
import https from "https";
import { getPool } from "../database/db";
import logger from "../utils/logger";
import {
ExternalRestApiConnection,
ExternalRestApiConnectionFilter,
RestApiTestRequest,
RestApiTestResult,
AuthType,
} from "../types/externalRestApiTypes";
import { ApiResponse } from "../types/common";
import crypto from "crypto";
const pool = getPool();
// 암호화 설정
const ENCRYPTION_KEY =
process.env.DB_PASSWORD_SECRET || "default-secret-key-change-in-production";
const ALGORITHM = "aes-256-gcm";
export class ExternalRestApiConnectionService {
/**
* REST API 연결 목록 조회
*/
static async getConnections(
filter: ExternalRestApiConnectionFilter = {},
userCompanyCode?: string
): Promise<ApiResponse<ExternalRestApiConnection[]>> {
try {
let query = `
SELECT
id, connection_name, description, base_url, endpoint_path, default_headers,
default_method,
-- DB 스키마의 컬럼명은 default_request_body 기준이고
-- 코드에서는 default_body 필드로 사용하기 위해 alias 처리
default_request_body AS default_body,
auth_type, auth_config, timeout, retry_count, retry_delay,
company_code, is_active, created_date, created_by,
updated_date, updated_by, last_test_date, last_test_result, last_test_message
FROM external_rest_api_connections
WHERE 1=1
`;
const params: any[] = [];
let paramIndex = 1;
// 회사별 필터링 (최고 관리자가 아닌 경우 필수)
if (userCompanyCode && userCompanyCode !== "*") {
query += ` AND company_code = $${paramIndex}`;
params.push(userCompanyCode);
paramIndex++;
logger.info(`회사별 REST API 연결 필터링: ${userCompanyCode}`);
} else if (userCompanyCode === "*") {
logger.info(`최고 관리자: 모든 REST API 연결 조회`);
// 필터가 있으면 적용
if (filter.company_code) {
query += ` AND company_code = $${paramIndex}`;
params.push(filter.company_code);
paramIndex++;
}
} else {
// userCompanyCode가 없는 경우 (하위 호환성)
if (filter.company_code) {
query += ` AND company_code = $${paramIndex}`;
params.push(filter.company_code);
paramIndex++;
}
}
// 활성 상태 필터
if (filter.is_active) {
query += ` AND is_active = $${paramIndex}`;
params.push(filter.is_active);
paramIndex++;
}
// 인증 타입 필터
if (filter.auth_type) {
query += ` AND auth_type = $${paramIndex}`;
params.push(filter.auth_type);
paramIndex++;
}
// 검색어 필터 (연결명, 설명, URL)
if (filter.search) {
query += ` AND (
connection_name ILIKE $${paramIndex} OR
description ILIKE $${paramIndex} OR
base_url ILIKE $${paramIndex}
)`;
params.push(`%${filter.search}%`);
paramIndex++;
}
query += ` ORDER BY created_date DESC`;
const result: QueryResult<any> = await pool.query(query, params);
// 민감 정보 복호화
const connections = result.rows.map((row: any) => ({
...row,
auth_config: row.auth_config
? this.decryptSensitiveData(row.auth_config)
: null,
}));
return {
success: true,
data: connections,
message: `${connections.length}개의 연결을 조회했습니다.`,
};
} catch (error) {
logger.error("REST API 연결 목록 조회 오류:", error);
return {
success: false,
message: "연결 목록 조회에 실패했습니다.",
error: {
code: "FETCH_ERROR",
details: error instanceof Error ? error.message : "알 수 없는 오류",
},
};
}
}
/**
* REST API 연결 상세 조회
*/
static async getConnectionById(
id: number,
userCompanyCode?: string
): Promise<ApiResponse<ExternalRestApiConnection>> {
try {
let query = `
SELECT
id, connection_name, description, base_url, endpoint_path, default_headers,
default_method,
default_request_body AS default_body,
auth_type, auth_config, timeout, retry_count, retry_delay,
company_code, is_active, created_date, created_by,
updated_date, updated_by, last_test_date, last_test_result, last_test_message
FROM external_rest_api_connections
WHERE id = $1
`;
const params: any[] = [id];
// 회사별 필터링 (최고 관리자가 아닌 경우)
if (userCompanyCode && userCompanyCode !== "*") {
query += ` AND company_code = $2`;
params.push(userCompanyCode);
}
const result: QueryResult<any> = await pool.query(query, params);
if (result.rows.length === 0) {
return {
success: false,
message: "연결을 찾을 수 없거나 권한이 없습니다.",
};
}
const connection = result.rows[0];
connection.auth_config = connection.auth_config
? this.decryptSensitiveData(connection.auth_config)
: null;
// 디버깅: 조회된 연결 정보 로깅
logger.info(`REST API 연결 조회 결과 (ID: ${id}): connection_name=${connection.connection_name}, default_method=${connection.default_method}, endpoint_path=${connection.endpoint_path}`);
return {
success: true,
data: connection,
message: "연결을 조회했습니다.",
};
} catch (error) {
logger.error("REST API 연결 상세 조회 오류:", error);
return {
success: false,
message: "연결 조회에 실패했습니다.",
error: {
code: "FETCH_ERROR",
details: error instanceof Error ? error.message : "알 수 없는 오류",
},
};
}
}
/**
* REST API 연결 생성
*/
static async createConnection(
data: ExternalRestApiConnection
): Promise<ApiResponse<ExternalRestApiConnection>> {
try {
// 유효성 검증
this.validateConnectionData(data);
// 민감 정보 암호화
const encryptedAuthConfig = data.auth_config
? this.encryptSensitiveData(data.auth_config)
: null;
const query = `
INSERT INTO external_rest_api_connections (
connection_name, description, base_url, endpoint_path, default_headers,
default_method, default_request_body,
auth_type, auth_config, timeout, retry_count, retry_delay,
company_code, is_active, created_by
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
RETURNING *
`;
const params = [
data.connection_name,
data.description || null,
data.base_url,
data.endpoint_path || null,
JSON.stringify(data.default_headers || {}),
data.default_method || "GET",
data.default_body || null,
data.auth_type,
encryptedAuthConfig ? JSON.stringify(encryptedAuthConfig) : null,
data.timeout || 30000,
data.retry_count || 0,
data.retry_delay || 1000,
data.company_code || "*",
data.is_active || "Y",
data.created_by || "system",
];
// 디버깅: 저장하려는 데이터 로깅
logger.info(`REST API 연결 생성 요청 데이터:`, {
connection_name: data.connection_name,
default_method: data.default_method,
endpoint_path: data.endpoint_path,
base_url: data.base_url,
default_body: data.default_body ? "있음" : "없음",
});
const result: QueryResult<any> = await pool.query(query, params);
logger.info(`REST API 연결 생성 성공: ${data.connection_name}`);
return {
success: true,
data: result.rows[0],
message: "연결이 생성되었습니다.",
};
} catch (error: any) {
logger.error("REST API 연결 생성 오류:", error);
// 중복 키 오류 처리
if (error.code === "23505") {
return {
success: false,
message: "이미 존재하는 연결명입니다.",
};
}
return {
success: false,
message: "연결 생성에 실패했습니다.",
error: {
code: "CREATE_ERROR",
details: error instanceof Error ? error.message : "알 수 없는 오류",
},
};
}
}
/**
* REST API 연결 수정
*/
static async updateConnection(
id: number,
data: Partial<ExternalRestApiConnection>,
userCompanyCode?: string
): Promise<ApiResponse<ExternalRestApiConnection>> {
try {
// 기존 연결 확인 (회사 코드로 권한 체크)
const existing = await this.getConnectionById(id, userCompanyCode);
if (!existing.success) {
return existing;
}
// 민감 정보 암호화
const encryptedAuthConfig = data.auth_config
? this.encryptSensitiveData(data.auth_config)
: undefined;
const updateFields: string[] = [];
const params: any[] = [];
let paramIndex = 1;
if (data.connection_name !== undefined) {
updateFields.push(`connection_name = $${paramIndex}`);
params.push(data.connection_name);
paramIndex++;
}
if (data.description !== undefined) {
updateFields.push(`description = $${paramIndex}`);
params.push(data.description);
paramIndex++;
}
if (data.base_url !== undefined) {
updateFields.push(`base_url = $${paramIndex}`);
params.push(data.base_url);
paramIndex++;
}
if (data.endpoint_path !== undefined) {
updateFields.push(`endpoint_path = $${paramIndex}`);
params.push(data.endpoint_path);
paramIndex++;
}
if (data.default_headers !== undefined) {
updateFields.push(`default_headers = $${paramIndex}`);
params.push(JSON.stringify(data.default_headers));
paramIndex++;
}
if (data.default_method !== undefined) {
updateFields.push(`default_method = $${paramIndex}`);
params.push(data.default_method);
paramIndex++;
logger.info(`수정 요청 - default_method: ${data.default_method}`);
}
if (data.default_body !== undefined) {
updateFields.push(`default_request_body = $${paramIndex}`);
params.push(data.default_body); // null이면 DB에서 NULL로 저장됨
paramIndex++;
logger.info(`수정 요청 - default_body: ${data.default_body ? "있음" : "삭제(null)"}`);
}
if (data.auth_type !== undefined) {
updateFields.push(`auth_type = $${paramIndex}`);
params.push(data.auth_type);
paramIndex++;
}
if (encryptedAuthConfig !== undefined) {
updateFields.push(`auth_config = $${paramIndex}`);
params.push(JSON.stringify(encryptedAuthConfig));
paramIndex++;
}
if (data.timeout !== undefined) {
updateFields.push(`timeout = $${paramIndex}`);
params.push(data.timeout);
paramIndex++;
}
if (data.retry_count !== undefined) {
updateFields.push(`retry_count = $${paramIndex}`);
params.push(data.retry_count);
paramIndex++;
}
if (data.retry_delay !== undefined) {
updateFields.push(`retry_delay = $${paramIndex}`);
params.push(data.retry_delay);
paramIndex++;
}
if (data.is_active !== undefined) {
updateFields.push(`is_active = $${paramIndex}`);
params.push(data.is_active);
paramIndex++;
}
if (data.updated_by !== undefined) {
updateFields.push(`updated_by = $${paramIndex}`);
params.push(data.updated_by);
paramIndex++;
}
updateFields.push(`updated_date = NOW()`);
params.push(id);
const query = `
UPDATE external_rest_api_connections
SET ${updateFields.join(", ")}
WHERE id = $${paramIndex}
RETURNING *
`;
const result: QueryResult<any> = await pool.query(query, params);
logger.info(`REST API 연결 수정 성공: ID ${id}`);
return {
success: true,
data: result.rows[0],
message: "연결이 수정되었습니다.",
};
} catch (error: any) {
logger.error("REST API 연결 수정 오류:", error);
if (error.code === "23505") {
return {
success: false,
message: "이미 존재하는 연결명입니다.",
};
}
return {
success: false,
message: "연결 수정에 실패했습니다.",
error: {
code: "UPDATE_ERROR",
details: error instanceof Error ? error.message : "알 수 없는 오류",
},
};
}
}
/**
* REST API 연결 삭제
*/
static async deleteConnection(
id: number,
userCompanyCode?: string
): Promise<ApiResponse<void>> {
try {
let query = `
DELETE FROM external_rest_api_connections
WHERE id = $1
`;
const params: any[] = [id];
// 회사별 필터링 (최고 관리자가 아닌 경우)
if (userCompanyCode && userCompanyCode !== "*") {
query += ` AND company_code = $2`;
params.push(userCompanyCode);
}
query += ` RETURNING connection_name`;
const result: QueryResult<any> = await pool.query(query, params);
if (result.rows.length === 0) {
return {
success: false,
message: "연결을 찾을 수 없거나 권한이 없습니다.",
};
}
logger.info(
`REST API 연결 삭제 성공: ${result.rows[0].connection_name} (회사: ${userCompanyCode || "전체"})`
);
return {
success: true,
message: "연결이 삭제되었습니다.",
};
} catch (error) {
logger.error("REST API 연결 삭제 오류:", error);
return {
success: false,
message: "연결 삭제에 실패했습니다.",
error: {
code: "DELETE_ERROR",
details: error instanceof Error ? error.message : "알 수 없는 오류",
},
};
}
}
/**
* REST API 연결 테스트 (테스트 요청 데이터 기반)
*/
static async testConnection(
testRequest: RestApiTestRequest,
userCompanyCode?: string
): Promise<RestApiTestResult> {
const startTime = Date.now();
try {
// 헤더 구성
const headers = { ...testRequest.headers };
// 인증 헤더 추가
if (testRequest.auth_type === "db-token") {
const cfg = testRequest.auth_config || {};
const {
dbTableName,
dbValueColumn,
dbWhereColumn,
dbWhereValue,
dbHeaderName,
dbHeaderTemplate,
} = cfg;
if (!dbTableName || !dbValueColumn) {
throw new Error("DB 토큰 설정이 올바르지 않습니다.");
}
if (!userCompanyCode) {
throw new Error("DB 토큰 모드에서는 회사 코드가 필요합니다.");
}
const hasWhereColumn = !!dbWhereColumn;
const hasWhereValue =
dbWhereValue !== undefined && dbWhereValue !== null && dbWhereValue !== "";
// where 컬럼/값은 둘 다 비우거나 둘 다 채워야 함
if (hasWhereColumn !== hasWhereValue) {
throw new Error(
"DB 토큰 설정에서 조건 컬럼과 조건 값은 둘 다 비우거나 둘 다 입력해야 합니다."
);
}
// 식별자 검증 (간단한 화이트리스트)
const identifierRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
if (
!identifierRegex.test(dbTableName) ||
!identifierRegex.test(dbValueColumn) ||
(hasWhereColumn && !identifierRegex.test(dbWhereColumn as string))
) {
throw new Error(
"DB 토큰 설정에 유효하지 않은 테이블 또는 컬럼명이 포함되어 있습니다."
);
}
let sql = `
SELECT ${dbValueColumn} AS token_value
FROM ${dbTableName}
WHERE company_code = $1
`;
const params: any[] = [userCompanyCode];
if (hasWhereColumn && hasWhereValue) {
sql += ` AND ${dbWhereColumn} = $2`;
params.push(dbWhereValue);
}
sql += `
ORDER BY updated_date DESC
LIMIT 1
`;
const tokenResult: QueryResult<any> = await pool.query(sql, params);
if (tokenResult.rowCount === 0) {
throw new Error("DB에서 토큰을 찾을 수 없습니다.");
}
const tokenValue = tokenResult.rows[0]["token_value"];
const headerName = dbHeaderName || "Authorization";
const template = dbHeaderTemplate || "Bearer {{value}}";
headers[headerName] = template.replace("{{value}}", tokenValue);
} else if (
testRequest.auth_type === "bearer" &&
testRequest.auth_config?.token
) {
headers["Authorization"] = `Bearer ${testRequest.auth_config.token}`;
} else if (testRequest.auth_type === "basic" && testRequest.auth_config) {
const credentials = Buffer.from(
`${testRequest.auth_config.username}:${testRequest.auth_config.password}`
).toString("base64");
headers["Authorization"] = `Basic ${credentials}`;
} else if (
testRequest.auth_type === "api-key" &&
testRequest.auth_config
) {
if (testRequest.auth_config.keyLocation === "header") {
headers[testRequest.auth_config.keyName] =
testRequest.auth_config.keyValue;
}
}
// URL 구성
let url = testRequest.base_url;
if (testRequest.endpoint) {
url = testRequest.endpoint.startsWith("/")
? `${testRequest.base_url}${testRequest.endpoint}`
: `${testRequest.base_url}/${testRequest.endpoint}`;
}
// API Key가 쿼리에 있는 경우
if (
testRequest.auth_type === "api-key" &&
testRequest.auth_config?.keyLocation === "query" &&
testRequest.auth_config?.keyName &&
testRequest.auth_config?.keyValue
) {
const separator = url.includes("?") ? "&" : "?";
url = `${url}${separator}${testRequest.auth_config.keyName}=${testRequest.auth_config.keyValue}`;
}
logger.info(
`REST API 연결 테스트: ${testRequest.method || "GET"} ${url}`
);
// Body 처리
let body: any = undefined;
if (testRequest.body) {
// 이미 문자열이면 그대로, 객체면 JSON 문자열로 변환
if (typeof testRequest.body === "string") {
body = testRequest.body;
} else {
body = JSON.stringify(testRequest.body);
}
// Content-Type 헤더가 없으면 기본적으로 application/json 추가
const hasContentType = Object.keys(headers).some(
(k) => k.toLowerCase() === "content-type"
);
if (!hasContentType) {
headers["Content-Type"] = "application/json";
}
}
// HTTP 요청 실행
// [인수인계 중요] 2024-11-27 추가
// 특정 레거시/내부망 API(예: thiratis.com)의 경우 SSL 인증서 체인 문제로 인해
// Node.js 레벨에서 검증 실패(UNABLE_TO_VERIFY_LEAF_SIGNATURE)가 발생합니다.
//
// 원래는 인프라(OS/Docker)에 루트 CA를 등록하는 것이 정석이나,
// 유지보수 및 설정 편의성을 위해 코드 레벨에서 '특정 도메인'에 한해서만
// SSL 검증을 우회하도록 예외 처리를 해두었습니다.
//
// ※ 보안 주의: 여기에 모르는 도메인을 함부로 추가하면 중간자 공격(MITM)에 취약해질 수 있습니다.
// 꼭 필요한 신뢰할 수 있는 도메인만 추가하세요.
const bypassDomains = ["thiratis.com"];
const shouldBypassTls = bypassDomains.some((domain) =>
url.includes(domain)
);
const httpsAgent = new https.Agent({
// bypassDomains에 포함된 URL이면 검증을 무시(false), 아니면 정상 검증(true)
rejectUnauthorized: !shouldBypassTls,
});
const requestConfig = {
url,
method: (testRequest.method || "GET") as any,
headers,
data: body,
httpsAgent,
timeout: testRequest.timeout || 30000,
// 4xx/5xx 도 예외가 아니라 응답 객체로 처리
validateStatus: () => true,
};
// 요청 상세 로그 (민감 정보는 최소화)
logger.info(
`REST API 연결 테스트 요청 상세: ${JSON.stringify({
method: requestConfig.method,
url: requestConfig.url,
headers: {
...requestConfig.headers,
// Authorization 헤더는 마스킹
Authorization: requestConfig.headers?.Authorization
? "***masked***"
: undefined,
},
hasBody: !!body,
})}`
);
const response: AxiosResponse = await axios.request(requestConfig);
const responseTime = Date.now() - startTime;
// axios는 response.data에 이미 파싱된 응답 본문을 담아준다.
// JSON이 아니어도 그대로 내려보내서 프론트에서 확인할 수 있게 한다.
const responseData = response.data ?? null;
return {
success: response.status >= 200 && response.status < 300,
message:
response.status >= 200 && response.status < 300
? "연결 성공"
: `연결 실패 (${response.status} ${response.statusText})`,
response_time: responseTime,
status_code: response.status,
response_data: responseData,
};
} catch (error) {
const responseTime = Date.now() - startTime;
logger.error("REST API 연결 테스트 오류:", error);
return {
success: false,
message: "연결 실패",
response_time: responseTime,
error_details:
error instanceof Error ? error.message : "알 수 없는 오류",
};
}
}
/**
* REST API 연결 테스트 (ID 기반)
*/
static async testConnectionById(
id: number,
endpoint?: string
): Promise<RestApiTestResult> {
try {
const connectionResult = await this.getConnectionById(id);
if (!connectionResult.success || !connectionResult.data) {
return {
success: false,
message: "연결을 찾을 수 없습니다.",
};
}
const connection = connectionResult.data;
// 리스트에서 endpoint를 넘기지 않으면,
// 저장된 endpoint_path를 기본 엔드포인트로 사용
const effectiveEndpoint =
endpoint || connection.endpoint_path || undefined;
const testRequest: RestApiTestRequest = {
id: connection.id,
base_url: connection.base_url,
endpoint: effectiveEndpoint,
method: (connection.default_method as any) || "GET", // 기본 메서드 적용
headers: connection.default_headers,
body: connection.default_body, // 기본 바디 적용
auth_type: connection.auth_type,
auth_config: connection.auth_config,
timeout: connection.timeout,
};
const result = await this.testConnection(
testRequest,
connection.company_code
);
// 테스트 결과 저장
await pool.query(
`
UPDATE external_rest_api_connections
SET
last_test_date = NOW(),
last_test_result = $1,
last_test_message = $2
WHERE id = $3
`,
[result.success ? "Y" : "N", result.message, id]
);
return result;
} catch (error) {
logger.error("REST API 연결 테스트 (ID) 오류:", error);
const errorMessage =
error instanceof Error ? error.message : "알 수 없는 오류";
// 예외가 발생한 경우에도 마지막 테스트 결과를 실패로 기록
try {
await pool.query(
`
UPDATE external_rest_api_connections
SET
last_test_date = NOW(),
last_test_result = $1,
last_test_message = $2
WHERE id = $3
`,
["N", errorMessage, id]
);
} catch (updateError) {
logger.error(
"REST API 연결 테스트 (ID) 오류 기록 실패:",
updateError
);
}
return {
success: false,
message: "연결 테스트에 실패했습니다.",
error_details: errorMessage,
};
}
}
/**
* 민감 정보 암호화
*/
private static encryptSensitiveData(authConfig: any): any {
if (!authConfig) return null;
const encrypted = { ...authConfig };
// 암호화 대상 필드
if (encrypted.keyValue) {
encrypted.keyValue = this.encrypt(encrypted.keyValue);
}
if (encrypted.token) {
encrypted.token = this.encrypt(encrypted.token);
}
if (encrypted.password) {
encrypted.password = this.encrypt(encrypted.password);
}
if (encrypted.clientSecret) {
encrypted.clientSecret = this.encrypt(encrypted.clientSecret);
}
return encrypted;
}
/**
* 민감 정보 복호화
*/
private static decryptSensitiveData(authConfig: any): any {
if (!authConfig) return null;
const decrypted = { ...authConfig };
// 복호화 대상 필드
try {
if (decrypted.keyValue) {
decrypted.keyValue = this.decrypt(decrypted.keyValue);
}
if (decrypted.token) {
decrypted.token = this.decrypt(decrypted.token);
}
if (decrypted.password) {
decrypted.password = this.decrypt(decrypted.password);
}
if (decrypted.clientSecret) {
decrypted.clientSecret = this.decrypt(decrypted.clientSecret);
}
} catch (error) {
logger.warn("민감 정보 복호화 실패 (암호화되지 않은 데이터일 수 있음)");
}
return decrypted;
}
/**
* 암호화 헬퍼
*/
private static encrypt(text: string): string {
const iv = crypto.randomBytes(16);
const key = crypto.scryptSync(ENCRYPTION_KEY, "salt", 32);
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(text, "utf8", "hex");
encrypted += cipher.final("hex");
const authTag = cipher.getAuthTag();
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
}
/**
* 복호화 헬퍼
*/
private static decrypt(text: string): string {
const parts = text.split(":");
if (parts.length !== 3) {
// 암호화되지 않은 데이터
return text;
}
const iv = Buffer.from(parts[0], "hex");
const authTag = Buffer.from(parts[1], "hex");
const encryptedText = parts[2];
const key = crypto.scryptSync(ENCRYPTION_KEY, "salt", 32);
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedText, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
/**
* REST API 데이터 조회 (화면관리용 프록시)
* 저장된 연결 정보를 사용하여 외부 REST API를 호출하고 데이터를 반환
*/
static async fetchData(
connectionId: number,
endpoint?: string,
jsonPath?: string,
userCompanyCode?: string
): Promise<ApiResponse<any>> {
try {
// 연결 정보 조회
const connectionResult = await this.getConnectionById(connectionId, userCompanyCode);
if (!connectionResult.success || !connectionResult.data) {
return {
success: false,
message: "REST API 연결을 찾을 수 없습니다.",
error: {
code: "CONNECTION_NOT_FOUND",
details: `연결 ID ${connectionId}를 찾을 수 없습니다.`,
},
};
}
const connection = connectionResult.data;
// 비활성화된 연결인지 확인
if (connection.is_active !== "Y") {
return {
success: false,
message: "비활성화된 REST API 연결입니다.",
error: {
code: "CONNECTION_INACTIVE",
details: "연결이 비활성화 상태입니다.",
},
};
}
// 엔드포인트 결정 (파라미터 > 저장된 값)
const effectiveEndpoint = endpoint || connection.endpoint_path || "";
// API 호출을 위한 테스트 요청 생성
const testRequest: RestApiTestRequest = {
id: connection.id,
base_url: connection.base_url,
endpoint: effectiveEndpoint,
method: (connection.default_method as any) || "GET",
headers: connection.default_headers,
body: connection.default_body,
auth_type: connection.auth_type,
auth_config: connection.auth_config,
timeout: connection.timeout,
};
// API 호출
const result = await this.testConnection(testRequest, connection.company_code);
if (!result.success) {
return {
success: false,
message: result.message || "REST API 호출에 실패했습니다.",
error: {
code: "API_CALL_FAILED",
details: result.error_details,
},
};
}
// 응답 데이터에서 jsonPath로 데이터 추출
let extractedData = result.response_data;
logger.info(`REST API 원본 응답 데이터 타입: ${typeof result.response_data}`);
logger.info(`REST API 원본 응답 데이터 (일부): ${JSON.stringify(result.response_data)?.substring(0, 500)}`);
if (jsonPath && result.response_data) {
try {
// jsonPath로 데이터 추출 (예: "data", "data.items", "result.list")
const pathParts = jsonPath.split(".");
logger.info(`JSON Path 파싱: ${jsonPath} -> [${pathParts.join(", ")}]`);
for (const part of pathParts) {
if (extractedData && typeof extractedData === "object") {
extractedData = (extractedData as any)[part];
logger.info(`JSON Path '${part}' 추출 결과 타입: ${typeof extractedData}, 배열?: ${Array.isArray(extractedData)}`);
} else {
logger.warn(`JSON Path '${part}' 추출 실패: extractedData가 객체가 아님`);
break;
}
}
} catch (pathError) {
logger.warn(`JSON Path 추출 실패: ${jsonPath}`, pathError);
// 추출 실패 시 원본 데이터 반환
extractedData = result.response_data;
}
}
// 데이터가 배열이 아닌 경우 배열로 변환
// null이나 undefined인 경우 빈 배열로 처리
let dataArray: any[] = [];
if (extractedData === null || extractedData === undefined) {
logger.warn("추출된 데이터가 null/undefined입니다. 원본 응답 데이터를 사용합니다.");
// jsonPath 추출 실패 시 원본 데이터에서 직접 컬럼 추출 시도
if (result.response_data && typeof result.response_data === "object") {
dataArray = Array.isArray(result.response_data) ? result.response_data : [result.response_data];
}
} else {
dataArray = Array.isArray(extractedData) ? extractedData : [extractedData];
}
logger.info(`최종 데이터 배열 길이: ${dataArray.length}`);
if (dataArray.length > 0) {
logger.info(`첫 번째 데이터 항목: ${JSON.stringify(dataArray[0])?.substring(0, 300)}`);
}
// 컬럼 정보 추출 (첫 번째 유효한 데이터 기준)
let columns: Array<{ columnName: string; columnLabel: string; dataType: string }> = [];
// 첫 번째 유효한 객체 찾기
const firstValidItem = dataArray.find(item => item && typeof item === "object" && !Array.isArray(item));
if (firstValidItem) {
columns = Object.keys(firstValidItem).map((key) => ({
columnName: key,
columnLabel: key,
dataType: typeof firstValidItem[key],
}));
logger.info(`추출된 컬럼 수: ${columns.length}, 컬럼명: [${columns.map(c => c.columnName).join(", ")}]`);
} else {
logger.warn("유효한 데이터 항목을 찾을 수 없어 컬럼을 추출할 수 없습니다.");
}
return {
success: true,
data: {
rows: dataArray,
columns,
total: dataArray.length,
connectionInfo: {
connectionId: connection.id,
connectionName: connection.connection_name,
baseUrl: connection.base_url,
endpoint: effectiveEndpoint,
},
},
message: `${dataArray.length}개의 데이터를 조회했습니다.`,
};
} catch (error) {
logger.error("REST API 데이터 조회 오류:", error);
return {
success: false,
message: "REST API 데이터 조회에 실패했습니다.",
error: {
code: "FETCH_ERROR",
details: error instanceof Error ? error.message : "알 수 없는 오류",
},
};
}
}
/**
* 연결 데이터 유효성 검증
*/
private static validateConnectionData(data: ExternalRestApiConnection): void {
if (!data.connection_name || data.connection_name.trim() === "") {
throw new Error("연결명은 필수입니다.");
}
if (!data.base_url || data.base_url.trim() === "") {
throw new Error("기본 URL은 필수입니다.");
}
// URL 형식 검증
try {
new URL(data.base_url);
} catch {
throw new Error("올바른 URL 형식이 아닙니다.");
}
// 인증 타입 검증
const validAuthTypes: AuthType[] = [
"none",
"api-key",
"bearer",
"basic",
"oauth2",
"db-token",
];
if (!validAuthTypes.includes(data.auth_type)) {
throw new Error("올바르지 않은 인증 타입입니다.");
}
}
}