Merge branch 'feature/rest-api-integration' of http://39.117.244.52:3000/kjs/ERP-node
This commit is contained in:
@@ -3,6 +3,7 @@ import { PostgreSQLConnector } from './PostgreSQLConnector';
|
||||
import { MariaDBConnector } from './MariaDBConnector';
|
||||
import { MSSQLConnector } from './MSSQLConnector';
|
||||
import { OracleConnector } from './OracleConnector';
|
||||
import { RestApiConnector, RestApiConfig } from './RestApiConnector';
|
||||
|
||||
export class DatabaseConnectorFactory {
|
||||
private static connectors = new Map<string, DatabaseConnector>();
|
||||
@@ -33,6 +34,9 @@ export class DatabaseConnectorFactory {
|
||||
case 'oracle':
|
||||
connector = new OracleConnector(config);
|
||||
break;
|
||||
case 'restapi':
|
||||
connector = new RestApiConnector(config as RestApiConfig);
|
||||
break;
|
||||
// Add other database types here
|
||||
default:
|
||||
throw new Error(`지원하지 않는 데이터베이스 타입: ${type}`);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DatabaseConnector, ConnectionConfig, QueryResult } from '../interfaces/DatabaseConnector';
|
||||
import { ConnectionTestResult, TableInfo } from '../types/externalDbTypes';
|
||||
// @ts-ignore
|
||||
import * as mssql from 'mssql';
|
||||
|
||||
export class MSSQLConnector implements DatabaseConnector {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import {
|
||||
DatabaseConnector,
|
||||
ConnectionConfig,
|
||||
QueryResult,
|
||||
} from "../interfaces/DatabaseConnector";
|
||||
import { ConnectionTestResult, TableInfo } from "../types/externalDbTypes";
|
||||
import * as mysql from "mysql2/promise";
|
||||
import { DatabaseConnector, ConnectionConfig, QueryResult } from '../interfaces/DatabaseConnector';
|
||||
import { ConnectionTestResult, TableInfo } from '../types/externalDbTypes';
|
||||
// @ts-ignore
|
||||
import * as mysql from 'mysql2/promise';
|
||||
|
||||
export class MariaDBConnector implements DatabaseConnector {
|
||||
private connection: mysql.Connection | null = null;
|
||||
@@ -22,18 +19,8 @@ export class MariaDBConnector implements DatabaseConnector {
|
||||
user: this.config.user,
|
||||
password: this.config.password,
|
||||
database: this.config.database,
|
||||
// 🔧 MySQL2에서 지원하는 타임아웃 설정
|
||||
connectTimeout: this.config.connectionTimeoutMillis || 30000, // 연결 타임아웃 30초
|
||||
ssl: typeof this.config.ssl === "boolean" ? undefined : this.config.ssl,
|
||||
// 🔧 MySQL2에서 지원하는 추가 설정
|
||||
charset: "utf8mb4",
|
||||
timezone: "Z",
|
||||
supportBigNumbers: true,
|
||||
bigNumberStrings: true,
|
||||
// 🔧 연결 풀 설정 (단일 연결이지만 안정성을 위해)
|
||||
dateStrings: true,
|
||||
debug: false,
|
||||
trace: false,
|
||||
connectTimeout: this.config.connectionTimeoutMillis,
|
||||
ssl: typeof this.config.ssl === 'boolean' ? undefined : this.config.ssl,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -49,9 +36,7 @@ export class MariaDBConnector implements DatabaseConnector {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
await this.connect();
|
||||
const [rows] = await this.connection!.query(
|
||||
"SELECT VERSION() as version"
|
||||
);
|
||||
const [rows] = await this.connection!.query("SELECT VERSION() as version");
|
||||
const version = (rows as any[])[0]?.version || "Unknown";
|
||||
const responseTime = Date.now() - startTime;
|
||||
await this.disconnect();
|
||||
@@ -79,18 +64,7 @@ export class MariaDBConnector implements DatabaseConnector {
|
||||
async executeQuery(query: string): Promise<QueryResult> {
|
||||
try {
|
||||
await this.connect();
|
||||
|
||||
// 🔧 쿼리 타임아웃 수동 구현 (60초)
|
||||
const queryTimeout = this.config.queryTimeoutMillis || 60000;
|
||||
const queryPromise = this.connection!.query(query);
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error("쿼리 실행 타임아웃")), queryTimeout);
|
||||
});
|
||||
|
||||
const [rows, fields] = (await Promise.race([
|
||||
queryPromise,
|
||||
timeoutPromise,
|
||||
])) as any;
|
||||
const [rows, fields] = await this.connection!.query(query);
|
||||
await this.disconnect();
|
||||
return {
|
||||
rows: rows as any[],
|
||||
@@ -133,54 +107,28 @@ export class MariaDBConnector implements DatabaseConnector {
|
||||
|
||||
async getColumns(tableName: string): Promise<any[]> {
|
||||
try {
|
||||
console.log(`🔍 MariaDB 컬럼 조회 시작: ${tableName}`);
|
||||
console.log(`[MariaDBConnector] getColumns 호출: tableName=${tableName}`);
|
||||
await this.connect();
|
||||
|
||||
// 🔧 컬럼 조회 타임아웃 수동 구현 (30초)
|
||||
const queryTimeout = this.config.queryTimeoutMillis || 30000;
|
||||
// 스키마명을 명시적으로 확인
|
||||
const schemaQuery = `SELECT DATABASE() as schema_name`;
|
||||
const [schemaResult] = await this.connection!.query(schemaQuery);
|
||||
const schemaName =
|
||||
(schemaResult as any[])[0]?.schema_name || this.config.database;
|
||||
|
||||
console.log(`📋 사용할 스키마: ${schemaName}`);
|
||||
|
||||
const query = `
|
||||
console.log(`[MariaDBConnector] 연결 완료, 쿼리 실행 시작`);
|
||||
|
||||
const [rows] = await this.connection!.query(`
|
||||
SELECT
|
||||
COLUMN_NAME as column_name,
|
||||
DATA_TYPE as data_type,
|
||||
IS_NULLABLE as is_nullable,
|
||||
COLUMN_DEFAULT as column_default,
|
||||
COLUMN_COMMENT as column_comment
|
||||
COLUMN_DEFAULT as column_default
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
|
||||
ORDER BY ORDINAL_POSITION;
|
||||
`;
|
||||
|
||||
console.log(
|
||||
`📋 실행할 쿼리: ${query.trim()}, 파라미터: [${schemaName}, ${tableName}]`
|
||||
);
|
||||
|
||||
const queryPromise = this.connection!.query(query, [
|
||||
schemaName,
|
||||
tableName,
|
||||
]);
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error("컬럼 조회 타임아웃")), queryTimeout);
|
||||
});
|
||||
|
||||
const [rows] = (await Promise.race([
|
||||
queryPromise,
|
||||
timeoutPromise,
|
||||
])) as any;
|
||||
|
||||
console.log(
|
||||
`✅ MariaDB 컬럼 조회 완료: ${tableName}, ${rows ? rows.length : 0}개 컬럼`
|
||||
);
|
||||
`, [tableName]);
|
||||
|
||||
console.log(`[MariaDBConnector] 쿼리 결과:`, rows);
|
||||
console.log(`[MariaDBConnector] 결과 개수:`, Array.isArray(rows) ? rows.length : 'not array');
|
||||
|
||||
await this.disconnect();
|
||||
return rows as any[];
|
||||
} catch (error: any) {
|
||||
console.error(`[MariaDBConnector] getColumns 오류:`, error);
|
||||
await this.disconnect();
|
||||
throw new Error(`컬럼 정보 조회 실패: ${error.message}`);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-ignore
|
||||
import * as oracledb from 'oracledb';
|
||||
import { DatabaseConnector, ConnectionConfig, QueryResult } from '../interfaces/DatabaseConnector';
|
||||
import { ConnectionTestResult, TableInfo } from '../types/externalDbTypes';
|
||||
@@ -100,7 +101,7 @@ export class OracleConnector implements DatabaseConnector {
|
||||
|
||||
// Oracle XE 21c 쿼리 실행 옵션
|
||||
const options: any = {
|
||||
outFormat: oracledb.OUT_FORMAT_OBJECT, // OBJECT format
|
||||
outFormat: (oracledb as any).OUT_FORMAT_OBJECT, // OBJECT format
|
||||
maxRows: 10000, // XE 제한 고려
|
||||
fetchArraySize: 100
|
||||
};
|
||||
@@ -176,6 +177,8 @@ export class OracleConnector implements DatabaseConnector {
|
||||
|
||||
async getColumns(tableName: string): Promise<any[]> {
|
||||
try {
|
||||
console.log(`[OracleConnector] getColumns 호출: tableName=${tableName}`);
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
column_name,
|
||||
@@ -190,16 +193,23 @@ export class OracleConnector implements DatabaseConnector {
|
||||
ORDER BY column_id
|
||||
`;
|
||||
|
||||
console.log(`[OracleConnector] 쿼리 실행 시작: ${query}`);
|
||||
const result = await this.executeQuery(query, [tableName]);
|
||||
|
||||
return result.rows.map((row: any) => ({
|
||||
console.log(`[OracleConnector] 쿼리 결과:`, result.rows);
|
||||
console.log(`[OracleConnector] 결과 개수:`, result.rows ? result.rows.length : 'null/undefined');
|
||||
|
||||
const mappedResult = result.rows.map((row: any) => ({
|
||||
column_name: row.COLUMN_NAME,
|
||||
data_type: this.formatOracleDataType(row),
|
||||
is_nullable: row.NULLABLE === 'Y' ? 'YES' : 'NO',
|
||||
column_default: row.DATA_DEFAULT
|
||||
}));
|
||||
|
||||
console.log(`[OracleConnector] 매핑된 결과:`, mappedResult);
|
||||
return mappedResult;
|
||||
} catch (error: any) {
|
||||
console.error('Oracle 테이블 컬럼 조회 실패:', error);
|
||||
console.error('[OracleConnector] getColumns 오류:', error);
|
||||
throw new Error(`테이블 컬럼 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
261
backend-node/src/database/RestApiConnector.ts
Normal file
261
backend-node/src/database/RestApiConnector.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user