2025-10-21 10:59:15 +09:00
import { Pool , QueryResult } from "pg" ;
2025-11-27 16:42:48 +09:00
import axios , { AxiosResponse } from "axios" ;
import https from "https" ;
2025-10-21 10:59:15 +09:00
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 (
2025-10-28 11:54:44 +09:00
filter : ExternalRestApiConnectionFilter = { } ,
userCompanyCode? : string
2025-10-21 10:59:15 +09:00
) : Promise < ApiResponse < ExternalRestApiConnection [ ] > > {
try {
let query = `
SELECT
2025-10-27 18:33:15 +09:00
id , connection_name , description , base_url , endpoint_path , default_headers ,
2025-11-27 16:42:48 +09:00
default_method ,
-- DB 스 키 마 의 컬 럼 명 은 default_request_body 기 준 이 고
-- 코 드 에 서 는 default_body 필 드 로 사 용 하 기 위 해 alias 처 리
default_request_body AS default_body ,
2025-10-21 10:59:15 +09:00
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 ;
2025-10-28 11:54:44 +09:00
// 회사별 필터링 (최고 관리자가 아닌 경우 필수)
if ( userCompanyCode && userCompanyCode !== "*" ) {
2025-10-21 10:59:15 +09:00
query += ` AND company_code = $ ${ paramIndex } ` ;
2025-10-28 11:54:44 +09:00
params . push ( userCompanyCode ) ;
2025-10-21 10:59:15 +09:00
paramIndex ++ ;
2025-10-28 11:54:44 +09:00
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 ++ ;
}
2025-10-21 10:59:15 +09:00
}
// 활성 상태 필터
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 (
2025-10-28 11:54:44 +09:00
id : number ,
userCompanyCode? : string
2025-10-21 10:59:15 +09:00
) : Promise < ApiResponse < ExternalRestApiConnection > > {
try {
2025-10-28 11:54:44 +09:00
let query = `
2025-10-21 10:59:15 +09:00
SELECT
2025-10-27 18:33:15 +09:00
id , connection_name , description , base_url , endpoint_path , default_headers ,
2025-11-27 16:42:48 +09:00
default_method ,
default_request_body AS default_body ,
2025-10-21 10:59:15 +09:00
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
` ;
2025-10-28 11:54:44 +09:00
const params : any [ ] = [ id ] ;
// 회사별 필터링 (최고 관리자가 아닌 경우)
if ( userCompanyCode && userCompanyCode !== "*" ) {
query += ` AND company_code = $ 2 ` ;
params . push ( userCompanyCode ) ;
}
const result : QueryResult < any > = await pool . query ( query , params ) ;
2025-10-21 10:59:15 +09:00
if ( result . rows . length === 0 ) {
return {
success : false ,
2025-10-28 11:54:44 +09:00
message : "연결을 찾을 수 없거나 권한이 없습니다." ,
2025-10-21 10:59:15 +09:00
} ;
}
const connection = result . rows [ 0 ] ;
connection . auth_config = connection . auth_config
? this . decryptSensitiveData ( connection . auth_config )
: null ;
2025-11-28 14:45:04 +09:00
// 디버깅: 조회된 연결 정보 로깅
logger . info ( ` REST API 연결 조회 결과 (ID: ${ id } ): connection_name= ${ connection . connection_name } , default_method= ${ connection . default_method } , endpoint_path= ${ connection . endpoint_path } ` ) ;
2025-10-21 10:59:15 +09:00
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 (
2025-10-27 18:33:15 +09:00
connection_name , description , base_url , endpoint_path , default_headers ,
2025-11-27 16:42:48 +09:00
default_method , default_request_body ,
2025-10-21 10:59:15 +09:00
auth_type , auth_config , timeout , retry_count , retry_delay ,
company_code , is_active , created_by
2025-11-27 16:42:48 +09:00
) VALUES ( $1 , $2 , $3 , $4 , $5 , $6 , $7 , $8 , $9 , $10 , $11 , $12 , $13 , $14 , $15 )
2025-10-21 10:59:15 +09:00
RETURNING *
` ;
const params = [
data . connection_name ,
data . description || null ,
data . base_url ,
2025-10-27 18:33:15 +09:00
data . endpoint_path || null ,
2025-10-21 10:59:15 +09:00
JSON . stringify ( data . default_headers || { } ) ,
2025-11-27 16:42:48 +09:00
data . default_method || "GET" ,
data . default_body || null ,
2025-10-21 10:59:15 +09:00
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" ,
] ;
2025-11-28 14:45:04 +09:00
// 디버깅: 저장하려는 데이터 로깅
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 ? "있음" : "없음" ,
} ) ;
2025-10-21 10:59:15 +09:00
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 ,
2025-10-28 11:54:44 +09:00
data : Partial < ExternalRestApiConnection > ,
userCompanyCode? : string
2025-10-21 10:59:15 +09:00
) : Promise < ApiResponse < ExternalRestApiConnection > > {
try {
2025-10-28 11:54:44 +09:00
// 기존 연결 확인 (회사 코드로 권한 체크)
const existing = await this . getConnectionById ( id , userCompanyCode ) ;
2025-10-21 10:59:15 +09:00
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 ++ ;
}
2025-10-27 18:33:15 +09:00
if ( data . endpoint_path !== undefined ) {
updateFields . push ( ` endpoint_path = $ ${ paramIndex } ` ) ;
params . push ( data . endpoint_path ) ;
paramIndex ++ ;
}
2025-10-21 10:59:15 +09:00
if ( data . default_headers !== undefined ) {
updateFields . push ( ` default_headers = $ ${ paramIndex } ` ) ;
params . push ( JSON . stringify ( data . default_headers ) ) ;
paramIndex ++ ;
}
2025-11-27 16:42:48 +09:00
if ( data . default_method !== undefined ) {
updateFields . push ( ` default_method = $ ${ paramIndex } ` ) ;
params . push ( data . default_method ) ;
paramIndex ++ ;
2025-11-28 14:45:04 +09:00
logger . info ( ` 수정 요청 - default_method: ${ data . default_method } ` ) ;
2025-11-27 16:42:48 +09:00
}
if ( data . default_body !== undefined ) {
updateFields . push ( ` default_request_body = $ ${ paramIndex } ` ) ;
2025-11-28 14:45:04 +09:00
params . push ( data . default_body ) ; // null이면 DB에서 NULL로 저장됨
2025-11-27 16:42:48 +09:00
paramIndex ++ ;
2025-11-28 14:45:04 +09:00
logger . info ( ` 수정 요청 - default_body: ${ data . default_body ? "있음" : "삭제(null)" } ` ) ;
2025-11-27 16:42:48 +09:00
}
2025-10-21 10:59:15 +09:00
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 연 결 삭 제
* /
2025-10-28 11:54:44 +09:00
static async deleteConnection (
id : number ,
userCompanyCode? : string
) : Promise < ApiResponse < void > > {
2025-10-21 10:59:15 +09:00
try {
2025-10-28 11:54:44 +09:00
let query = `
2025-10-21 10:59:15 +09:00
DELETE FROM external_rest_api_connections
WHERE id = $1
` ;
2025-10-28 11:54:44 +09:00
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 ) ;
2025-10-21 10:59:15 +09:00
if ( result . rows . length === 0 ) {
return {
success : false ,
2025-10-28 11:54:44 +09:00
message : "연결을 찾을 수 없거나 권한이 없습니다." ,
2025-10-21 10:59:15 +09:00
} ;
}
2025-10-28 11:54:44 +09:00
logger . info (
` REST API 연결 삭제 성공: ${ result . rows [ 0 ] . connection_name } (회사: ${ userCompanyCode || "전체" } ) `
) ;
2025-10-21 10:59:15 +09:00
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 : "알 수 없는 오류" ,
} ,
} ;
}
}
/ * *
2025-11-28 11:35:36 +09:00
* 인 증 헤 더 생 성
2025-10-21 10:59:15 +09:00
* /
2025-11-28 11:35:36 +09:00
static async getAuthHeaders (
authType : AuthType ,
authConfig : any ,
companyCode? : string
) : Promise < Record < string , string > > {
const headers : Record < string , string > = { } ;
if ( authType === "db-token" ) {
const cfg = authConfig || { } ;
const {
dbTableName ,
dbValueColumn ,
dbWhereColumn ,
dbWhereValue ,
dbHeaderName ,
dbHeaderTemplate ,
} = cfg ;
if ( ! dbTableName || ! dbValueColumn ) {
throw new Error ( "DB 토큰 설정이 올바르지 않습니다." ) ;
}
2025-10-21 10:59:15 +09:00
2025-11-28 11:35:36 +09:00
if ( ! companyCode ) {
throw new Error ( "DB 토큰 모드에서는 회사 코드가 필요합니다." ) ;
}
2025-11-27 16:42:48 +09:00
2025-11-28 11:35:36 +09:00
const hasWhereColumn = ! ! dbWhereColumn ;
const hasWhereValue =
dbWhereValue !== undefined &&
dbWhereValue !== null &&
dbWhereValue !== "" ;
// where 컬럼/값은 둘 다 비우거나 둘 다 채워야 함
if ( hasWhereColumn !== hasWhereValue ) {
throw new Error (
"DB 토큰 설정에서 조건 컬럼과 조건 값은 둘 다 비우거나 둘 다 입력해야 합니다."
) ;
}
2025-11-27 16:42:48 +09:00
2025-11-28 11:35:36 +09:00
// 식별자 검증 (간단한 화이트리스트)
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 토큰 설정에 유효하지 않은 테이블 또는 컬럼명이 포함되어 있습니다."
) ;
}
2025-11-27 16:42:48 +09:00
2025-11-28 11:35:36 +09:00
let sql = `
SELECT $ { dbValueColumn } AS token_value
FROM $ { dbTableName }
WHERE company_code = $1
` ;
2025-11-27 16:42:48 +09:00
2025-11-28 11:35:36 +09:00
const params : any [ ] = [ companyCode ] ;
2025-11-27 16:42:48 +09:00
2025-11-28 11:35:36 +09:00
if ( hasWhereColumn && hasWhereValue ) {
sql += ` AND ${ dbWhereColumn } = $ 2 ` ;
params . push ( dbWhereValue ) ;
}
2025-11-27 16:42:48 +09:00
2025-11-28 11:35:36 +09:00
sql += `
ORDER BY updated_date DESC
LIMIT 1
` ;
2025-11-27 16:42:48 +09:00
2025-11-28 11:35:36 +09:00
const tokenResult : QueryResult < any > = await pool . query ( sql , params ) ;
2025-11-27 16:42:48 +09:00
2025-11-28 11:35:36 +09:00
if ( tokenResult . rowCount === 0 ) {
throw new Error ( "DB에서 토큰을 찾을 수 없습니다." ) ;
}
2025-11-27 16:42:48 +09:00
2025-11-28 11:35:36 +09:00
const tokenValue = tokenResult . rows [ 0 ] [ "token_value" ] ;
const headerName = dbHeaderName || "Authorization" ;
const template = dbHeaderTemplate || "Bearer {{value}}" ;
headers [ headerName ] = template . replace ( "{{value}}" , tokenValue ) ;
} else if ( authType === "bearer" && authConfig ? . token ) {
headers [ "Authorization" ] = ` Bearer ${ authConfig . token } ` ;
} else if ( authType === "basic" && authConfig ) {
const credentials = Buffer . from (
` ${ authConfig . username } : ${ authConfig . password } `
) . toString ( "base64" ) ;
headers [ "Authorization" ] = ` Basic ${ credentials } ` ;
} else if ( authType === "api-key" && authConfig ) {
if ( authConfig . keyLocation === "header" ) {
headers [ authConfig . keyName ] = authConfig . keyValue ;
}
}
2025-11-27 16:42:48 +09:00
2025-11-28 11:35:36 +09:00
return headers ;
}
2025-11-27 16:42:48 +09:00
2025-11-28 11:35:36 +09:00
/ * *
* REST API 연 결 테 스 트 ( 테 스 트 요 청 데 이 터 기 반 )
* /
static async testConnection (
testRequest : RestApiTestRequest ,
userCompanyCode? : string
) : Promise < RestApiTestResult > {
const startTime = Date . now ( ) ;
2025-11-27 16:42:48 +09:00
2025-11-28 11:35:36 +09:00
try {
// 헤더 구성
let headers = { . . . testRequest . headers } ;
// 인증 헤더 생성 및 병합
const authHeaders = await this . getAuthHeaders (
testRequest . auth_type ,
testRequest . auth_config ,
userCompanyCode
) ;
headers = { . . . headers , . . . authHeaders } ;
2025-10-21 10:59:15 +09:00
// 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 } `
) ;
2025-11-27 16:42:48 +09:00
// 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" ;
}
}
2025-11-27 17:11:39 +09:00
// 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 )
) ;
2025-11-27 16:42:48 +09:00
const httpsAgent = new https . Agent ( {
2025-11-27 17:11:39 +09:00
// bypassDomains에 포함된 URL이면 검증을 무시(false), 아니면 정상 검증(true)
rejectUnauthorized : ! shouldBypassTls ,
2025-10-21 10:59:15 +09:00
} ) ;
2025-11-27 16:42:48 +09:00
const requestConfig = {
url ,
method : ( testRequest . method || "GET" ) as any ,
headers ,
data : body ,
httpsAgent ,
timeout : testRequest.timeout || 30000 ,
// 4xx/5xx 도 예외가 아니라 응답 객체로 처리
validateStatus : ( ) = > true ,
} ;
2025-10-21 10:59:15 +09:00
2025-11-27 16:42:48 +09:00
// 요청 상세 로그 (민감 정보는 최소화)
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 ;
2025-10-21 10:59:15 +09:00
return {
2025-11-27 16:42:48 +09:00
success : response.status >= 200 && response . status < 300 ,
message :
response . status >= 200 && response . status < 300
2025-10-21 10:59:15 +09:00
? "연결 성공"
: ` 연결 실패 ( ${ 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 ;
2025-11-27 16:42:48 +09:00
// 리스트에서 endpoint를 넘기지 않으면,
// 저장된 endpoint_path를 기본 엔드포인트로 사용
const effectiveEndpoint =
endpoint || connection . endpoint_path || undefined ;
2025-10-21 10:59:15 +09:00
const testRequest : RestApiTestRequest = {
id : connection.id ,
base_url : connection.base_url ,
2025-11-27 16:42:48 +09:00
endpoint : effectiveEndpoint ,
method : ( connection . default_method as any ) || "GET" , // 기본 메서드 적용
2025-10-21 10:59:15 +09:00
headers : connection.default_headers ,
2025-11-27 16:42:48 +09:00
body : connection.default_body , // 기본 바디 적용
2025-10-21 10:59:15 +09:00
auth_type : connection.auth_type ,
auth_config : connection.auth_config ,
timeout : connection.timeout ,
} ;
2025-11-27 16:42:48 +09:00
const result = await this . testConnection (
testRequest ,
connection . company_code
) ;
2025-10-21 10:59:15 +09:00
// 테스트 결과 저장
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 ) ;
2025-11-27 17:11:30 +09:00
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
) ;
}
2025-10-21 10:59:15 +09:00
return {
success : false ,
message : "연결 테스트에 실패했습니다." ,
2025-11-27 17:11:30 +09:00
error_details : errorMessage ,
2025-10-21 10:59:15 +09:00
} ;
}
}
/ * *
* 민 감 정 보 암 호 화
* /
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 ;
}
2025-11-28 14:45:04 +09:00
/ * *
* 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 : "알 수 없는 오류" ,
} ,
} ;
}
}
2025-10-21 10:59:15 +09:00
/ * *
* 연 결 데 이 터 유 효 성 검 증
* /
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" ,
2025-11-27 16:42:48 +09:00
"db-token" ,
2025-10-21 10:59:15 +09:00
] ;
if ( ! validAuthTypes . includes ( data . auth_type ) ) {
throw new Error ( "올바르지 않은 인증 타입입니다." ) ;
}
}
}