워크플로우 restapi도 연결가능하고여러개 가능하게 구현시켜놓음
This commit is contained in:
parent
30e6595bf3
commit
9078873240
|
|
@ -66,11 +66,12 @@ export class FlowController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// REST API인 경우 테이블 존재 확인 스킵
|
// REST API 또는 다중 연결인 경우 테이블 존재 확인 스킵
|
||||||
const isRestApi = dbSourceType === "restapi";
|
const isRestApi = dbSourceType === "restapi" || dbSourceType === "multi_restapi";
|
||||||
|
const isMultiConnection = dbSourceType === "multi_restapi" || dbSourceType === "multi_external_db";
|
||||||
|
|
||||||
// 테이블 이름이 제공된 경우에만 존재 확인 (REST API 제외)
|
// 테이블 이름이 제공된 경우에만 존재 확인 (REST API 및 다중 연결 제외)
|
||||||
if (tableName && !isRestApi && !tableName.startsWith("_restapi_")) {
|
if (tableName && !isRestApi && !isMultiConnection && !tableName.startsWith("_restapi_") && !tableName.startsWith("_multi_restapi_") && !tableName.startsWith("_multi_external_db_")) {
|
||||||
const tableExists =
|
const tableExists =
|
||||||
await this.flowDefinitionService.checkTableExists(tableName);
|
await this.flowDefinitionService.checkTableExists(tableName);
|
||||||
if (!tableExists) {
|
if (!tableExists) {
|
||||||
|
|
@ -92,6 +93,7 @@ export class FlowController {
|
||||||
restApiConnectionId,
|
restApiConnectionId,
|
||||||
restApiEndpoint,
|
restApiEndpoint,
|
||||||
restApiJsonPath,
|
restApiJsonPath,
|
||||||
|
restApiConnections: req.body.restApiConnections, // 다중 REST API 설정
|
||||||
},
|
},
|
||||||
userId,
|
userId,
|
||||||
userCompanyCode
|
userCompanyCode
|
||||||
|
|
|
||||||
|
|
@ -1091,4 +1091,150 @@ export class ExternalRestApiConnectionService {
|
||||||
throw new Error("올바르지 않은 인증 타입입니다.");
|
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 : "알 수 없는 오류",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export class FlowDefinitionService {
|
||||||
restApiConnectionId: request.restApiConnectionId,
|
restApiConnectionId: request.restApiConnectionId,
|
||||||
restApiEndpoint: request.restApiEndpoint,
|
restApiEndpoint: request.restApiEndpoint,
|
||||||
restApiJsonPath: request.restApiJsonPath,
|
restApiJsonPath: request.restApiJsonPath,
|
||||||
|
restApiConnections: request.restApiConnections,
|
||||||
companyCode,
|
companyCode,
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
|
@ -38,9 +39,9 @@ export class FlowDefinitionService {
|
||||||
INSERT INTO flow_definition (
|
INSERT INTO flow_definition (
|
||||||
name, description, table_name, db_source_type, db_connection_id,
|
name, description, table_name, db_source_type, db_connection_id,
|
||||||
rest_api_connection_id, rest_api_endpoint, rest_api_json_path,
|
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 *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -52,7 +53,8 @@ export class FlowDefinitionService {
|
||||||
request.dbConnectionId || null,
|
request.dbConnectionId || null,
|
||||||
request.restApiConnectionId || null,
|
request.restApiConnectionId || null,
|
||||||
request.restApiEndpoint || null,
|
request.restApiEndpoint || null,
|
||||||
request.restApiJsonPath || "data",
|
request.restApiJsonPath || "response",
|
||||||
|
request.restApiConnections ? JSON.stringify(request.restApiConnections) : null,
|
||||||
companyCode,
|
companyCode,
|
||||||
userId,
|
userId,
|
||||||
];
|
];
|
||||||
|
|
@ -209,6 +211,19 @@ export class FlowDefinitionService {
|
||||||
* DB 행을 FlowDefinition 객체로 변환
|
* DB 행을 FlowDefinition 객체로 변환
|
||||||
*/
|
*/
|
||||||
private mapToFlowDefinition(row: any): 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 {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
|
|
@ -216,10 +231,12 @@ export class FlowDefinitionService {
|
||||||
tableName: row.table_name,
|
tableName: row.table_name,
|
||||||
dbSourceType: row.db_source_type || "internal",
|
dbSourceType: row.db_source_type || "internal",
|
||||||
dbConnectionId: row.db_connection_id,
|
dbConnectionId: row.db_connection_id,
|
||||||
// REST API 관련 필드
|
// REST API 관련 필드 (단일)
|
||||||
restApiConnectionId: row.rest_api_connection_id,
|
restApiConnectionId: row.rest_api_connection_id,
|
||||||
restApiEndpoint: row.rest_api_endpoint,
|
restApiEndpoint: row.rest_api_endpoint,
|
||||||
restApiJsonPath: row.rest_api_json_path,
|
restApiJsonPath: row.rest_api_json_path,
|
||||||
|
// 다중 REST API 관련 필드
|
||||||
|
restApiConnections: restApiConnections,
|
||||||
companyCode: row.company_code || "*",
|
companyCode: row.company_code || "*",
|
||||||
isActive: row.is_active,
|
isActive: row.is_active,
|
||||||
createdBy: row.created_by,
|
createdBy: row.created_by,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
export interface FlowDefinition {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
tableName: string;
|
tableName: string;
|
||||||
dbSourceType?: "internal" | "external" | "restapi"; // 데이터 소스 타입
|
dbSourceType?: "internal" | "external" | "restapi" | "multi_restapi" | "multi_external_db"; // 데이터 소스 타입
|
||||||
dbConnectionId?: number; // 외부 DB 연결 ID (external인 경우)
|
dbConnectionId?: number; // 외부 DB 연결 ID (external인 경우)
|
||||||
// REST API 관련 필드
|
// REST API 관련 필드 (단일)
|
||||||
restApiConnectionId?: number; // REST API 연결 ID (restapi인 경우)
|
restApiConnectionId?: number; // REST API 연결 ID (restapi인 경우)
|
||||||
restApiEndpoint?: string; // REST API 엔드포인트
|
restApiEndpoint?: string; // REST API 엔드포인트
|
||||||
restApiJsonPath?: string; // JSON 응답에서 데이터 경로 (기본: data)
|
restApiJsonPath?: string; // JSON 응답에서 데이터 경로 (기본: data)
|
||||||
|
// 다중 REST API 관련 필드
|
||||||
|
restApiConnections?: RestApiConnectionConfig[]; // 다중 REST API 설정 배열
|
||||||
companyCode: string; // 회사 코드 (* = 공통)
|
companyCode: string; // 회사 코드 (* = 공통)
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
|
|
@ -26,12 +46,14 @@ export interface CreateFlowDefinitionRequest {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
tableName: string;
|
tableName: string;
|
||||||
dbSourceType?: "internal" | "external" | "restapi"; // 데이터 소스 타입
|
dbSourceType?: "internal" | "external" | "restapi" | "multi_restapi" | "multi_external_db"; // 데이터 소스 타입
|
||||||
dbConnectionId?: number; // 외부 DB 연결 ID
|
dbConnectionId?: number; // 외부 DB 연결 ID
|
||||||
// REST API 관련 필드
|
// REST API 관련 필드 (단일)
|
||||||
restApiConnectionId?: number; // REST API 연결 ID
|
restApiConnectionId?: number; // REST API 연결 ID
|
||||||
restApiEndpoint?: string; // REST API 엔드포인트
|
restApiEndpoint?: string; // REST API 엔드포인트
|
||||||
restApiJsonPath?: string; // JSON 응답에서 데이터 경로
|
restApiJsonPath?: string; // JSON 응답에서 데이터 경로
|
||||||
|
// 다중 REST API 관련 필드
|
||||||
|
restApiConnections?: RestApiConnectionConfig[]; // 다중 REST API 설정 배열
|
||||||
companyCode?: string; // 회사 코드 (미제공 시 사용자의 company_code 사용)
|
companyCode?: string; // 회사 코드 (미제공 시 사용자의 company_code 사용)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -319,6 +319,10 @@ export default function FlowEditorPage() {
|
||||||
flowTableName={flowDefinition?.tableName} // 플로우 정의의 테이블명 전달
|
flowTableName={flowDefinition?.tableName} // 플로우 정의의 테이블명 전달
|
||||||
flowDbSourceType={flowDefinition?.dbSourceType} // DB 소스 타입 전달
|
flowDbSourceType={flowDefinition?.dbSourceType} // DB 소스 타입 전달
|
||||||
flowDbConnectionId={flowDefinition?.dbConnectionId} // 외부 DB 연결 ID 전달
|
flowDbConnectionId={flowDefinition?.dbConnectionId} // 외부 DB 연결 ID 전달
|
||||||
|
flowRestApiConnectionId={flowDefinition?.restApiConnectionId} // REST API 연결 ID 전달
|
||||||
|
flowRestApiEndpoint={flowDefinition?.restApiEndpoint} // REST API 엔드포인트 전달
|
||||||
|
flowRestApiJsonPath={flowDefinition?.restApiJsonPath} // REST API JSON 경로 전달
|
||||||
|
flowRestApiConnections={flowDefinition?.restApiConnections} // 다중 REST API 설정 전달
|
||||||
onClose={() => setSelectedStep(null)}
|
onClose={() => setSelectedStep(null)}
|
||||||
onUpdate={loadFlowData}
|
onUpdate={loadFlowData}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,30 @@ export default function FlowManagementPage() {
|
||||||
// REST API 연결 관련 상태
|
// REST API 연결 관련 상태
|
||||||
const [restApiConnections, setRestApiConnections] = useState<ExternalRestApiConnection[]>([]);
|
const [restApiConnections, setRestApiConnections] = useState<ExternalRestApiConnection[]>([]);
|
||||||
const [restApiEndpoint, setRestApiEndpoint] = useState("");
|
const [restApiEndpoint, setRestApiEndpoint] = useState("");
|
||||||
const [restApiJsonPath, setRestApiJsonPath] = useState("data");
|
const [restApiJsonPath, setRestApiJsonPath] = useState("response");
|
||||||
|
|
||||||
|
// 다중 REST API 선택 상태
|
||||||
|
interface RestApiConfig {
|
||||||
|
connectionId: number;
|
||||||
|
connectionName: string;
|
||||||
|
endpoint: string;
|
||||||
|
jsonPath: string;
|
||||||
|
alias: string; // 컬럼 접두어 (예: "api1_")
|
||||||
|
}
|
||||||
|
const [selectedRestApis, setSelectedRestApis] = useState<RestApiConfig[]>([]);
|
||||||
|
const [isMultiRestApi, setIsMultiRestApi] = useState(false); // 다중 REST API 모드
|
||||||
|
|
||||||
|
// 다중 외부 DB 선택 상태
|
||||||
|
interface ExternalDbConfig {
|
||||||
|
connectionId: number;
|
||||||
|
connectionName: string;
|
||||||
|
dbType: string;
|
||||||
|
tableName: string;
|
||||||
|
alias: string; // 컬럼 접두어 (예: "db1_")
|
||||||
|
}
|
||||||
|
const [selectedExternalDbs, setSelectedExternalDbs] = useState<ExternalDbConfig[]>([]);
|
||||||
|
const [isMultiExternalDb, setIsMultiExternalDb] = useState(false); // 다중 외부 DB 모드
|
||||||
|
const [multiDbTableLists, setMultiDbTableLists] = useState<Record<number, string[]>>({}); // 각 DB별 테이블 목록
|
||||||
|
|
||||||
// 생성 폼 상태
|
// 생성 폼 상태
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
|
|
@ -207,25 +230,161 @@ export default function FlowManagementPage() {
|
||||||
}
|
}
|
||||||
}, [selectedDbSource]);
|
}, [selectedDbSource]);
|
||||||
|
|
||||||
|
// 다중 외부 DB 추가
|
||||||
|
const addExternalDbConfig = async (connectionId: number) => {
|
||||||
|
const connection = externalConnections.find(c => c.id === connectionId);
|
||||||
|
if (!connection) return;
|
||||||
|
|
||||||
|
// 이미 추가된 경우 스킵
|
||||||
|
if (selectedExternalDbs.some(db => db.connectionId === connectionId)) {
|
||||||
|
toast({
|
||||||
|
title: "이미 추가됨",
|
||||||
|
description: "해당 외부 DB가 이미 추가되어 있습니다.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 해당 DB의 테이블 목록 로드
|
||||||
|
try {
|
||||||
|
const data = await ExternalDbConnectionAPI.getTables(connectionId);
|
||||||
|
if (data.success && data.data) {
|
||||||
|
const tables = Array.isArray(data.data) ? data.data : [];
|
||||||
|
const tableNames = tables
|
||||||
|
.map((t: string | { tableName?: string; table_name?: string; tablename?: string; name?: string }) =>
|
||||||
|
typeof t === "string" ? t : t.tableName || t.table_name || t.tablename || t.name,
|
||||||
|
)
|
||||||
|
.filter(Boolean);
|
||||||
|
setMultiDbTableLists(prev => ({ ...prev, [connectionId]: tableNames }));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("외부 DB 테이블 목록 조회 오류:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newConfig: ExternalDbConfig = {
|
||||||
|
connectionId,
|
||||||
|
connectionName: connection.connection_name,
|
||||||
|
dbType: connection.db_type,
|
||||||
|
tableName: "",
|
||||||
|
alias: `db${selectedExternalDbs.length + 1}_`, // 자동 별칭 생성
|
||||||
|
};
|
||||||
|
|
||||||
|
setSelectedExternalDbs([...selectedExternalDbs, newConfig]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 다중 외부 DB 삭제
|
||||||
|
const removeExternalDbConfig = (connectionId: number) => {
|
||||||
|
setSelectedExternalDbs(selectedExternalDbs.filter(db => db.connectionId !== connectionId));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 다중 외부 DB 설정 업데이트
|
||||||
|
const updateExternalDbConfig = (connectionId: number, field: keyof ExternalDbConfig, value: string) => {
|
||||||
|
setSelectedExternalDbs(selectedExternalDbs.map(db =>
|
||||||
|
db.connectionId === connectionId ? { ...db, [field]: value } : db
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 다중 REST API 추가
|
||||||
|
const addRestApiConfig = (connectionId: number) => {
|
||||||
|
const connection = restApiConnections.find(c => c.id === connectionId);
|
||||||
|
if (!connection) return;
|
||||||
|
|
||||||
|
// 이미 추가된 경우 스킵
|
||||||
|
if (selectedRestApis.some(api => api.connectionId === connectionId)) {
|
||||||
|
toast({
|
||||||
|
title: "이미 추가됨",
|
||||||
|
description: "해당 REST API가 이미 추가되어 있습니다.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 연결 테이블의 기본값 사용
|
||||||
|
const newConfig: RestApiConfig = {
|
||||||
|
connectionId,
|
||||||
|
connectionName: connection.connection_name,
|
||||||
|
endpoint: connection.endpoint_path || "", // 연결 테이블의 기본 엔드포인트
|
||||||
|
jsonPath: "response", // 기본값
|
||||||
|
alias: `api${selectedRestApis.length + 1}_`, // 자동 별칭 생성
|
||||||
|
};
|
||||||
|
|
||||||
|
setSelectedRestApis([...selectedRestApis, newConfig]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 다중 REST API 삭제
|
||||||
|
const removeRestApiConfig = (connectionId: number) => {
|
||||||
|
setSelectedRestApis(selectedRestApis.filter(api => api.connectionId !== connectionId));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 다중 REST API 설정 업데이트
|
||||||
|
const updateRestApiConfig = (connectionId: number, field: keyof RestApiConfig, value: string) => {
|
||||||
|
setSelectedRestApis(selectedRestApis.map(api =>
|
||||||
|
api.connectionId === connectionId ? { ...api, [field]: value } : api
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
// 플로우 생성
|
// 플로우 생성
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
console.log("🚀 handleCreate called with formData:", formData);
|
console.log("🚀 handleCreate called with formData:", formData);
|
||||||
|
|
||||||
// REST API인 경우 테이블 이름 검증 스킵
|
// REST API 또는 다중 선택인 경우 테이블 이름 검증 스킵
|
||||||
const isRestApi = selectedDbSource.startsWith("restapi_");
|
const isRestApi = selectedDbSource.startsWith("restapi_") || isMultiRestApi;
|
||||||
|
const isMultiMode = isMultiRestApi || isMultiExternalDb;
|
||||||
|
|
||||||
if (!formData.name || (!isRestApi && !formData.tableName)) {
|
if (!formData.name || (!isRestApi && !isMultiMode && !formData.tableName)) {
|
||||||
console.log("❌ Validation failed:", { name: formData.name, tableName: formData.tableName, isRestApi });
|
console.log("❌ Validation failed:", { name: formData.name, tableName: formData.tableName, isRestApi, isMultiMode });
|
||||||
toast({
|
toast({
|
||||||
title: "입력 오류",
|
title: "입력 오류",
|
||||||
description: isRestApi ? "플로우 이름은 필수입니다." : "플로우 이름과 테이블 이름은 필수입니다.",
|
description: (isRestApi || isMultiMode) ? "플로우 이름은 필수입니다." : "플로우 이름과 테이블 이름은 필수입니다.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// REST API인 경우 엔드포인트 검증
|
// 다중 REST API 모드인 경우 검증
|
||||||
if (isRestApi && !restApiEndpoint) {
|
if (isMultiRestApi) {
|
||||||
|
if (selectedRestApis.length === 0) {
|
||||||
|
toast({
|
||||||
|
title: "입력 오류",
|
||||||
|
description: "최소 하나의 REST API를 추가해주세요.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 각 API의 엔드포인트 검증
|
||||||
|
const missingEndpoint = selectedRestApis.find(api => !api.endpoint);
|
||||||
|
if (missingEndpoint) {
|
||||||
|
toast({
|
||||||
|
title: "입력 오류",
|
||||||
|
description: `${missingEndpoint.connectionName}의 엔드포인트를 입력해주세요.`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (isMultiExternalDb) {
|
||||||
|
// 다중 외부 DB 모드인 경우 검증
|
||||||
|
if (selectedExternalDbs.length === 0) {
|
||||||
|
toast({
|
||||||
|
title: "입력 오류",
|
||||||
|
description: "최소 하나의 외부 DB를 추가해주세요.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 각 DB의 테이블 선택 검증
|
||||||
|
const missingTable = selectedExternalDbs.find(db => !db.tableName);
|
||||||
|
if (missingTable) {
|
||||||
|
toast({
|
||||||
|
title: "입력 오류",
|
||||||
|
description: `${missingTable.connectionName}의 테이블을 선택해주세요.`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (isRestApi && !restApiEndpoint) {
|
||||||
|
// 단일 REST API인 경우 엔드포인트 검증
|
||||||
toast({
|
toast({
|
||||||
title: "입력 오류",
|
title: "입력 오류",
|
||||||
description: "REST API 엔드포인트는 필수입니다.",
|
description: "REST API 엔드포인트는 필수입니다.",
|
||||||
|
|
@ -236,11 +395,15 @@ export default function FlowManagementPage() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 데이터 소스 타입 및 ID 파싱
|
// 데이터 소스 타입 및 ID 파싱
|
||||||
let dbSourceType: "internal" | "external" | "restapi" = "internal";
|
let dbSourceType: "internal" | "external" | "restapi" | "multi_restapi" | "multi_external_db" = "internal";
|
||||||
let dbConnectionId: number | undefined = undefined;
|
let dbConnectionId: number | undefined = undefined;
|
||||||
let restApiConnectionId: number | undefined = undefined;
|
let restApiConnectionId: number | undefined = undefined;
|
||||||
|
|
||||||
if (selectedDbSource === "internal") {
|
if (isMultiRestApi) {
|
||||||
|
dbSourceType = "multi_restapi";
|
||||||
|
} else if (isMultiExternalDb) {
|
||||||
|
dbSourceType = "multi_external_db";
|
||||||
|
} else if (selectedDbSource === "internal") {
|
||||||
dbSourceType = "internal";
|
dbSourceType = "internal";
|
||||||
} else if (selectedDbSource.startsWith("external_db_")) {
|
} else if (selectedDbSource.startsWith("external_db_")) {
|
||||||
dbSourceType = "external";
|
dbSourceType = "external";
|
||||||
|
|
@ -257,11 +420,27 @@ export default function FlowManagementPage() {
|
||||||
dbConnectionId,
|
dbConnectionId,
|
||||||
};
|
};
|
||||||
|
|
||||||
// REST API인 경우 추가 정보
|
// 다중 REST API인 경우
|
||||||
if (dbSourceType === "restapi") {
|
if (dbSourceType === "multi_restapi") {
|
||||||
|
requestData.restApiConnections = selectedRestApis;
|
||||||
|
// 다중 REST API는 첫 번째 API의 ID를 기본으로 사용
|
||||||
|
requestData.restApiConnectionId = selectedRestApis[0]?.connectionId;
|
||||||
|
requestData.restApiEndpoint = selectedRestApis[0]?.endpoint;
|
||||||
|
requestData.restApiJsonPath = selectedRestApis[0]?.jsonPath || "response";
|
||||||
|
// 가상 테이블명: 모든 연결 ID를 조합
|
||||||
|
requestData.tableName = `_multi_restapi_${selectedRestApis.map(a => a.connectionId).join("_")}`;
|
||||||
|
} else if (dbSourceType === "multi_external_db") {
|
||||||
|
// 다중 외부 DB인 경우
|
||||||
|
requestData.externalDbConnections = selectedExternalDbs;
|
||||||
|
// 첫 번째 DB의 ID를 기본으로 사용
|
||||||
|
requestData.dbConnectionId = selectedExternalDbs[0]?.connectionId;
|
||||||
|
// 가상 테이블명: 모든 연결 ID와 테이블명 조합
|
||||||
|
requestData.tableName = `_multi_external_db_${selectedExternalDbs.map(db => `${db.connectionId}_${db.tableName}`).join("_")}`;
|
||||||
|
} else if (dbSourceType === "restapi") {
|
||||||
|
// 단일 REST API인 경우
|
||||||
requestData.restApiConnectionId = restApiConnectionId;
|
requestData.restApiConnectionId = restApiConnectionId;
|
||||||
requestData.restApiEndpoint = restApiEndpoint;
|
requestData.restApiEndpoint = restApiEndpoint;
|
||||||
requestData.restApiJsonPath = restApiJsonPath || "data";
|
requestData.restApiJsonPath = restApiJsonPath || "response";
|
||||||
// REST API는 가상 테이블명 사용
|
// REST API는 가상 테이블명 사용
|
||||||
requestData.tableName = `_restapi_${restApiConnectionId}`;
|
requestData.tableName = `_restapi_${restApiConnectionId}`;
|
||||||
}
|
}
|
||||||
|
|
@ -277,7 +456,11 @@ export default function FlowManagementPage() {
|
||||||
setFormData({ name: "", description: "", tableName: "" });
|
setFormData({ name: "", description: "", tableName: "" });
|
||||||
setSelectedDbSource("internal");
|
setSelectedDbSource("internal");
|
||||||
setRestApiEndpoint("");
|
setRestApiEndpoint("");
|
||||||
setRestApiJsonPath("data");
|
setRestApiJsonPath("response");
|
||||||
|
setSelectedRestApis([]);
|
||||||
|
setSelectedExternalDbs([]);
|
||||||
|
setIsMultiRestApi(false);
|
||||||
|
setIsMultiExternalDb(false);
|
||||||
loadFlows();
|
loadFlows();
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
|
|
@ -485,13 +668,27 @@ export default function FlowManagementPage() {
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs sm:text-sm">데이터 소스</Label>
|
<Label className="text-xs sm:text-sm">데이터 소스</Label>
|
||||||
<Select
|
<Select
|
||||||
value={selectedDbSource}
|
value={isMultiRestApi ? "multi_restapi" : isMultiExternalDb ? "multi_external_db" : selectedDbSource}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setSelectedDbSource(value);
|
if (value === "multi_restapi") {
|
||||||
// 소스 변경 시 테이블 선택 및 REST API 설정 초기화
|
setIsMultiRestApi(true);
|
||||||
|
setIsMultiExternalDb(false);
|
||||||
|
setSelectedDbSource("internal");
|
||||||
|
} else if (value === "multi_external_db") {
|
||||||
|
setIsMultiExternalDb(true);
|
||||||
|
setIsMultiRestApi(false);
|
||||||
|
setSelectedDbSource("internal");
|
||||||
|
} else {
|
||||||
|
setIsMultiRestApi(false);
|
||||||
|
setIsMultiExternalDb(false);
|
||||||
|
setSelectedDbSource(value);
|
||||||
|
}
|
||||||
|
// 소스 변경 시 초기화
|
||||||
setFormData({ ...formData, tableName: "" });
|
setFormData({ ...formData, tableName: "" });
|
||||||
setRestApiEndpoint("");
|
setRestApiEndpoint("");
|
||||||
setRestApiJsonPath("data");
|
setRestApiJsonPath("response");
|
||||||
|
setSelectedRestApis([]);
|
||||||
|
setSelectedExternalDbs([]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||||
|
|
@ -504,7 +701,7 @@ export default function FlowManagementPage() {
|
||||||
{/* 외부 DB 연결 */}
|
{/* 외부 DB 연결 */}
|
||||||
{externalConnections.length > 0 && (
|
{externalConnections.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<SelectItem value="__divider_db__" disabled className="text-xs text-muted-foreground">
|
<SelectItem value="__divider_db__" disabled className="text-muted-foreground text-xs">
|
||||||
-- 외부 데이터베이스 --
|
-- 외부 데이터베이스 --
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
{externalConnections.map((conn) => (
|
{externalConnections.map((conn) => (
|
||||||
|
|
@ -518,7 +715,7 @@ export default function FlowManagementPage() {
|
||||||
{/* REST API 연결 */}
|
{/* REST API 연결 */}
|
||||||
{restApiConnections.length > 0 && (
|
{restApiConnections.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<SelectItem value="__divider_api__" disabled className="text-xs text-muted-foreground">
|
<SelectItem value="__divider_api__" disabled className="text-muted-foreground text-xs">
|
||||||
-- REST API --
|
-- REST API --
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
{restApiConnections.map((conn) => (
|
{restApiConnections.map((conn) => (
|
||||||
|
|
@ -528,6 +725,25 @@ export default function FlowManagementPage() {
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 다중 연결 옵션 */}
|
||||||
|
{(externalConnections.length > 0 || restApiConnections.length > 0) && (
|
||||||
|
<>
|
||||||
|
<SelectItem value="__divider_multi__" disabled className="text-muted-foreground text-xs">
|
||||||
|
-- 다중 연결 (데이터 병합) --
|
||||||
|
</SelectItem>
|
||||||
|
{externalConnections.length > 0 && (
|
||||||
|
<SelectItem value="multi_external_db">
|
||||||
|
다중 외부 DB (데이터 병합)
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
{restApiConnections.length > 0 && (
|
||||||
|
<SelectItem value="multi_restapi">
|
||||||
|
다중 REST API (데이터 병합)
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||||
|
|
@ -535,8 +751,160 @@ export default function FlowManagementPage() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* REST API인 경우 엔드포인트 설정 */}
|
{/* 다중 REST API 선택 UI */}
|
||||||
{selectedDbSource.startsWith("restapi_") ? (
|
{isMultiRestApi && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs sm:text-sm">REST API 연결 목록</Label>
|
||||||
|
<Select
|
||||||
|
value=""
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (value) {
|
||||||
|
addRestApiConfig(parseInt(value));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 w-[180px] text-xs sm:h-9 sm:text-sm">
|
||||||
|
<SelectValue placeholder="API 추가..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{restApiConnections
|
||||||
|
.filter(conn => !selectedRestApis.some(api => api.connectionId === conn.id))
|
||||||
|
.map((conn) => (
|
||||||
|
<SelectItem key={conn.id} value={String(conn.id)}>
|
||||||
|
{conn.connection_name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedRestApis.length === 0 ? (
|
||||||
|
<div className="rounded-md border border-dashed p-4 text-center">
|
||||||
|
<p className="text-muted-foreground text-xs sm:text-sm">
|
||||||
|
위에서 REST API를 추가해주세요
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{selectedRestApis.map((api) => (
|
||||||
|
<div key={api.connectionId} className="flex items-center justify-between rounded-md border bg-muted/30 px-3 py-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium">{api.connectionName}</span>
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
({api.endpoint || "기본 엔드포인트"})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
onClick={() => removeRestApiConfig(api.connectionId)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="text-muted-foreground text-[10px] sm:text-xs">
|
||||||
|
선택한 REST API들의 데이터가 자동으로 병합됩니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 다중 외부 DB 선택 UI */}
|
||||||
|
{isMultiExternalDb && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs sm:text-sm">외부 DB 연결 목록</Label>
|
||||||
|
<Select
|
||||||
|
value=""
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (value) {
|
||||||
|
addExternalDbConfig(parseInt(value));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 w-[180px] text-xs sm:h-9 sm:text-sm">
|
||||||
|
<SelectValue placeholder="DB 추가..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{externalConnections
|
||||||
|
.filter(conn => !selectedExternalDbs.some(db => db.connectionId === conn.id))
|
||||||
|
.map((conn) => (
|
||||||
|
<SelectItem key={conn.id} value={String(conn.id)}>
|
||||||
|
{conn.connection_name} ({conn.db_type?.toUpperCase()})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedExternalDbs.length === 0 ? (
|
||||||
|
<div className="rounded-md border border-dashed p-4 text-center">
|
||||||
|
<p className="text-muted-foreground text-xs sm:text-sm">
|
||||||
|
위에서 외부 DB를 추가해주세요
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{selectedExternalDbs.map((db) => (
|
||||||
|
<div key={db.connectionId} className="rounded-md border p-3 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{db.connectionName} ({db.dbType?.toUpperCase()})
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
onClick={() => removeExternalDbConfig(db.connectionId)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<Label className="text-[10px]">테이블</Label>
|
||||||
|
<Select
|
||||||
|
value={db.tableName}
|
||||||
|
onValueChange={(value) => updateExternalDbConfig(db.connectionId, "tableName", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-7 text-xs">
|
||||||
|
<SelectValue placeholder="테이블 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{(multiDbTableLists[db.connectionId] || []).map((table) => (
|
||||||
|
<SelectItem key={table} value={table}>
|
||||||
|
{table}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-[10px]">별칭 (접두어)</Label>
|
||||||
|
<Input
|
||||||
|
value={db.alias}
|
||||||
|
onChange={(e) => updateExternalDbConfig(db.connectionId, "alias", e.target.value)}
|
||||||
|
placeholder="db1_"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="text-muted-foreground text-[10px] sm:text-xs">
|
||||||
|
선택한 외부 DB들의 데이터가 자동으로 병합됩니다. 각 DB별 테이블을 선택해주세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 단일 REST API인 경우 엔드포인트 설정 */}
|
||||||
|
{!isMultiRestApi && selectedDbSource.startsWith("restapi_") && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="restApiEndpoint" className="text-xs sm:text-sm">
|
<Label htmlFor="restApiEndpoint" className="text-xs sm:text-sm">
|
||||||
|
|
@ -569,8 +937,10 @@ export default function FlowManagementPage() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
)}
|
||||||
/* 테이블 선택 (내부 DB 또는 외부 DB) */
|
|
||||||
|
{/* 테이블 선택 (내부 DB 또는 단일 외부 DB - 다중 선택 모드가 아닌 경우만) */}
|
||||||
|
{!isMultiRestApi && !isMultiExternalDb && !selectedDbSource.startsWith("restapi_") && (
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="tableName" className="text-xs sm:text-sm">
|
<Label htmlFor="tableName" className="text-xs sm:text-sm">
|
||||||
연결 테이블 *
|
연결 테이블 *
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,27 @@ import { Label } from "@/components/ui/label";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { FlowConditionGroup, FlowCondition, ConditionOperator } from "@/types/flow";
|
import { FlowConditionGroup, FlowCondition, ConditionOperator } from "@/types/flow";
|
||||||
import { getTableColumns } from "@/lib/api/tableManagement";
|
import { getTableColumns } from "@/lib/api/tableManagement";
|
||||||
|
import { ExternalRestApiConnectionAPI } from "@/lib/api/externalRestApiConnection";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
// 다중 REST API 연결 설정
|
||||||
|
interface RestApiConnectionConfig {
|
||||||
|
connectionId: number;
|
||||||
|
connectionName: string;
|
||||||
|
endpoint: string;
|
||||||
|
jsonPath: string;
|
||||||
|
alias: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface FlowConditionBuilderProps {
|
interface FlowConditionBuilderProps {
|
||||||
flowId: number;
|
flowId: number;
|
||||||
tableName?: string; // 조회할 테이블명
|
tableName?: string; // 조회할 테이블명
|
||||||
dbSourceType?: "internal" | "external"; // DB 소스 타입
|
dbSourceType?: "internal" | "external" | "restapi" | "multi_restapi"; // DB 소스 타입
|
||||||
dbConnectionId?: number; // 외부 DB 연결 ID
|
dbConnectionId?: number; // 외부 DB 연결 ID
|
||||||
|
restApiConnectionId?: number; // REST API 연결 ID (단일)
|
||||||
|
restApiEndpoint?: string; // REST API 엔드포인트 (단일)
|
||||||
|
restApiJsonPath?: string; // REST API JSON 경로 (단일)
|
||||||
|
restApiConnections?: RestApiConnectionConfig[]; // 다중 REST API 설정
|
||||||
condition?: FlowConditionGroup;
|
condition?: FlowConditionGroup;
|
||||||
onChange: (condition: FlowConditionGroup | undefined) => void;
|
onChange: (condition: FlowConditionGroup | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -45,6 +59,10 @@ export function FlowConditionBuilder({
|
||||||
tableName,
|
tableName,
|
||||||
dbSourceType = "internal",
|
dbSourceType = "internal",
|
||||||
dbConnectionId,
|
dbConnectionId,
|
||||||
|
restApiConnectionId,
|
||||||
|
restApiEndpoint,
|
||||||
|
restApiJsonPath,
|
||||||
|
restApiConnections,
|
||||||
condition,
|
condition,
|
||||||
onChange,
|
onChange,
|
||||||
}: FlowConditionBuilderProps) {
|
}: FlowConditionBuilderProps) {
|
||||||
|
|
@ -65,9 +83,10 @@ export function FlowConditionBuilder({
|
||||||
}
|
}
|
||||||
}, [condition]);
|
}, [condition]);
|
||||||
|
|
||||||
// 테이블 컬럼 로드 - 내부/외부 DB 모두 지원
|
// 테이블 컬럼 로드 - 내부/외부 DB 및 REST API 모두 지원
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!tableName) {
|
// REST API인 경우 tableName이 없어도 진행 가능
|
||||||
|
if (!tableName && dbSourceType !== "restapi" && dbSourceType !== "multi_restapi") {
|
||||||
setColumns([]);
|
setColumns([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -79,8 +98,106 @@ export function FlowConditionBuilder({
|
||||||
tableName,
|
tableName,
|
||||||
dbSourceType,
|
dbSourceType,
|
||||||
dbConnectionId,
|
dbConnectionId,
|
||||||
|
restApiConnectionId,
|
||||||
|
restApiEndpoint,
|
||||||
|
restApiJsonPath,
|
||||||
|
restApiConnections,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 다중 REST API인 경우
|
||||||
|
if (dbSourceType === "multi_restapi" && restApiConnections && restApiConnections.length > 0) {
|
||||||
|
try {
|
||||||
|
console.log("🌐 [FlowConditionBuilder] 다중 REST API 컬럼 로드 시작:", restApiConnections);
|
||||||
|
|
||||||
|
// 각 API에서 컬럼 정보 수집
|
||||||
|
const allColumns: any[] = [];
|
||||||
|
|
||||||
|
for (const config of restApiConnections) {
|
||||||
|
try {
|
||||||
|
const effectiveJsonPath = (!config.jsonPath || config.jsonPath === "data") ? "response" : config.jsonPath;
|
||||||
|
|
||||||
|
const restApiData = await ExternalRestApiConnectionAPI.fetchData(
|
||||||
|
config.connectionId,
|
||||||
|
config.endpoint,
|
||||||
|
effectiveJsonPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (restApiData.columns && restApiData.columns.length > 0) {
|
||||||
|
// 별칭 적용
|
||||||
|
const prefixedColumns = restApiData.columns.map((col) => ({
|
||||||
|
column_name: config.alias ? `${config.alias}${col.columnName}` : col.columnName,
|
||||||
|
data_type: col.dataType || "varchar",
|
||||||
|
displayName: `${col.columnLabel || col.columnName} (${config.connectionName})`,
|
||||||
|
sourceApi: config.connectionName,
|
||||||
|
}));
|
||||||
|
allColumns.push(...prefixedColumns);
|
||||||
|
}
|
||||||
|
} catch (apiError) {
|
||||||
|
console.warn(`API ${config.connectionId} 컬럼 로드 실패:`, apiError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ [FlowConditionBuilder] 다중 REST API 컬럼 로드 완료:", allColumns.length, "items");
|
||||||
|
setColumns(allColumns);
|
||||||
|
} catch (multiApiError) {
|
||||||
|
console.error("❌ 다중 REST API 컬럼 로드 실패:", multiApiError);
|
||||||
|
setColumns([]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 단일 REST API인 경우 (dbSourceType이 restapi이거나 tableName이 _restapi_로 시작)
|
||||||
|
const isRestApi = dbSourceType === "restapi" || tableName?.startsWith("_restapi_");
|
||||||
|
|
||||||
|
// tableName에서 REST API 연결 ID 추출 (restApiConnectionId가 없는 경우)
|
||||||
|
let effectiveRestApiConnectionId = restApiConnectionId;
|
||||||
|
if (isRestApi && !effectiveRestApiConnectionId && tableName) {
|
||||||
|
const match = tableName.match(/_restapi_(\d+)/);
|
||||||
|
if (match) {
|
||||||
|
effectiveRestApiConnectionId = parseInt(match[1]);
|
||||||
|
console.log("🔍 tableName에서 REST API 연결 ID 추출:", effectiveRestApiConnectionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRestApi && effectiveRestApiConnectionId) {
|
||||||
|
try {
|
||||||
|
// jsonPath가 "data"이거나 없으면 "response"로 변경 (thiratis API 응답 구조에 맞춤)
|
||||||
|
const effectiveJsonPath = (!restApiJsonPath || restApiJsonPath === "data") ? "response" : restApiJsonPath;
|
||||||
|
|
||||||
|
console.log("🌐 [FlowConditionBuilder] REST API 컬럼 로드 시작:", {
|
||||||
|
connectionId: effectiveRestApiConnectionId,
|
||||||
|
endpoint: restApiEndpoint,
|
||||||
|
jsonPath: restApiJsonPath,
|
||||||
|
effectiveJsonPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const restApiData = await ExternalRestApiConnectionAPI.fetchData(
|
||||||
|
effectiveRestApiConnectionId,
|
||||||
|
restApiEndpoint,
|
||||||
|
effectiveJsonPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("✅ [FlowConditionBuilder] REST API columns response:", restApiData);
|
||||||
|
|
||||||
|
if (restApiData.columns && restApiData.columns.length > 0) {
|
||||||
|
const columnList = restApiData.columns.map((col) => ({
|
||||||
|
column_name: col.columnName,
|
||||||
|
data_type: col.dataType || "varchar",
|
||||||
|
displayName: col.columnLabel || col.columnName,
|
||||||
|
}));
|
||||||
|
console.log("✅ Setting REST API columns:", columnList.length, "items", columnList);
|
||||||
|
setColumns(columnList);
|
||||||
|
} else {
|
||||||
|
console.warn("❌ No columns in REST API response");
|
||||||
|
setColumns([]);
|
||||||
|
}
|
||||||
|
} catch (restApiError) {
|
||||||
|
console.error("❌ REST API 컬럼 로드 실패:", restApiError);
|
||||||
|
setColumns([]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 외부 DB인 경우
|
// 외부 DB인 경우
|
||||||
if (dbSourceType === "external" && dbConnectionId) {
|
if (dbSourceType === "external" && dbConnectionId) {
|
||||||
const token = localStorage.getItem("authToken");
|
const token = localStorage.getItem("authToken");
|
||||||
|
|
@ -148,7 +265,7 @@ export function FlowConditionBuilder({
|
||||||
};
|
};
|
||||||
|
|
||||||
loadColumns();
|
loadColumns();
|
||||||
}, [tableName, dbSourceType, dbConnectionId]);
|
}, [tableName, dbSourceType, dbConnectionId, restApiConnectionId, restApiEndpoint, restApiJsonPath]);
|
||||||
|
|
||||||
// 조건 변경 시 부모에 전달
|
// 조건 변경 시 부모에 전달
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,25 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection";
|
import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection";
|
||||||
|
|
||||||
|
// 다중 REST API 연결 설정
|
||||||
|
interface RestApiConnectionConfig {
|
||||||
|
connectionId: number;
|
||||||
|
connectionName: string;
|
||||||
|
endpoint: string;
|
||||||
|
jsonPath: string;
|
||||||
|
alias: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface FlowStepPanelProps {
|
interface FlowStepPanelProps {
|
||||||
step: FlowStep;
|
step: FlowStep;
|
||||||
flowId: number;
|
flowId: number;
|
||||||
flowTableName?: string; // 플로우 정의에서 선택한 테이블명
|
flowTableName?: string; // 플로우 정의에서 선택한 테이블명
|
||||||
flowDbSourceType?: "internal" | "external"; // 플로우의 DB 소스 타입
|
flowDbSourceType?: "internal" | "external" | "restapi" | "multi_restapi"; // 플로우의 DB 소스 타입
|
||||||
flowDbConnectionId?: number; // 플로우의 외부 DB 연결 ID
|
flowDbConnectionId?: number; // 플로우의 외부 DB 연결 ID
|
||||||
|
flowRestApiConnectionId?: number; // 플로우의 REST API 연결 ID (단일)
|
||||||
|
flowRestApiEndpoint?: string; // REST API 엔드포인트 (단일)
|
||||||
|
flowRestApiJsonPath?: string; // REST API JSON 경로 (단일)
|
||||||
|
flowRestApiConnections?: RestApiConnectionConfig[]; // 다중 REST API 설정
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onUpdate: () => void;
|
onUpdate: () => void;
|
||||||
}
|
}
|
||||||
|
|
@ -46,6 +59,10 @@ export function FlowStepPanel({
|
||||||
flowTableName,
|
flowTableName,
|
||||||
flowDbSourceType = "internal",
|
flowDbSourceType = "internal",
|
||||||
flowDbConnectionId,
|
flowDbConnectionId,
|
||||||
|
flowRestApiConnectionId,
|
||||||
|
flowRestApiEndpoint,
|
||||||
|
flowRestApiJsonPath,
|
||||||
|
flowRestApiConnections,
|
||||||
onClose,
|
onClose,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
}: FlowStepPanelProps) {
|
}: FlowStepPanelProps) {
|
||||||
|
|
@ -56,6 +73,9 @@ export function FlowStepPanel({
|
||||||
flowTableName,
|
flowTableName,
|
||||||
flowDbSourceType,
|
flowDbSourceType,
|
||||||
flowDbConnectionId,
|
flowDbConnectionId,
|
||||||
|
flowRestApiConnectionId,
|
||||||
|
flowRestApiEndpoint,
|
||||||
|
flowRestApiJsonPath,
|
||||||
final: step.tableName || flowTableName || "",
|
final: step.tableName || flowTableName || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -315,10 +335,11 @@ export function FlowStepPanel({
|
||||||
setFormData(newFormData);
|
setFormData(newFormData);
|
||||||
}, [step.id, flowTableName]); // flowTableName도 의존성 추가
|
}, [step.id, flowTableName]); // flowTableName도 의존성 추가
|
||||||
|
|
||||||
// 테이블 선택 시 컬럼 로드 - 내부/외부 DB 모두 지원
|
// 테이블 선택 시 컬럼 로드 - 내부/외부 DB 및 REST API 모두 지원
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadColumns = async () => {
|
const loadColumns = async () => {
|
||||||
if (!formData.tableName) {
|
// 다중 REST API인 경우 tableName 없이도 컬럼 로드 가능
|
||||||
|
if (!formData.tableName && flowDbSourceType !== "multi_restapi") {
|
||||||
setColumns([]);
|
setColumns([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -329,8 +350,74 @@ export function FlowStepPanel({
|
||||||
tableName: formData.tableName,
|
tableName: formData.tableName,
|
||||||
flowDbSourceType,
|
flowDbSourceType,
|
||||||
flowDbConnectionId,
|
flowDbConnectionId,
|
||||||
|
flowRestApiConnectionId,
|
||||||
|
flowRestApiConnections,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 다중 REST API인 경우
|
||||||
|
if (flowDbSourceType === "multi_restapi" && flowRestApiConnections && flowRestApiConnections.length > 0) {
|
||||||
|
console.log("🌐 다중 REST API 컬럼 로드 시작");
|
||||||
|
const { ExternalRestApiConnectionAPI } = await import("@/lib/api/externalRestApiConnection");
|
||||||
|
|
||||||
|
const allColumns: any[] = [];
|
||||||
|
|
||||||
|
for (const config of flowRestApiConnections) {
|
||||||
|
try {
|
||||||
|
const effectiveJsonPath = (!config.jsonPath || config.jsonPath === "data") ? "response" : config.jsonPath;
|
||||||
|
|
||||||
|
const restApiData = await ExternalRestApiConnectionAPI.fetchData(
|
||||||
|
config.connectionId,
|
||||||
|
config.endpoint,
|
||||||
|
effectiveJsonPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (restApiData.columns && restApiData.columns.length > 0) {
|
||||||
|
const prefixedColumns = restApiData.columns.map((col) => ({
|
||||||
|
column_name: config.alias ? `${config.alias}${col.columnName}` : col.columnName,
|
||||||
|
data_type: col.dataType || "varchar",
|
||||||
|
displayName: `${col.columnLabel || col.columnName} (${config.connectionName})`,
|
||||||
|
}));
|
||||||
|
allColumns.push(...prefixedColumns);
|
||||||
|
}
|
||||||
|
} catch (apiError) {
|
||||||
|
console.warn(`API ${config.connectionId} 컬럼 로드 실패:`, apiError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ 다중 REST API 컬럼 로드 완료:", allColumns.length, "items");
|
||||||
|
setColumns(allColumns);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 단일 REST API인 경우
|
||||||
|
const isRestApi = flowDbSourceType === "restapi" || formData.tableName?.startsWith("_restapi_");
|
||||||
|
|
||||||
|
if (isRestApi && flowRestApiConnectionId) {
|
||||||
|
console.log("🌐 단일 REST API 컬럼 로드 시작");
|
||||||
|
const { ExternalRestApiConnectionAPI } = await import("@/lib/api/externalRestApiConnection");
|
||||||
|
|
||||||
|
const effectiveJsonPath = (!flowRestApiJsonPath || flowRestApiJsonPath === "data") ? "response" : flowRestApiJsonPath;
|
||||||
|
|
||||||
|
const restApiData = await ExternalRestApiConnectionAPI.fetchData(
|
||||||
|
flowRestApiConnectionId,
|
||||||
|
flowRestApiEndpoint,
|
||||||
|
effectiveJsonPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (restApiData.columns && restApiData.columns.length > 0) {
|
||||||
|
const columnList = restApiData.columns.map((col) => ({
|
||||||
|
column_name: col.columnName,
|
||||||
|
data_type: col.dataType || "varchar",
|
||||||
|
displayName: col.columnLabel || col.columnName,
|
||||||
|
}));
|
||||||
|
console.log("✅ REST API 컬럼 로드 완료:", columnList.length, "items");
|
||||||
|
setColumns(columnList);
|
||||||
|
} else {
|
||||||
|
setColumns([]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 외부 DB인 경우
|
// 외부 DB인 경우
|
||||||
if (flowDbSourceType === "external" && flowDbConnectionId) {
|
if (flowDbSourceType === "external" && flowDbConnectionId) {
|
||||||
const token = localStorage.getItem("authToken");
|
const token = localStorage.getItem("authToken");
|
||||||
|
|
@ -399,7 +486,7 @@ export function FlowStepPanel({
|
||||||
};
|
};
|
||||||
|
|
||||||
loadColumns();
|
loadColumns();
|
||||||
}, [formData.tableName, flowDbSourceType, flowDbConnectionId]);
|
}, [formData.tableName, flowDbSourceType, flowDbConnectionId, flowRestApiConnectionId, flowRestApiEndpoint, flowRestApiJsonPath, flowRestApiConnections]);
|
||||||
|
|
||||||
// formData의 최신 값을 항상 참조하기 위한 ref
|
// formData의 최신 값을 항상 참조하기 위한 ref
|
||||||
const formDataRef = useRef(formData);
|
const formDataRef = useRef(formData);
|
||||||
|
|
@ -661,6 +748,10 @@ export function FlowStepPanel({
|
||||||
tableName={formData.tableName}
|
tableName={formData.tableName}
|
||||||
dbSourceType={flowDbSourceType}
|
dbSourceType={flowDbSourceType}
|
||||||
dbConnectionId={flowDbConnectionId}
|
dbConnectionId={flowDbConnectionId}
|
||||||
|
restApiConnectionId={flowRestApiConnectionId}
|
||||||
|
restApiEndpoint={flowRestApiEndpoint}
|
||||||
|
restApiJsonPath={flowRestApiJsonPath}
|
||||||
|
restApiConnections={flowRestApiConnections}
|
||||||
condition={formData.conditionJson}
|
condition={formData.conditionJson}
|
||||||
onChange={(condition) => setFormData({ ...formData, conditionJson: condition })}
|
onChange={(condition) => setFormData({ ...formData, conditionJson: condition })}
|
||||||
/>
|
/>
|
||||||
|
|
@ -852,7 +943,7 @@ export function FlowStepPanel({
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
value={opt.value}
|
value={opt.value}
|
||||||
disabled={opt.value !== "internal" && opt.value !== "external_db"}
|
disabled={opt.value !== "internal" && opt.value !== "external_db" && opt.value !== "rest_api"}
|
||||||
>
|
>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
@ -1044,6 +1135,132 @@ export function FlowStepPanel({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* REST API 연동 설정 */}
|
||||||
|
{formData.integrationType === "rest_api" && (
|
||||||
|
<div className="space-y-4 rounded-lg border p-4">
|
||||||
|
<div>
|
||||||
|
<Label>REST API 연결</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.integrationConfig?.connectionId?.toString() || ""}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const connectionId = parseInt(value);
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
integrationConfig: {
|
||||||
|
type: "rest_api",
|
||||||
|
connectionId,
|
||||||
|
operation: "update",
|
||||||
|
endpoint: "",
|
||||||
|
method: "POST",
|
||||||
|
bodyTemplate: "{}",
|
||||||
|
} as any,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="REST API 연결 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{flowRestApiConnections && flowRestApiConnections.length > 0 ? (
|
||||||
|
flowRestApiConnections.map((api) => (
|
||||||
|
<SelectItem key={api.connectionId} value={api.connectionId.toString()}>
|
||||||
|
{api.connectionName}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : flowRestApiConnectionId ? (
|
||||||
|
<SelectItem value={flowRestApiConnectionId.toString()}>
|
||||||
|
기본 REST API 연결
|
||||||
|
</SelectItem>
|
||||||
|
) : (
|
||||||
|
<SelectItem value="" disabled>
|
||||||
|
연결된 REST API가 없습니다
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formData.integrationConfig?.connectionId && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Label>HTTP 메서드</Label>
|
||||||
|
<Select
|
||||||
|
value={(formData.integrationConfig as any).method || "POST"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
integrationConfig: {
|
||||||
|
...formData.integrationConfig!,
|
||||||
|
method: value,
|
||||||
|
} as any,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="GET">GET</SelectItem>
|
||||||
|
<SelectItem value="POST">POST</SelectItem>
|
||||||
|
<SelectItem value="PUT">PUT</SelectItem>
|
||||||
|
<SelectItem value="PATCH">PATCH</SelectItem>
|
||||||
|
<SelectItem value="DELETE">DELETE</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>엔드포인트</Label>
|
||||||
|
<Input
|
||||||
|
value={(formData.integrationConfig as any).endpoint || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
integrationConfig: {
|
||||||
|
...formData.integrationConfig!,
|
||||||
|
endpoint: e.target.value,
|
||||||
|
} as any,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="/api/update"
|
||||||
|
/>
|
||||||
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
|
데이터 이동 시 호출할 API 엔드포인트
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>요청 바디 (JSON)</Label>
|
||||||
|
<Textarea
|
||||||
|
value={(formData.integrationConfig as any).bodyTemplate || "{}"}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
integrationConfig: {
|
||||||
|
...formData.integrationConfig!,
|
||||||
|
bodyTemplate: e.target.value,
|
||||||
|
} as any,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder='{"id": "{{dataId}}", "status": "approved"}'
|
||||||
|
rows={4}
|
||||||
|
className="font-mono text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-md bg-blue-50 p-3">
|
||||||
|
<p className="text-sm text-blue-900">
|
||||||
|
💡 템플릿 변수를 사용하여 동적 값을 삽입할 수 있습니다:
|
||||||
|
<br />• {`{{dataId}}`} - 이동하는 데이터의 ID
|
||||||
|
<br />• {`{{currentUser}}`} - 현재 사용자
|
||||||
|
<br />• {`{{currentTimestamp}}`} - 현재 시간
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -846,13 +846,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
restApiConnectionId: selectedScreen?.restApiConnectionId,
|
restApiConnectionId: selectedScreen?.restApiConnectionId,
|
||||||
restApiEndpoint: selectedScreen?.restApiEndpoint,
|
restApiEndpoint: selectedScreen?.restApiEndpoint,
|
||||||
restApiJsonPath: selectedScreen?.restApiJsonPath,
|
restApiJsonPath: selectedScreen?.restApiJsonPath,
|
||||||
|
// 전체 selectedScreen 객체도 출력
|
||||||
|
fullScreen: selectedScreen,
|
||||||
});
|
});
|
||||||
|
|
||||||
// REST API 데이터 소스인 경우
|
// REST API 데이터 소스인 경우
|
||||||
// tableName이 restapi_로 시작하면 REST API로 간주
|
// 1. dataSourceType이 "restapi"인 경우
|
||||||
|
// 2. tableName이 restapi_ 또는 _restapi_로 시작하는 경우
|
||||||
|
// 3. restApiConnectionId가 있는 경우
|
||||||
const isRestApi = selectedScreen?.dataSourceType === "restapi" ||
|
const isRestApi = selectedScreen?.dataSourceType === "restapi" ||
|
||||||
selectedScreen?.tableName?.startsWith("restapi_") ||
|
selectedScreen?.tableName?.startsWith("restapi_") ||
|
||||||
selectedScreen?.tableName?.startsWith("_restapi_");
|
selectedScreen?.tableName?.startsWith("_restapi_") ||
|
||||||
|
!!selectedScreen?.restApiConnectionId;
|
||||||
|
|
||||||
|
console.log("🔍 [ScreenDesigner] REST API 여부:", { isRestApi });
|
||||||
|
|
||||||
if (isRestApi && (selectedScreen?.restApiConnectionId || selectedScreen?.tableName)) {
|
if (isRestApi && (selectedScreen?.restApiConnectionId || selectedScreen?.tableName)) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,28 @@ export interface FlowConditionGroup {
|
||||||
conditions: FlowCondition[];
|
conditions: FlowCondition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 다중 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_")
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 플로우 정의
|
// 플로우 정의
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -41,6 +63,17 @@ export interface FlowDefinition {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
tableName: string;
|
tableName: string;
|
||||||
|
// 데이터 소스 관련
|
||||||
|
dbSourceType?: "internal" | "external" | "restapi" | "multi_restapi" | "multi_external_db";
|
||||||
|
dbConnectionId?: number;
|
||||||
|
// REST API 관련 (단일)
|
||||||
|
restApiConnectionId?: number;
|
||||||
|
restApiEndpoint?: string;
|
||||||
|
restApiJsonPath?: string;
|
||||||
|
// 다중 REST API 관련
|
||||||
|
restApiConnections?: RestApiConnectionConfig[];
|
||||||
|
// 다중 외부 DB 관련
|
||||||
|
externalDbConnections?: ExternalDbConnectionConfig[];
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|
@ -53,12 +86,16 @@ export interface CreateFlowDefinitionRequest {
|
||||||
description?: string;
|
description?: string;
|
||||||
tableName: string;
|
tableName: string;
|
||||||
// 데이터 소스 관련
|
// 데이터 소스 관련
|
||||||
dbSourceType?: "internal" | "external" | "restapi";
|
dbSourceType?: "internal" | "external" | "restapi" | "multi_restapi" | "multi_external_db";
|
||||||
dbConnectionId?: number;
|
dbConnectionId?: number;
|
||||||
// REST API 관련
|
// REST API 관련 (단일)
|
||||||
restApiConnectionId?: number;
|
restApiConnectionId?: number;
|
||||||
restApiEndpoint?: string;
|
restApiEndpoint?: string;
|
||||||
restApiJsonPath?: string;
|
restApiJsonPath?: string;
|
||||||
|
// 다중 REST API 관련
|
||||||
|
restApiConnections?: RestApiConnectionConfig[];
|
||||||
|
// 다중 외부 DB 관련
|
||||||
|
externalDbConnections?: ExternalDbConnectionConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateFlowDefinitionRequest {
|
export interface UpdateFlowDefinitionRequest {
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ export const OPERATION_OPTIONS = [
|
||||||
export const INTEGRATION_TYPE_OPTIONS = [
|
export const INTEGRATION_TYPE_OPTIONS = [
|
||||||
{ value: "internal", label: "내부 DB (기본)" },
|
{ value: "internal", label: "내부 DB (기본)" },
|
||||||
{ value: "external_db", label: "외부 DB 연동" },
|
{ value: "external_db", label: "외부 DB 연동" },
|
||||||
{ value: "rest_api", label: "REST API (추후 지원)" },
|
{ value: "rest_api", label: "REST API 연동" },
|
||||||
{ value: "webhook", label: "Webhook (추후 지원)" },
|
{ value: "webhook", label: "Webhook (추후 지원)" },
|
||||||
{ value: "hybrid", label: "복합 연동 (추후 지원)" },
|
{ value: "hybrid", label: "복합 연동 (추후 지원)" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue