262 lines
8.3 KiB
TypeScript
262 lines
8.3 KiB
TypeScript
import axios, { AxiosInstance, AxiosResponse } from 'axios';
|
|
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 인스턴스 생성
|
|
this.httpClient = axios.create({
|
|
baseURL: config.baseUrl,
|
|
timeout: config.timeout || 30000,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-API-Key': config.apiKey,
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
|
|
// 요청/응답 인터셉터 설정
|
|
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<void> {
|
|
try {
|
|
// 연결 테스트 - 기본 엔드포인트 호출
|
|
await this.httpClient.get('/health', { timeout: 5000 });
|
|
console.log(`[RestApiConnector] 연결 성공: ${this.config.baseUrl}`);
|
|
} catch (error) {
|
|
// health 엔드포인트가 없을 수 있으므로 404는 정상으로 처리
|
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
|
console.log(`[RestApiConnector] 연결 성공 (health 엔드포인트 없음): ${this.config.baseUrl}`);
|
|
return;
|
|
}
|
|
console.error(`[RestApiConnector] 연결 실패: ${this.config.baseUrl}`, error);
|
|
throw new Error(`REST API 연결 실패: ${error instanceof Error ? error.message : '알 수 없는 오류'}`);
|
|
}
|
|
}
|
|
|
|
async disconnect(): Promise<void> {
|
|
// REST API는 연결 해제가 필요 없음
|
|
console.log(`[RestApiConnector] 연결 해제: ${this.config.baseUrl}`);
|
|
}
|
|
|
|
async testConnection(): Promise<ConnectionTestResult> {
|
|
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()
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
async executeQuery(endpoint: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', data?: any): Promise<QueryResult> {
|
|
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<TableInfo[]> {
|
|
// 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<TableInfo[]> {
|
|
return this.getTables();
|
|
}
|
|
|
|
async getColumns(endpoint: string): Promise<any[]> {
|
|
try {
|
|
// GET 요청으로 샘플 데이터를 가져와서 필드 구조 파악
|
|
const result = await this.executeQuery(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<any[]> {
|
|
return this.getColumns(endpoint);
|
|
}
|
|
|
|
// REST API 전용 메서드들
|
|
async getData(endpoint: string, params?: Record<string, any>): Promise<any[]> {
|
|
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
|
|
const result = await this.executeQuery(endpoint + queryString, 'GET');
|
|
return result.rows;
|
|
}
|
|
|
|
async postData(endpoint: string, data: any): Promise<any> {
|
|
const result = await this.executeQuery(endpoint, 'POST', data);
|
|
return result.rows[0];
|
|
}
|
|
|
|
async putData(endpoint: string, data: any): Promise<any> {
|
|
const result = await this.executeQuery(endpoint, 'PUT', data);
|
|
return result.rows[0];
|
|
}
|
|
|
|
async deleteData(endpoint: string): Promise<any> {
|
|
const result = await this.executeQuery(endpoint, 'DELETE');
|
|
return result.rows[0];
|
|
}
|
|
}
|