워크플로우 restapi도 연결가능하고여러개 가능하게 구현시켜놓음
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 : "알 수 없는 오류",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 사용)
|
||||
}
|
||||
|
||||
|
||||
@@ -319,6 +319,10 @@ export default function FlowEditorPage() {
|
||||
flowTableName={flowDefinition?.tableName} // 플로우 정의의 테이블명 전달
|
||||
flowDbSourceType={flowDefinition?.dbSourceType} // DB 소스 타입 전달
|
||||
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)}
|
||||
onUpdate={loadFlowData}
|
||||
/>
|
||||
|
||||
@@ -64,7 +64,30 @@ export default function FlowManagementPage() {
|
||||
// REST API 연결 관련 상태
|
||||
const [restApiConnections, setRestApiConnections] = useState<ExternalRestApiConnection[]>([]);
|
||||
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({
|
||||
@@ -207,25 +230,161 @@ export default function FlowManagementPage() {
|
||||
}
|
||||
}, [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 () => {
|
||||
console.log("🚀 handleCreate called with formData:", formData);
|
||||
|
||||
// REST API인 경우 테이블 이름 검증 스킵
|
||||
const isRestApi = selectedDbSource.startsWith("restapi_");
|
||||
// REST API 또는 다중 선택인 경우 테이블 이름 검증 스킵
|
||||
const isRestApi = selectedDbSource.startsWith("restapi_") || isMultiRestApi;
|
||||
const isMultiMode = isMultiRestApi || isMultiExternalDb;
|
||||
|
||||
if (!formData.name || (!isRestApi && !formData.tableName)) {
|
||||
console.log("❌ Validation failed:", { name: formData.name, tableName: formData.tableName, isRestApi });
|
||||
if (!formData.name || (!isRestApi && !isMultiMode && !formData.tableName)) {
|
||||
console.log("❌ Validation failed:", { name: formData.name, tableName: formData.tableName, isRestApi, isMultiMode });
|
||||
toast({
|
||||
title: "입력 오류",
|
||||
description: isRestApi ? "플로우 이름은 필수입니다." : "플로우 이름과 테이블 이름은 필수입니다.",
|
||||
description: (isRestApi || isMultiMode) ? "플로우 이름은 필수입니다." : "플로우 이름과 테이블 이름은 필수입니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// REST API인 경우 엔드포인트 검증
|
||||
if (isRestApi && !restApiEndpoint) {
|
||||
// 다중 REST API 모드인 경우 검증
|
||||
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({
|
||||
title: "입력 오류",
|
||||
description: "REST API 엔드포인트는 필수입니다.",
|
||||
@@ -236,11 +395,15 @@ export default function FlowManagementPage() {
|
||||
|
||||
try {
|
||||
// 데이터 소스 타입 및 ID 파싱
|
||||
let dbSourceType: "internal" | "external" | "restapi" = "internal";
|
||||
let dbSourceType: "internal" | "external" | "restapi" | "multi_restapi" | "multi_external_db" = "internal";
|
||||
let dbConnectionId: 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";
|
||||
} else if (selectedDbSource.startsWith("external_db_")) {
|
||||
dbSourceType = "external";
|
||||
@@ -257,11 +420,27 @@ export default function FlowManagementPage() {
|
||||
dbConnectionId,
|
||||
};
|
||||
|
||||
// REST API인 경우 추가 정보
|
||||
if (dbSourceType === "restapi") {
|
||||
// 다중 REST API인 경우
|
||||
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.restApiEndpoint = restApiEndpoint;
|
||||
requestData.restApiJsonPath = restApiJsonPath || "data";
|
||||
requestData.restApiJsonPath = restApiJsonPath || "response";
|
||||
// REST API는 가상 테이블명 사용
|
||||
requestData.tableName = `_restapi_${restApiConnectionId}`;
|
||||
}
|
||||
@@ -277,7 +456,11 @@ export default function FlowManagementPage() {
|
||||
setFormData({ name: "", description: "", tableName: "" });
|
||||
setSelectedDbSource("internal");
|
||||
setRestApiEndpoint("");
|
||||
setRestApiJsonPath("data");
|
||||
setRestApiJsonPath("response");
|
||||
setSelectedRestApis([]);
|
||||
setSelectedExternalDbs([]);
|
||||
setIsMultiRestApi(false);
|
||||
setIsMultiExternalDb(false);
|
||||
loadFlows();
|
||||
} else {
|
||||
toast({
|
||||
@@ -485,13 +668,27 @@ export default function FlowManagementPage() {
|
||||
<div>
|
||||
<Label className="text-xs sm:text-sm">데이터 소스</Label>
|
||||
<Select
|
||||
value={selectedDbSource}
|
||||
value={isMultiRestApi ? "multi_restapi" : isMultiExternalDb ? "multi_external_db" : selectedDbSource}
|
||||
onValueChange={(value) => {
|
||||
setSelectedDbSource(value);
|
||||
// 소스 변경 시 테이블 선택 및 REST API 설정 초기화
|
||||
if (value === "multi_restapi") {
|
||||
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: "" });
|
||||
setRestApiEndpoint("");
|
||||
setRestApiJsonPath("data");
|
||||
setRestApiJsonPath("response");
|
||||
setSelectedRestApis([]);
|
||||
setSelectedExternalDbs([]);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||
@@ -504,7 +701,7 @@ export default function FlowManagementPage() {
|
||||
{/* 외부 DB 연결 */}
|
||||
{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>
|
||||
{externalConnections.map((conn) => (
|
||||
@@ -518,7 +715,7 @@ export default function FlowManagementPage() {
|
||||
{/* REST API 연결 */}
|
||||
{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 --
|
||||
</SelectItem>
|
||||
{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>
|
||||
</Select>
|
||||
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||
@@ -535,8 +751,160 @@ export default function FlowManagementPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* REST API인 경우 엔드포인트 설정 */}
|
||||
{selectedDbSource.startsWith("restapi_") ? (
|
||||
{/* 다중 REST API 선택 UI */}
|
||||
{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>
|
||||
<Label htmlFor="restApiEndpoint" className="text-xs sm:text-sm">
|
||||
@@ -569,8 +937,10 @@ export default function FlowManagementPage() {
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* 테이블 선택 (내부 DB 또는 외부 DB) */
|
||||
)}
|
||||
|
||||
{/* 테이블 선택 (내부 DB 또는 단일 외부 DB - 다중 선택 모드가 아닌 경우만) */}
|
||||
{!isMultiRestApi && !isMultiExternalDb && !selectedDbSource.startsWith("restapi_") && (
|
||||
<div>
|
||||
<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 { FlowConditionGroup, FlowCondition, ConditionOperator } from "@/types/flow";
|
||||
import { getTableColumns } from "@/lib/api/tableManagement";
|
||||
import { ExternalRestApiConnectionAPI } from "@/lib/api/externalRestApiConnection";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// 다중 REST API 연결 설정
|
||||
interface RestApiConnectionConfig {
|
||||
connectionId: number;
|
||||
connectionName: string;
|
||||
endpoint: string;
|
||||
jsonPath: string;
|
||||
alias: string;
|
||||
}
|
||||
|
||||
interface FlowConditionBuilderProps {
|
||||
flowId: number;
|
||||
tableName?: string; // 조회할 테이블명
|
||||
dbSourceType?: "internal" | "external"; // DB 소스 타입
|
||||
dbSourceType?: "internal" | "external" | "restapi" | "multi_restapi"; // DB 소스 타입
|
||||
dbConnectionId?: number; // 외부 DB 연결 ID
|
||||
restApiConnectionId?: number; // REST API 연결 ID (단일)
|
||||
restApiEndpoint?: string; // REST API 엔드포인트 (단일)
|
||||
restApiJsonPath?: string; // REST API JSON 경로 (단일)
|
||||
restApiConnections?: RestApiConnectionConfig[]; // 다중 REST API 설정
|
||||
condition?: FlowConditionGroup;
|
||||
onChange: (condition: FlowConditionGroup | undefined) => void;
|
||||
}
|
||||
@@ -45,6 +59,10 @@ export function FlowConditionBuilder({
|
||||
tableName,
|
||||
dbSourceType = "internal",
|
||||
dbConnectionId,
|
||||
restApiConnectionId,
|
||||
restApiEndpoint,
|
||||
restApiJsonPath,
|
||||
restApiConnections,
|
||||
condition,
|
||||
onChange,
|
||||
}: FlowConditionBuilderProps) {
|
||||
@@ -65,9 +83,10 @@ export function FlowConditionBuilder({
|
||||
}
|
||||
}, [condition]);
|
||||
|
||||
// 테이블 컬럼 로드 - 내부/외부 DB 모두 지원
|
||||
// 테이블 컬럼 로드 - 내부/외부 DB 및 REST API 모두 지원
|
||||
useEffect(() => {
|
||||
if (!tableName) {
|
||||
// REST API인 경우 tableName이 없어도 진행 가능
|
||||
if (!tableName && dbSourceType !== "restapi" && dbSourceType !== "multi_restapi") {
|
||||
setColumns([]);
|
||||
return;
|
||||
}
|
||||
@@ -79,8 +98,106 @@ export function FlowConditionBuilder({
|
||||
tableName,
|
||||
dbSourceType,
|
||||
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인 경우
|
||||
if (dbSourceType === "external" && dbConnectionId) {
|
||||
const token = localStorage.getItem("authToken");
|
||||
@@ -148,7 +265,7 @@ export function FlowConditionBuilder({
|
||||
};
|
||||
|
||||
loadColumns();
|
||||
}, [tableName, dbSourceType, dbConnectionId]);
|
||||
}, [tableName, dbSourceType, dbConnectionId, restApiConnectionId, restApiEndpoint, restApiJsonPath]);
|
||||
|
||||
// 조건 변경 시 부모에 전달
|
||||
useEffect(() => {
|
||||
|
||||
@@ -30,12 +30,25 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection";
|
||||
|
||||
// 다중 REST API 연결 설정
|
||||
interface RestApiConnectionConfig {
|
||||
connectionId: number;
|
||||
connectionName: string;
|
||||
endpoint: string;
|
||||
jsonPath: string;
|
||||
alias: string;
|
||||
}
|
||||
|
||||
interface FlowStepPanelProps {
|
||||
step: FlowStep;
|
||||
flowId: number;
|
||||
flowTableName?: string; // 플로우 정의에서 선택한 테이블명
|
||||
flowDbSourceType?: "internal" | "external"; // 플로우의 DB 소스 타입
|
||||
flowDbSourceType?: "internal" | "external" | "restapi" | "multi_restapi"; // 플로우의 DB 소스 타입
|
||||
flowDbConnectionId?: number; // 플로우의 외부 DB 연결 ID
|
||||
flowRestApiConnectionId?: number; // 플로우의 REST API 연결 ID (단일)
|
||||
flowRestApiEndpoint?: string; // REST API 엔드포인트 (단일)
|
||||
flowRestApiJsonPath?: string; // REST API JSON 경로 (단일)
|
||||
flowRestApiConnections?: RestApiConnectionConfig[]; // 다중 REST API 설정
|
||||
onClose: () => void;
|
||||
onUpdate: () => void;
|
||||
}
|
||||
@@ -46,6 +59,10 @@ export function FlowStepPanel({
|
||||
flowTableName,
|
||||
flowDbSourceType = "internal",
|
||||
flowDbConnectionId,
|
||||
flowRestApiConnectionId,
|
||||
flowRestApiEndpoint,
|
||||
flowRestApiJsonPath,
|
||||
flowRestApiConnections,
|
||||
onClose,
|
||||
onUpdate,
|
||||
}: FlowStepPanelProps) {
|
||||
@@ -56,6 +73,9 @@ export function FlowStepPanel({
|
||||
flowTableName,
|
||||
flowDbSourceType,
|
||||
flowDbConnectionId,
|
||||
flowRestApiConnectionId,
|
||||
flowRestApiEndpoint,
|
||||
flowRestApiJsonPath,
|
||||
final: step.tableName || flowTableName || "",
|
||||
});
|
||||
|
||||
@@ -315,10 +335,11 @@ export function FlowStepPanel({
|
||||
setFormData(newFormData);
|
||||
}, [step.id, flowTableName]); // flowTableName도 의존성 추가
|
||||
|
||||
// 테이블 선택 시 컬럼 로드 - 내부/외부 DB 모두 지원
|
||||
// 테이블 선택 시 컬럼 로드 - 내부/외부 DB 및 REST API 모두 지원
|
||||
useEffect(() => {
|
||||
const loadColumns = async () => {
|
||||
if (!formData.tableName) {
|
||||
// 다중 REST API인 경우 tableName 없이도 컬럼 로드 가능
|
||||
if (!formData.tableName && flowDbSourceType !== "multi_restapi") {
|
||||
setColumns([]);
|
||||
return;
|
||||
}
|
||||
@@ -329,8 +350,74 @@ export function FlowStepPanel({
|
||||
tableName: formData.tableName,
|
||||
flowDbSourceType,
|
||||
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인 경우
|
||||
if (flowDbSourceType === "external" && flowDbConnectionId) {
|
||||
const token = localStorage.getItem("authToken");
|
||||
@@ -399,7 +486,7 @@ export function FlowStepPanel({
|
||||
};
|
||||
|
||||
loadColumns();
|
||||
}, [formData.tableName, flowDbSourceType, flowDbConnectionId]);
|
||||
}, [formData.tableName, flowDbSourceType, flowDbConnectionId, flowRestApiConnectionId, flowRestApiEndpoint, flowRestApiJsonPath, flowRestApiConnections]);
|
||||
|
||||
// formData의 최신 값을 항상 참조하기 위한 ref
|
||||
const formDataRef = useRef(formData);
|
||||
@@ -661,6 +748,10 @@ export function FlowStepPanel({
|
||||
tableName={formData.tableName}
|
||||
dbSourceType={flowDbSourceType}
|
||||
dbConnectionId={flowDbConnectionId}
|
||||
restApiConnectionId={flowRestApiConnectionId}
|
||||
restApiEndpoint={flowRestApiEndpoint}
|
||||
restApiJsonPath={flowRestApiJsonPath}
|
||||
restApiConnections={flowRestApiConnections}
|
||||
condition={formData.conditionJson}
|
||||
onChange={(condition) => setFormData({ ...formData, conditionJson: condition })}
|
||||
/>
|
||||
@@ -852,7 +943,7 @@ export function FlowStepPanel({
|
||||
<SelectItem
|
||||
key={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}
|
||||
</SelectItem>
|
||||
@@ -1044,6 +1135,132 @@ export function FlowStepPanel({
|
||||
)}
|
||||
</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>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -846,13 +846,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
restApiConnectionId: selectedScreen?.restApiConnectionId,
|
||||
restApiEndpoint: selectedScreen?.restApiEndpoint,
|
||||
restApiJsonPath: selectedScreen?.restApiJsonPath,
|
||||
// 전체 selectedScreen 객체도 출력
|
||||
fullScreen: selectedScreen,
|
||||
});
|
||||
|
||||
// REST API 데이터 소스인 경우
|
||||
// tableName이 restapi_로 시작하면 REST API로 간주
|
||||
// 1. dataSourceType이 "restapi"인 경우
|
||||
// 2. tableName이 restapi_ 또는 _restapi_로 시작하는 경우
|
||||
// 3. restApiConnectionId가 있는 경우
|
||||
const isRestApi = selectedScreen?.dataSourceType === "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)) {
|
||||
try {
|
||||
|
||||
@@ -33,6 +33,28 @@ export interface FlowConditionGroup {
|
||||
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;
|
||||
description?: 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;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -53,12 +86,16 @@ export interface CreateFlowDefinitionRequest {
|
||||
description?: string;
|
||||
tableName: string;
|
||||
// 데이터 소스 관련
|
||||
dbSourceType?: "internal" | "external" | "restapi";
|
||||
dbSourceType?: "internal" | "external" | "restapi" | "multi_restapi" | "multi_external_db";
|
||||
dbConnectionId?: number;
|
||||
// REST API 관련
|
||||
// REST API 관련 (단일)
|
||||
restApiConnectionId?: number;
|
||||
restApiEndpoint?: string;
|
||||
restApiJsonPath?: string;
|
||||
// 다중 REST API 관련
|
||||
restApiConnections?: RestApiConnectionConfig[];
|
||||
// 다중 외부 DB 관련
|
||||
externalDbConnections?: ExternalDbConnectionConfig[];
|
||||
}
|
||||
|
||||
export interface UpdateFlowDefinitionRequest {
|
||||
|
||||
@@ -126,7 +126,7 @@ export const OPERATION_OPTIONS = [
|
||||
export const INTEGRATION_TYPE_OPTIONS = [
|
||||
{ value: "internal", label: "내부 DB (기본)" },
|
||||
{ value: "external_db", label: "외부 DB 연동" },
|
||||
{ value: "rest_api", label: "REST API (추후 지원)" },
|
||||
{ value: "rest_api", label: "REST API 연동" },
|
||||
{ value: "webhook", label: "Webhook (추후 지원)" },
|
||||
{ value: "hybrid", label: "복합 연동 (추후 지원)" },
|
||||
] as const;
|
||||
|
||||
Reference in New Issue
Block a user