import axios, { AxiosInstance, AxiosResponse } from "axios"; import https from "https"; import { DatabaseConnector, ConnectionConfig, QueryResult, } from "../interfaces/DatabaseConnector"; import { ConnectionTestResult, TableInfo } from "../types/externalDbTypes"; export interface RestApiConfig { baseUrl: string; apiKey: string; timeout?: number; // ConnectionConfig 호환성을 위한 더미 필드들 (사용하지 않음) host?: string; port?: number; database?: string; user?: string; password?: string; } export class RestApiConnector implements DatabaseConnector { private httpClient: AxiosInstance; private config: RestApiConfig; constructor(config: RestApiConfig) { this.config = config; // Axios 인스턴스 생성 // 🔐 apiKey가 없을 수도 있으므로 Authorization 헤더는 선택적으로만 추가 const defaultHeaders: Record = { "Content-Type": "application/json", Accept: "application/json", }; if (config.apiKey) { defaultHeaders["Authorization"] = `Bearer ${config.apiKey}`; } this.httpClient = axios.create({ baseURL: config.baseUrl, timeout: config.timeout || 30000, headers: defaultHeaders, // ⚠️ 외부 API 중 자체 서명 인증서를 사용하는 경우가 있어서 // 인증서 검증을 끈 HTTPS 에이전트를 사용한다. // 내부망/신뢰된 시스템 전용으로 사용해야 하며, // 공개 인터넷용 API에는 적용하면 안 된다. httpsAgent: new https.Agent({ rejectUnauthorized: false }), }); // 요청/응답 인터셉터 설정 this.setupInterceptors(); } private setupInterceptors() { // 요청 인터셉터 this.httpClient.interceptors.request.use( (config) => { console.log( `[RestApiConnector] 요청: ${config.method?.toUpperCase()} ${config.url}` ); return config; }, (error) => { console.error("[RestApiConnector] 요청 오류:", error); return Promise.reject(error); } ); // 응답 인터셉터 this.httpClient.interceptors.response.use( (response) => { console.log( `[RestApiConnector] 응답: ${response.status} ${response.statusText}` ); return response; }, (error) => { console.error( "[RestApiConnector] 응답 오류:", error.response?.status, error.response?.statusText ); return Promise.reject(error); } ); } async connect(): Promise { // 기존에는 /health 엔드포인트를 호출해서 미리 연결을 검사했지만, // 일반 외부 API들은 /health가 없거나 401/500을 반환하는 경우가 많아 // 불필요하게 예외가 나면서 미리보기/배치 실행이 막히는 문제가 있었다. // // 따라서 여기서는 "연결 준비 완료" 정도만 로그로 남기고 // 실제 호출 실패 여부는 executeRequest 단계에서만 판단하도록 한다. console.log( `[RestApiConnector] 연결 준비 완료 (사전 헬스체크 생략): ${this.config.baseUrl}` ); return; } async disconnect(): Promise { // REST API는 연결 해제가 필요 없음 console.log(`[RestApiConnector] 연결 해제: ${this.config.baseUrl}`); } async testConnection(): Promise { try { await this.connect(); return { success: true, message: "REST API 연결이 성공했습니다.", details: { response_time: Date.now(), }, }; } catch (error) { return { success: false, message: error instanceof Error ? error.message : "REST API 연결에 실패했습니다.", details: { response_time: Date.now(), }, }; } } // 🔥 DatabaseConnector 인터페이스 호환용 executeQuery (사용하지 않음) async executeQuery(query: string, params?: any[]): Promise { // REST API는 executeRequest를 사용해야 함 throw new Error( "REST API Connector는 executeQuery를 지원하지 않습니다. executeRequest를 사용하세요." ); } // 🔥 실제 REST API 요청을 위한 메서드 async executeRequest( endpoint: string, method: "GET" | "POST" | "PUT" | "DELETE" = "GET", data?: any ): Promise { try { const startTime = Date.now(); let response: AxiosResponse; // HTTP 메서드에 따른 요청 실행 switch (method.toUpperCase()) { case "GET": response = await this.httpClient.get(endpoint); break; case "POST": response = await this.httpClient.post(endpoint, data); break; case "PUT": response = await this.httpClient.put(endpoint, data); break; case "DELETE": response = await this.httpClient.delete(endpoint); break; default: throw new Error(`지원하지 않는 HTTP 메서드: ${method}`); } const executionTime = Date.now() - startTime; const responseData = response.data; console.log(`[RestApiConnector] 원본 응답 데이터:`, { type: typeof responseData, isArray: Array.isArray(responseData), keys: typeof responseData === "object" ? Object.keys(responseData) : "not object", responseData: responseData, }); // 응답 데이터 처리 let rows: any[]; if (Array.isArray(responseData)) { rows = responseData; } else if ( responseData && responseData.data && Array.isArray(responseData.data) ) { // API 응답이 {success: true, data: [...]} 형태인 경우 rows = responseData.data; } else if ( responseData && responseData.data && typeof responseData.data === "object" ) { // API 응답이 {success: true, data: {...}} 형태인 경우 (단일 객체) rows = [responseData.data]; } else if ( responseData && typeof responseData === "object" && !Array.isArray(responseData) ) { // 단일 객체 응답인 경우 rows = [responseData]; } else { rows = []; } console.log(`[RestApiConnector] 처리된 rows:`, { rowsLength: rows.length, firstRow: rows.length > 0 ? rows[0] : "no data", allRows: rows, }); console.log(`[RestApiConnector] API 호출 결과:`, { endpoint, method, status: response.status, rowCount: rows.length, executionTime: `${executionTime}ms`, }); return { rows: rows, rowCount: rows.length, fields: rows.length > 0 ? Object.keys(rows[0]).map((key) => ({ name: key, type: "string" })) : [], }; } catch (error) { console.error( `[RestApiConnector] API 호출 오류 (${method} ${endpoint}):`, error ); if (axios.isAxiosError(error)) { throw new Error( `REST API 호출 실패: ${error.response?.status} ${error.response?.statusText}` ); } throw new Error( `REST API 호출 실패: ${error instanceof Error ? error.message : "알 수 없는 오류"}` ); } } async getTables(): Promise { // REST API의 경우 "테이블"은 사용 가능한 엔드포인트를 의미 // 일반적인 REST API 엔드포인트들을 반환 return [ { table_name: "/api/users", columns: [], description: "사용자 정보 API", }, { table_name: "/api/data", columns: [], description: "기본 데이터 API", }, { table_name: "/api/custom", columns: [], description: "사용자 정의 엔드포인트", }, ]; } async getTableList(): Promise { return this.getTables(); } async getColumns(endpoint: string): Promise { try { // GET 요청으로 샘플 데이터를 가져와서 필드 구조 파악 const result = await this.executeRequest(endpoint, "GET"); if (result.rows.length > 0) { const sampleRow = result.rows[0]; return Object.keys(sampleRow).map((key) => ({ column_name: key, data_type: typeof sampleRow[key], is_nullable: "YES", column_default: null, description: `${key} 필드`, })); } return []; } catch (error) { console.error( `[RestApiConnector] 컬럼 정보 조회 오류 (${endpoint}):`, error ); return []; } } async getTableColumns(endpoint: string): Promise { return this.getColumns(endpoint); } // REST API 전용 메서드들 async getData( endpoint: string, params?: Record ): Promise { const queryString = params ? "?" + new URLSearchParams(params).toString() : ""; const result = await this.executeRequest(endpoint + queryString, "GET"); return result.rows; } async postData(endpoint: string, data: any): Promise { const result = await this.executeRequest(endpoint, "POST", data); return result.rows[0]; } async putData(endpoint: string, data: any): Promise { const result = await this.executeRequest(endpoint, "PUT", data); return result.rows[0]; } async deleteData(endpoint: string): Promise { const result = await this.executeRequest(endpoint, "DELETE"); return result.rows[0]; } }