워크플로우 restapi도 연결가능하고여러개 가능하게 구현시켜놓음

This commit is contained in:
leeheejin
2025-12-02 14:24:43 +09:00
parent 30e6595bf3
commit 9078873240
11 changed files with 989 additions and 50 deletions

View File

@@ -66,11 +66,12 @@ export class FlowController {
return;
}
// REST API인 경우 테이블 존재 확인 스킵
const isRestApi = dbSourceType === "restapi";
// REST API 또는 다중 연결인 경우 테이블 존재 확인 스킵
const isRestApi = dbSourceType === "restapi" || dbSourceType === "multi_restapi";
const isMultiConnection = dbSourceType === "multi_restapi" || dbSourceType === "multi_external_db";
// 테이블 이름이 제공된 경우에만 존재 확인 (REST API 제외)
if (tableName && !isRestApi && !tableName.startsWith("_restapi_")) {
// 테이블 이름이 제공된 경우에만 존재 확인 (REST API 및 다중 연결 제외)
if (tableName && !isRestApi && !isMultiConnection && !tableName.startsWith("_restapi_") && !tableName.startsWith("_multi_restapi_") && !tableName.startsWith("_multi_external_db_")) {
const tableExists =
await this.flowDefinitionService.checkTableExists(tableName);
if (!tableExists) {
@@ -92,6 +93,7 @@ export class FlowController {
restApiConnectionId,
restApiEndpoint,
restApiJsonPath,
restApiConnections: req.body.restApiConnections, // 다중 REST API 설정
},
userId,
userCompanyCode

View File

@@ -1091,4 +1091,150 @@ export class ExternalRestApiConnectionService {
throw new Error("올바르지 않은 인증 타입입니다.");
}
}
/**
* 다중 REST API 데이터 조회 및 병합
* 여러 REST API의 응답을 병합하여 하나의 데이터셋으로 반환
*/
static async fetchMultipleData(
configs: Array<{
connectionId: number;
endpoint: string;
jsonPath: string;
alias: string;
}>,
userCompanyCode?: string
): Promise<ApiResponse<{
rows: any[];
columns: Array<{ columnName: string; columnLabel: string; dataType: string; sourceApi: string }>;
total: number;
sources: Array<{ connectionId: number; connectionName: string; rowCount: number }>;
}>> {
try {
logger.info(`다중 REST API 데이터 조회 시작: ${configs.length}개 API`);
// 각 API에서 데이터 조회
const results = await Promise.all(
configs.map(async (config) => {
try {
const result = await this.fetchData(
config.connectionId,
config.endpoint,
config.jsonPath,
userCompanyCode
);
if (result.success && result.data) {
return {
success: true,
connectionId: config.connectionId,
connectionName: result.data.connectionInfo.connectionName,
alias: config.alias,
rows: result.data.rows,
columns: result.data.columns,
};
} else {
logger.warn(`API ${config.connectionId} 조회 실패:`, result.message);
return {
success: false,
connectionId: config.connectionId,
connectionName: "",
alias: config.alias,
rows: [],
columns: [],
error: result.message,
};
}
} catch (error) {
logger.error(`API ${config.connectionId} 조회 오류:`, error);
return {
success: false,
connectionId: config.connectionId,
connectionName: "",
alias: config.alias,
rows: [],
columns: [],
error: error instanceof Error ? error.message : "알 수 없는 오류",
};
}
})
);
// 성공한 결과만 필터링
const successfulResults = results.filter(r => r.success);
if (successfulResults.length === 0) {
return {
success: false,
message: "모든 REST API 조회에 실패했습니다.",
error: {
code: "ALL_APIS_FAILED",
details: results.map(r => ({ connectionId: r.connectionId, error: r.error })),
},
};
}
// 컬럼 병합 (별칭 적용)
const mergedColumns: Array<{ columnName: string; columnLabel: string; dataType: string; sourceApi: string }> = [];
for (const result of successfulResults) {
for (const col of result.columns) {
const prefixedColumnName = result.alias ? `${result.alias}${col.columnName}` : col.columnName;
mergedColumns.push({
columnName: prefixedColumnName,
columnLabel: `${col.columnLabel} (${result.connectionName})`,
dataType: col.dataType,
sourceApi: result.connectionName,
});
}
}
// 데이터 병합 (가로 병합: 각 API의 첫 번째 행끼리 병합)
// 참고: 실제 사용 시에는 조인 키가 필요할 수 있음
const maxRows = Math.max(...successfulResults.map(r => r.rows.length));
const mergedRows: any[] = [];
for (let i = 0; i < maxRows; i++) {
const mergedRow: any = {};
for (const result of successfulResults) {
const row = result.rows[i] || {};
for (const [key, value] of Object.entries(row)) {
const prefixedKey = result.alias ? `${result.alias}${key}` : key;
mergedRow[prefixedKey] = value;
}
}
mergedRows.push(mergedRow);
}
logger.info(`다중 REST API 데이터 병합 완료: ${mergedRows.length}개 행, ${mergedColumns.length}개 컬럼`);
return {
success: true,
data: {
rows: mergedRows,
columns: mergedColumns,
total: mergedRows.length,
sources: successfulResults.map(r => ({
connectionId: r.connectionId,
connectionName: r.connectionName,
rowCount: r.rows.length,
})),
},
message: `${successfulResults.length}개 API에서 총 ${mergedRows.length}개 데이터를 조회했습니다.`,
};
} catch (error) {
logger.error("다중 REST API 데이터 조회 오류:", error);
return {
success: false,
message: "다중 REST API 데이터 조회에 실패했습니다.",
error: {
code: "MULTI_FETCH_ERROR",
details: error instanceof Error ? error.message : "알 수 없는 오류",
},
};
}
}
}

View File

@@ -30,6 +30,7 @@ export class FlowDefinitionService {
restApiConnectionId: request.restApiConnectionId,
restApiEndpoint: request.restApiEndpoint,
restApiJsonPath: request.restApiJsonPath,
restApiConnections: request.restApiConnections,
companyCode,
userId,
});
@@ -38,9 +39,9 @@ export class FlowDefinitionService {
INSERT INTO flow_definition (
name, description, table_name, db_source_type, db_connection_id,
rest_api_connection_id, rest_api_endpoint, rest_api_json_path,
company_code, created_by
rest_api_connections, company_code, created_by
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *
`;
@@ -52,7 +53,8 @@ export class FlowDefinitionService {
request.dbConnectionId || null,
request.restApiConnectionId || null,
request.restApiEndpoint || null,
request.restApiJsonPath || "data",
request.restApiJsonPath || "response",
request.restApiConnections ? JSON.stringify(request.restApiConnections) : null,
companyCode,
userId,
];
@@ -209,6 +211,19 @@ export class FlowDefinitionService {
* DB 행을 FlowDefinition 객체로 변환
*/
private mapToFlowDefinition(row: any): FlowDefinition {
// rest_api_connections 파싱 (JSONB → 배열)
let restApiConnections = undefined;
if (row.rest_api_connections) {
try {
restApiConnections = typeof row.rest_api_connections === 'string'
? JSON.parse(row.rest_api_connections)
: row.rest_api_connections;
} catch (e) {
console.warn("Failed to parse rest_api_connections:", e);
restApiConnections = [];
}
}
return {
id: row.id,
name: row.name,
@@ -216,10 +231,12 @@ export class FlowDefinitionService {
tableName: row.table_name,
dbSourceType: row.db_source_type || "internal",
dbConnectionId: row.db_connection_id,
// REST API 관련 필드
// REST API 관련 필드 (단일)
restApiConnectionId: row.rest_api_connection_id,
restApiEndpoint: row.rest_api_endpoint,
restApiJsonPath: row.rest_api_json_path,
// 다중 REST API 관련 필드
restApiConnections: restApiConnections,
companyCode: row.company_code || "*",
isActive: row.is_active,
createdBy: row.created_by,

View File

@@ -2,18 +2,38 @@
* 플로우 관리 시스템 타입 정의
*/
// 다중 REST API 연결 설정
export interface RestApiConnectionConfig {
connectionId: number;
connectionName: string;
endpoint: string;
jsonPath: string;
alias: string; // 컬럼 접두어 (예: "api1_")
}
// 다중 외부 DB 연결 설정
export interface ExternalDbConnectionConfig {
connectionId: number;
connectionName: string;
dbType: string;
tableName: string;
alias: string; // 컬럼 접두어 (예: "db1_")
}
// 플로우 정의
export interface FlowDefinition {
id: number;
name: string;
description?: string;
tableName: string;
dbSourceType?: "internal" | "external" | "restapi"; // 데이터 소스 타입
dbSourceType?: "internal" | "external" | "restapi" | "multi_restapi" | "multi_external_db"; // 데이터 소스 타입
dbConnectionId?: number; // 외부 DB 연결 ID (external인 경우)
// REST API 관련 필드
// REST API 관련 필드 (단일)
restApiConnectionId?: number; // REST API 연결 ID (restapi인 경우)
restApiEndpoint?: string; // REST API 엔드포인트
restApiJsonPath?: string; // JSON 응답에서 데이터 경로 (기본: data)
// 다중 REST API 관련 필드
restApiConnections?: RestApiConnectionConfig[]; // 다중 REST API 설정 배열
companyCode: string; // 회사 코드 (* = 공통)
isActive: boolean;
createdBy?: string;
@@ -26,12 +46,14 @@ export interface CreateFlowDefinitionRequest {
name: string;
description?: string;
tableName: string;
dbSourceType?: "internal" | "external" | "restapi"; // 데이터 소스 타입
dbSourceType?: "internal" | "external" | "restapi" | "multi_restapi" | "multi_external_db"; // 데이터 소스 타입
dbConnectionId?: number; // 외부 DB 연결 ID
// REST API 관련 필드
// REST API 관련 필드 (단일)
restApiConnectionId?: number; // REST API 연결 ID
restApiEndpoint?: string; // REST API 엔드포인트
restApiJsonPath?: string; // JSON 응답에서 데이터 경로
// 다중 REST API 관련 필드
restApiConnections?: RestApiConnectionConfig[]; // 다중 REST API 설정 배열
companyCode?: string; // 회사 코드 (미제공 시 사용자의 company_code 사용)
}