제어관리 노드 작동 방식 수정
This commit is contained in:
@@ -26,7 +26,6 @@ export type NodeType =
|
||||
| "externalDBSource"
|
||||
| "restAPISource"
|
||||
| "condition"
|
||||
| "fieldMapping"
|
||||
| "dataTransform"
|
||||
| "insertAction"
|
||||
| "updateAction"
|
||||
@@ -429,14 +428,79 @@ export class NodeFlowExecutionService {
|
||||
return context.sourceData;
|
||||
} else if (parents.length === 1) {
|
||||
// 단일 부모: 부모의 결과 데이터 전달
|
||||
const parentResult = context.nodeResults.get(parents[0]);
|
||||
return parentResult?.data || context.sourceData;
|
||||
const parentId = parents[0];
|
||||
const parentResult = context.nodeResults.get(parentId);
|
||||
let data = parentResult?.data || context.sourceData;
|
||||
|
||||
// 🔥 조건 노드에서 온 데이터인 경우 sourceHandle 확인
|
||||
const edge = edges.find(
|
||||
(e) => e.source === parentId && e.target === nodeId
|
||||
);
|
||||
if (
|
||||
edge?.sourceHandle &&
|
||||
data &&
|
||||
typeof data === "object" &&
|
||||
"conditionResult" in data
|
||||
) {
|
||||
// 조건 노드의 결과 객체
|
||||
if (edge.sourceHandle === "true") {
|
||||
logger.info(
|
||||
`✅ TRUE 브랜치 데이터 사용: ${data.trueData?.length || 0}건`
|
||||
);
|
||||
return data.trueData || [];
|
||||
} else if (edge.sourceHandle === "false") {
|
||||
logger.info(
|
||||
`✅ FALSE 브랜치 데이터 사용: ${data.falseData?.length || 0}건`
|
||||
);
|
||||
return data.falseData || [];
|
||||
} else {
|
||||
// sourceHandle이 없거나 다른 값이면 allData 사용
|
||||
return data.allData || data;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
} else {
|
||||
// 다중 부모: 모든 부모의 데이터 병합
|
||||
return parents.map((parentId) => {
|
||||
const result = context.nodeResults.get(parentId);
|
||||
return result?.data || context.sourceData;
|
||||
const allData: any[] = [];
|
||||
|
||||
parents.forEach((parentId) => {
|
||||
const parentResult = context.nodeResults.get(parentId);
|
||||
let data = parentResult?.data || context.sourceData;
|
||||
|
||||
// 🔥 조건 노드에서 온 데이터인 경우 sourceHandle 확인
|
||||
const edge = edges.find(
|
||||
(e) => e.source === parentId && e.target === nodeId
|
||||
);
|
||||
if (
|
||||
edge?.sourceHandle &&
|
||||
data &&
|
||||
typeof data === "object" &&
|
||||
"conditionResult" in data
|
||||
) {
|
||||
// 조건 노드의 결과 객체
|
||||
if (edge.sourceHandle === "true") {
|
||||
data = data.trueData || [];
|
||||
} else if (edge.sourceHandle === "false") {
|
||||
data = data.falseData || [];
|
||||
} else {
|
||||
data = data.allData || data;
|
||||
}
|
||||
}
|
||||
|
||||
// 배열이면 펼쳐서 추가
|
||||
if (Array.isArray(data)) {
|
||||
allData.push(...data);
|
||||
} else {
|
||||
allData.push(data);
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`🔗 다중 부모 병합: ${parents.length}개 부모, 총 ${allData.length}건 데이터`
|
||||
);
|
||||
|
||||
return allData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,6 +517,9 @@ export class NodeFlowExecutionService {
|
||||
case "tableSource":
|
||||
return this.executeTableSource(node, context);
|
||||
|
||||
case "externalDBSource":
|
||||
return this.executeExternalDBSource(node, context);
|
||||
|
||||
case "restAPISource":
|
||||
return this.executeRestAPISource(node, context);
|
||||
|
||||
@@ -603,6 +670,60 @@ export class NodeFlowExecutionService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 DB 소스 노드 실행
|
||||
*/
|
||||
private static async executeExternalDBSource(
|
||||
node: FlowNode,
|
||||
context: ExecutionContext
|
||||
): Promise<any[]> {
|
||||
const { connectionId, tableName, schema, whereConditions } = node.data;
|
||||
|
||||
if (!connectionId || !tableName) {
|
||||
throw new Error("외부 DB 연결 정보 또는 테이블명이 설정되지 않았습니다.");
|
||||
}
|
||||
|
||||
logger.info(`🔌 외부 DB 소스 조회: ${connectionId}.${tableName}`);
|
||||
|
||||
try {
|
||||
// 연결 풀 서비스 임포트 (동적 임포트로 순환 참조 방지)
|
||||
const { ExternalDbConnectionPoolService } = await import(
|
||||
"./externalDbConnectionPoolService"
|
||||
);
|
||||
const poolService = ExternalDbConnectionPoolService.getInstance();
|
||||
|
||||
// 스키마 접두사 처리
|
||||
const schemaPrefix = schema ? `${schema}.` : "";
|
||||
const fullTableName = `${schemaPrefix}${tableName}`;
|
||||
|
||||
// WHERE 절 생성
|
||||
let sql = `SELECT * FROM ${fullTableName}`;
|
||||
let params: any[] = [];
|
||||
|
||||
if (whereConditions && whereConditions.length > 0) {
|
||||
const whereResult = this.buildWhereClause(whereConditions);
|
||||
sql += ` ${whereResult.clause}`;
|
||||
params = whereResult.values;
|
||||
}
|
||||
|
||||
logger.info(`📊 외부 DB 쿼리 실행: ${sql}`);
|
||||
|
||||
// 연결 풀을 통해 쿼리 실행
|
||||
const result = await poolService.executeQuery(connectionId, sql, params);
|
||||
|
||||
logger.info(
|
||||
`✅ 외부 DB 소스 조회 완료: ${tableName}, ${result.length}건`
|
||||
);
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
logger.error(`❌ 외부 DB 소스 조회 실패:`, error);
|
||||
throw new Error(
|
||||
`외부 DB 조회 실패 (연결 ID: ${connectionId}): ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 소스 노드 실행
|
||||
*/
|
||||
@@ -633,13 +754,13 @@ export class NodeFlowExecutionService {
|
||||
}
|
||||
|
||||
const schemaPrefix = schema ? `${schema}.` : "";
|
||||
const whereClause = whereConditions
|
||||
? `WHERE ${this.buildWhereClause(whereConditions)}`
|
||||
: "";
|
||||
const whereResult = whereConditions
|
||||
? this.buildWhereClause(whereConditions)
|
||||
: { clause: "", values: [] };
|
||||
|
||||
const sql = `SELECT * FROM ${schemaPrefix}${tableName} ${whereClause}`;
|
||||
const sql = `SELECT * FROM ${schemaPrefix}${tableName} ${whereResult.clause}`;
|
||||
|
||||
const result = await query(sql, []);
|
||||
const result = await query(sql, whereResult.values);
|
||||
|
||||
logger.info(`📊 테이블 소스 조회: ${tableName}, ${result.length}건`);
|
||||
|
||||
@@ -703,11 +824,15 @@ export class NodeFlowExecutionService {
|
||||
const executeInsert = async (txClient: any) => {
|
||||
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
||||
let insertedCount = 0;
|
||||
const insertedDataArray: any[] = [];
|
||||
|
||||
for (const data of dataArray) {
|
||||
const fields: string[] = [];
|
||||
const values: any[] = [];
|
||||
|
||||
// 🔥 삽입된 데이터 복사본 생성
|
||||
const insertedData = { ...data };
|
||||
|
||||
console.log("🗺️ 필드 매핑 처리 중...");
|
||||
fieldMappings.forEach((mapping: any) => {
|
||||
fields.push(mapping.targetField);
|
||||
@@ -720,25 +845,38 @@ export class NodeFlowExecutionService {
|
||||
` ${mapping.sourceField} → ${mapping.targetField}: ${value === undefined ? "❌ undefined" : "✅ " + value}`
|
||||
);
|
||||
values.push(value);
|
||||
|
||||
// 🔥 삽입된 값을 데이터에 반영
|
||||
insertedData[mapping.targetField] = value;
|
||||
});
|
||||
|
||||
const sql = `
|
||||
INSERT INTO ${targetTable} (${fields.join(", ")})
|
||||
VALUES (${fields.map((_, i) => `$${i + 1}`).join(", ")})
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
console.log("📝 실행할 SQL:", sql);
|
||||
console.log("📊 바인딩 값:", values);
|
||||
|
||||
await txClient.query(sql, values);
|
||||
const result = await txClient.query(sql, values);
|
||||
insertedCount++;
|
||||
|
||||
// 🔥 RETURNING으로 받은 실제 삽입 데이터 사용 (AUTO_INCREMENT 등 포함)
|
||||
if (result.rows && result.rows.length > 0) {
|
||||
insertedDataArray.push(result.rows[0]);
|
||||
} else {
|
||||
// RETURNING이 없으면 생성한 데이터 사용
|
||||
insertedDataArray.push(insertedData);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`✅ INSERT 완료 (내부 DB): ${targetTable}, ${insertedCount}건`
|
||||
);
|
||||
|
||||
return { insertedCount };
|
||||
// 🔥 삽입된 데이터 반환 (AUTO_INCREMENT ID 등 포함)
|
||||
return insertedDataArray;
|
||||
};
|
||||
|
||||
// 🔥 클라이언트가 전달되었으면 사용, 아니면 독립 트랜잭션 생성
|
||||
@@ -781,6 +919,7 @@ export class NodeFlowExecutionService {
|
||||
try {
|
||||
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
||||
let insertedCount = 0;
|
||||
const insertedDataArray: any[] = [];
|
||||
|
||||
// 🔥 Oracle의 경우 autoCommit을 false로 설정하여 트랜잭션 제어
|
||||
const isOracle = externalDbType.toLowerCase() === "oracle";
|
||||
@@ -788,6 +927,7 @@ export class NodeFlowExecutionService {
|
||||
for (const data of dataArray) {
|
||||
const fields: string[] = [];
|
||||
const values: any[] = [];
|
||||
const insertedData: any = { ...data };
|
||||
|
||||
fieldMappings.forEach((mapping: any) => {
|
||||
fields.push(mapping.targetField);
|
||||
@@ -796,6 +936,8 @@ export class NodeFlowExecutionService {
|
||||
? mapping.staticValue
|
||||
: data[mapping.sourceField];
|
||||
values.push(value);
|
||||
// 🔥 삽입된 데이터 객체에 매핑된 값 적용
|
||||
insertedData[mapping.targetField] = value;
|
||||
});
|
||||
|
||||
// 외부 DB별 SQL 문법 차이 처리
|
||||
@@ -828,6 +970,7 @@ export class NodeFlowExecutionService {
|
||||
|
||||
await connector.executeQuery(sql, params);
|
||||
insertedCount++;
|
||||
insertedDataArray.push(insertedData);
|
||||
}
|
||||
|
||||
// 🔥 Oracle의 경우 명시적 COMMIT
|
||||
@@ -841,7 +984,8 @@ export class NodeFlowExecutionService {
|
||||
`✅ INSERT 완료 (외부 DB): ${externalTargetTable}, ${insertedCount}건`
|
||||
);
|
||||
|
||||
return { insertedCount };
|
||||
// 🔥 삽입된 데이터 반환 (외부 DB는 자동 생성 ID 없으므로 입력 데이터 기반)
|
||||
return insertedDataArray;
|
||||
} catch (error) {
|
||||
// 🔥 Oracle의 경우 오류 시 ROLLBACK
|
||||
await this.rollbackExternalTransaction(connector, externalDbType);
|
||||
@@ -985,38 +1129,28 @@ export class NodeFlowExecutionService {
|
||||
connectionId: number,
|
||||
dbType: string
|
||||
): Promise<any> {
|
||||
// 외부 DB 커넥션 정보 조회
|
||||
const connectionData: any = await queryOne(
|
||||
"SELECT * FROM external_db_connections WHERE id = $1",
|
||||
[connectionId]
|
||||
// 🔥 연결 풀 서비스를 통한 연결 관리 (연결 풀 고갈 방지)
|
||||
const { ExternalDbConnectionPoolService } = await import(
|
||||
"./externalDbConnectionPoolService"
|
||||
);
|
||||
const poolService = ExternalDbConnectionPoolService.getInstance();
|
||||
const pool = await poolService.getPool(connectionId);
|
||||
|
||||
if (!connectionData) {
|
||||
throw new Error(`외부 DB 커넥션을 찾을 수 없습니다: ${connectionId}`);
|
||||
}
|
||||
|
||||
// 패스워드 복호화
|
||||
const { EncryptUtil } = await import("../utils/encryptUtil");
|
||||
const decryptedPassword = EncryptUtil.decrypt(connectionData.password);
|
||||
|
||||
const config = {
|
||||
host: connectionData.host,
|
||||
port: connectionData.port,
|
||||
database: connectionData.database_name,
|
||||
user: connectionData.username,
|
||||
password: decryptedPassword,
|
||||
// DatabaseConnectorFactory와 호환되도록 래퍼 객체 반환
|
||||
return {
|
||||
executeQuery: async (sql: string, params?: any[]) => {
|
||||
const result = await pool.query(sql, params);
|
||||
return {
|
||||
rows: Array.isArray(result) ? result : [result],
|
||||
rowCount: Array.isArray(result) ? result.length : 1,
|
||||
affectedRows: Array.isArray(result) ? result.length : 1,
|
||||
};
|
||||
},
|
||||
disconnect: async () => {
|
||||
// 연결 풀은 자동 관리되므로 즉시 종료하지 않음
|
||||
logger.debug(`📌 연결 풀 유지 (ID: ${connectionId})`);
|
||||
},
|
||||
};
|
||||
|
||||
// DatabaseConnectorFactory를 사용하여 외부 DB 연결
|
||||
const { DatabaseConnectorFactory } = await import(
|
||||
"../database/DatabaseConnectorFactory"
|
||||
);
|
||||
|
||||
return await DatabaseConnectorFactory.createConnector(
|
||||
dbType,
|
||||
config,
|
||||
connectionId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1107,12 +1241,16 @@ export class NodeFlowExecutionService {
|
||||
const executeUpdate = async (txClient: any) => {
|
||||
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
||||
let updatedCount = 0;
|
||||
const updatedDataArray: any[] = [];
|
||||
|
||||
for (const data of dataArray) {
|
||||
const setClauses: string[] = [];
|
||||
const values: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 🔥 업데이트된 데이터 복사본 생성
|
||||
const updatedData = { ...data };
|
||||
|
||||
console.log("🗺️ 필드 매핑 처리 중...");
|
||||
fieldMappings.forEach((mapping: any) => {
|
||||
const value =
|
||||
@@ -1123,21 +1261,35 @@ export class NodeFlowExecutionService {
|
||||
console.log(
|
||||
` ${mapping.sourceField} → ${mapping.targetField}: ${value === undefined ? "❌ undefined" : "✅ " + value}`
|
||||
);
|
||||
setClauses.push(`${mapping.targetField} = $${paramIndex}`);
|
||||
values.push(value);
|
||||
paramIndex++;
|
||||
|
||||
// targetField가 비어있지 않은 경우만 추가
|
||||
if (mapping.targetField) {
|
||||
setClauses.push(`${mapping.targetField} = $${paramIndex}`);
|
||||
values.push(value);
|
||||
paramIndex++;
|
||||
|
||||
// 🔥 업데이트된 값을 데이터에 반영
|
||||
updatedData[mapping.targetField] = value;
|
||||
} else {
|
||||
console.log(
|
||||
`⚠️ targetField가 비어있어 스킵: ${mapping.sourceField}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const whereClause = this.buildWhereClause(
|
||||
const whereResult = this.buildWhereClause(
|
||||
whereConditions,
|
||||
data,
|
||||
paramIndex
|
||||
);
|
||||
|
||||
// WHERE 절의 값들을 values 배열에 추가
|
||||
values.push(...whereResult.values);
|
||||
|
||||
const sql = `
|
||||
UPDATE ${targetTable}
|
||||
SET ${setClauses.join(", ")}
|
||||
${whereClause}
|
||||
${whereResult.clause}
|
||||
`;
|
||||
|
||||
console.log("📝 실행할 SQL:", sql);
|
||||
@@ -1145,13 +1297,17 @@ export class NodeFlowExecutionService {
|
||||
|
||||
const result = await txClient.query(sql, values);
|
||||
updatedCount += result.rowCount || 0;
|
||||
|
||||
// 🔥 업데이트된 데이터 저장
|
||||
updatedDataArray.push(updatedData);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`✅ UPDATE 완료 (내부 DB): ${targetTable}, ${updatedCount}건`
|
||||
);
|
||||
|
||||
return { updatedCount };
|
||||
// 🔥 업데이트된 데이터 반환 (다음 노드에서 사용)
|
||||
return updatedDataArray;
|
||||
};
|
||||
|
||||
// 🔥 클라이언트가 전달되었으면 사용, 아니면 독립 트랜잭션 생성
|
||||
@@ -1195,11 +1351,13 @@ export class NodeFlowExecutionService {
|
||||
try {
|
||||
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
||||
let updatedCount = 0;
|
||||
const updatedDataArray: any[] = [];
|
||||
|
||||
for (const data of dataArray) {
|
||||
const setClauses: string[] = [];
|
||||
const values: any[] = [];
|
||||
let paramIndex = 1;
|
||||
const updatedData: any = { ...data };
|
||||
|
||||
fieldMappings.forEach((mapping: any) => {
|
||||
const value =
|
||||
@@ -1222,6 +1380,8 @@ export class NodeFlowExecutionService {
|
||||
|
||||
values.push(value);
|
||||
paramIndex++;
|
||||
// 🔥 업데이트된 데이터 객체에 매핑된 값 적용
|
||||
updatedData[mapping.targetField] = value;
|
||||
});
|
||||
|
||||
// WHERE 조건 생성
|
||||
@@ -1263,6 +1423,7 @@ export class NodeFlowExecutionService {
|
||||
|
||||
const result = await connector.executeQuery(sql, values);
|
||||
updatedCount += result.rowCount || result.affectedRows || 0;
|
||||
updatedDataArray.push(updatedData);
|
||||
}
|
||||
|
||||
// 🔥 Oracle의 경우 명시적 COMMIT
|
||||
@@ -1276,7 +1437,8 @@ export class NodeFlowExecutionService {
|
||||
`✅ UPDATE 완료 (외부 DB): ${externalTargetTable}, ${updatedCount}건`
|
||||
);
|
||||
|
||||
return { updatedCount };
|
||||
// 🔥 업데이트된 데이터 반환
|
||||
return updatedDataArray;
|
||||
} catch (error) {
|
||||
// 🔥 Oracle의 경우 오류 시 ROLLBACK
|
||||
await this.rollbackExternalTransaction(connector, externalDbType);
|
||||
@@ -1439,24 +1601,32 @@ export class NodeFlowExecutionService {
|
||||
const executeDelete = async (txClient: any) => {
|
||||
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
||||
let deletedCount = 0;
|
||||
const deletedDataArray: any[] = [];
|
||||
|
||||
for (const data of dataArray) {
|
||||
console.log("🔍 WHERE 조건 처리 중...");
|
||||
const whereClause = this.buildWhereClause(whereConditions, data, 1);
|
||||
const whereResult = this.buildWhereClause(whereConditions, data, 1);
|
||||
|
||||
const sql = `DELETE FROM ${targetTable} ${whereClause}`;
|
||||
const sql = `DELETE FROM ${targetTable} ${whereResult.clause} RETURNING *`;
|
||||
|
||||
console.log("📝 실행할 SQL:", sql);
|
||||
console.log("📊 바인딩 값:", whereResult.values);
|
||||
|
||||
const result = await txClient.query(sql, []);
|
||||
const result = await txClient.query(sql, whereResult.values);
|
||||
deletedCount += result.rowCount || 0;
|
||||
|
||||
// 🔥 RETURNING으로 받은 삭제된 데이터 저장
|
||||
if (result.rows && result.rows.length > 0) {
|
||||
deletedDataArray.push(...result.rows);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`✅ DELETE 완료 (내부 DB): ${targetTable}, ${deletedCount}건`
|
||||
);
|
||||
|
||||
return { deletedCount };
|
||||
// 🔥 삭제된 데이터 반환 (로그 기록 등에 사용)
|
||||
return deletedDataArray;
|
||||
};
|
||||
|
||||
// 🔥 클라이언트가 전달되었으면 사용, 아니면 독립 트랜잭션 생성
|
||||
@@ -1499,6 +1669,7 @@ export class NodeFlowExecutionService {
|
||||
try {
|
||||
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
||||
let deletedCount = 0;
|
||||
const deletedDataArray: any[] = [];
|
||||
|
||||
for (const data of dataArray) {
|
||||
const whereClauses: string[] = [];
|
||||
@@ -1545,9 +1716,16 @@ export class NodeFlowExecutionService {
|
||||
);
|
||||
}
|
||||
|
||||
const sql = `DELETE FROM ${externalTargetTable} ${whereClause}`;
|
||||
// 🔥 삭제 전에 데이터 조회 (로그 기록 용도)
|
||||
const selectSql = `SELECT * FROM ${externalTargetTable} ${whereClause}`;
|
||||
const selectResult = await connector.executeQuery(selectSql, values);
|
||||
if (selectResult && selectResult.length > 0) {
|
||||
deletedDataArray.push(...selectResult);
|
||||
}
|
||||
|
||||
const result = await connector.executeQuery(sql, values);
|
||||
// 실제 삭제 수행
|
||||
const deleteSql = `DELETE FROM ${externalTargetTable} ${whereClause}`;
|
||||
const result = await connector.executeQuery(deleteSql, values);
|
||||
deletedCount += result.rowCount || result.affectedRows || 0;
|
||||
}
|
||||
|
||||
@@ -1562,7 +1740,8 @@ export class NodeFlowExecutionService {
|
||||
`✅ DELETE 완료 (외부 DB): ${externalTargetTable}, ${deletedCount}건`
|
||||
);
|
||||
|
||||
return { deletedCount };
|
||||
// 🔥 삭제된 데이터 반환
|
||||
return deletedDataArray;
|
||||
} catch (error) {
|
||||
// 🔥 Oracle의 경우 오류 시 ROLLBACK
|
||||
await this.rollbackExternalTransaction(connector, externalDbType);
|
||||
@@ -2135,16 +2314,93 @@ export class NodeFlowExecutionService {
|
||||
node: FlowNode,
|
||||
inputData: any,
|
||||
context: ExecutionContext
|
||||
): Promise<boolean> {
|
||||
): Promise<any> {
|
||||
const { conditions, logic } = node.data;
|
||||
|
||||
logger.info(
|
||||
`🔍 조건 노드 실행 - inputData 타입: ${typeof inputData}, 배열 여부: ${Array.isArray(inputData)}, 길이: ${Array.isArray(inputData) ? inputData.length : "N/A"}`
|
||||
);
|
||||
logger.info(`🔍 조건 개수: ${conditions?.length || 0}, 로직: ${logic}`);
|
||||
|
||||
if (inputData) {
|
||||
console.log(
|
||||
"📥 조건 노드 입력 데이터:",
|
||||
JSON.stringify(inputData, null, 2).substring(0, 500)
|
||||
);
|
||||
} else {
|
||||
console.log("⚠️ 조건 노드 입력 데이터가 없습니다!");
|
||||
}
|
||||
|
||||
// 조건이 없으면 모든 데이터 통과
|
||||
if (!conditions || conditions.length === 0) {
|
||||
logger.info("⚠️ 조건이 설정되지 않음 - 모든 데이터 통과");
|
||||
const dataArray = Array.isArray(inputData) ? inputData : [inputData];
|
||||
return {
|
||||
conditionResult: true,
|
||||
trueData: dataArray,
|
||||
falseData: [],
|
||||
allData: dataArray,
|
||||
};
|
||||
}
|
||||
|
||||
// inputData가 배열인 경우 각 항목을 필터링
|
||||
if (Array.isArray(inputData)) {
|
||||
const trueData: any[] = [];
|
||||
const falseData: any[] = [];
|
||||
|
||||
inputData.forEach((item: any) => {
|
||||
const results = conditions.map((condition: any) => {
|
||||
const fieldValue = item[condition.field];
|
||||
|
||||
let compareValue = condition.value;
|
||||
if (condition.valueType === "field") {
|
||||
compareValue = item[condition.value];
|
||||
logger.info(
|
||||
`🔄 필드 참조 비교: ${condition.field} (${fieldValue}) vs ${condition.value} (${compareValue})`
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`📊 고정값 비교: ${condition.field} (${fieldValue}) vs ${compareValue}`
|
||||
);
|
||||
}
|
||||
|
||||
return this.evaluateCondition(
|
||||
fieldValue,
|
||||
condition.operator,
|
||||
compareValue
|
||||
);
|
||||
});
|
||||
|
||||
const result =
|
||||
logic === "OR"
|
||||
? results.some((r: boolean) => r)
|
||||
: results.every((r: boolean) => r);
|
||||
|
||||
if (result) {
|
||||
trueData.push(item);
|
||||
} else {
|
||||
falseData.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`🔍 조건 필터링 결과: TRUE ${trueData.length}건 / FALSE ${falseData.length}건 (${logic} 로직)`
|
||||
);
|
||||
|
||||
return {
|
||||
conditionResult: trueData.length > 0,
|
||||
trueData,
|
||||
falseData,
|
||||
allData: inputData,
|
||||
};
|
||||
}
|
||||
|
||||
// 단일 객체인 경우
|
||||
const results = conditions.map((condition: any) => {
|
||||
const fieldValue = inputData[condition.field];
|
||||
|
||||
// 🔥 비교 값 타입 확인: "field" (필드 참조) 또는 "static" (고정값)
|
||||
let compareValue = condition.value;
|
||||
if (condition.valueType === "field") {
|
||||
// 필드 참조: inputData에서 해당 필드의 값을 가져옴
|
||||
compareValue = inputData[condition.value];
|
||||
logger.info(
|
||||
`🔄 필드 참조 비교: ${condition.field} (${fieldValue}) vs ${condition.value} (${compareValue})`
|
||||
@@ -2169,7 +2425,15 @@ export class NodeFlowExecutionService {
|
||||
|
||||
logger.info(`🔍 조건 평가 결과: ${result} (${logic} 로직)`);
|
||||
|
||||
return result;
|
||||
// ⚠️ 조건 노드는 TRUE/FALSE 브랜치를 위한 특별한 처리 필요
|
||||
// 조건 결과를 저장하고, 원본 데이터는 항상 반환
|
||||
// 다음 노드에서 sourceHandle을 기반으로 필터링됨
|
||||
return {
|
||||
conditionResult: result,
|
||||
trueData: result ? [inputData] : [],
|
||||
falseData: result ? [] : [inputData],
|
||||
allData: [inputData], // 일단 모든 데이터 전달
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2179,17 +2443,71 @@ export class NodeFlowExecutionService {
|
||||
conditions: any[],
|
||||
data?: any,
|
||||
startIndex: number = 1
|
||||
): string {
|
||||
): { clause: string; values: any[] } {
|
||||
if (!conditions || conditions.length === 0) {
|
||||
return "";
|
||||
return { clause: "", values: [] };
|
||||
}
|
||||
|
||||
const values: any[] = [];
|
||||
const clauses = conditions.map((condition, index) => {
|
||||
const value = data ? data[condition.field] : condition.value;
|
||||
return `${condition.field} ${condition.operator} $${startIndex + index}`;
|
||||
values.push(value);
|
||||
|
||||
// 연산자를 SQL 문법으로 변환
|
||||
let sqlOperator = condition.operator;
|
||||
switch (condition.operator.toUpperCase()) {
|
||||
case "EQUALS":
|
||||
sqlOperator = "=";
|
||||
break;
|
||||
case "NOT_EQUALS":
|
||||
case "NOTEQUALS":
|
||||
sqlOperator = "!=";
|
||||
break;
|
||||
case "GREATER_THAN":
|
||||
case "GREATERTHAN":
|
||||
sqlOperator = ">";
|
||||
break;
|
||||
case "LESS_THAN":
|
||||
case "LESSTHAN":
|
||||
sqlOperator = "<";
|
||||
break;
|
||||
case "GREATER_THAN_OR_EQUAL":
|
||||
case "GREATERTHANOREQUAL":
|
||||
sqlOperator = ">=";
|
||||
break;
|
||||
case "LESS_THAN_OR_EQUAL":
|
||||
case "LESSTHANOREQUAL":
|
||||
sqlOperator = "<=";
|
||||
break;
|
||||
case "LIKE":
|
||||
sqlOperator = "LIKE";
|
||||
break;
|
||||
case "NOT_LIKE":
|
||||
case "NOTLIKE":
|
||||
sqlOperator = "NOT LIKE";
|
||||
break;
|
||||
case "IN":
|
||||
sqlOperator = "IN";
|
||||
break;
|
||||
case "NOT_IN":
|
||||
case "NOTIN":
|
||||
sqlOperator = "NOT IN";
|
||||
break;
|
||||
case "IS_NULL":
|
||||
case "ISNULL":
|
||||
return `${condition.field} IS NULL`;
|
||||
case "IS_NOT_NULL":
|
||||
case "ISNOTNULL":
|
||||
return `${condition.field} IS NOT NULL`;
|
||||
default:
|
||||
// 이미 SQL 문법인 경우 (=, !=, >, < 등)
|
||||
sqlOperator = condition.operator;
|
||||
}
|
||||
|
||||
return `${condition.field} ${sqlOperator} $${startIndex + index}`;
|
||||
});
|
||||
|
||||
return `WHERE ${clauses.join(" AND ")}`;
|
||||
return { clause: `WHERE ${clauses.join(" AND ")}`, values };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2200,22 +2518,85 @@ export class NodeFlowExecutionService {
|
||||
operator: string,
|
||||
expectedValue: any
|
||||
): boolean {
|
||||
switch (operator) {
|
||||
case "equals":
|
||||
// NULL 체크
|
||||
if (operator === "IS_NULL" || operator === "isNull") {
|
||||
return (
|
||||
fieldValue === null || fieldValue === undefined || fieldValue === ""
|
||||
);
|
||||
}
|
||||
if (operator === "IS_NOT_NULL" || operator === "isNotNull") {
|
||||
return (
|
||||
fieldValue !== null && fieldValue !== undefined && fieldValue !== ""
|
||||
);
|
||||
}
|
||||
|
||||
// 비교 연산자: 타입 변환
|
||||
const normalizedOperator = operator.toUpperCase();
|
||||
|
||||
switch (normalizedOperator) {
|
||||
case "EQUALS":
|
||||
case "=":
|
||||
return fieldValue === expectedValue;
|
||||
case "notEquals":
|
||||
return fieldValue == expectedValue; // 느슨한 비교
|
||||
|
||||
case "NOT_EQUALS":
|
||||
case "NOTEQUALS":
|
||||
case "!=":
|
||||
return fieldValue !== expectedValue;
|
||||
case "greaterThan":
|
||||
return fieldValue != expectedValue;
|
||||
|
||||
case "GREATER_THAN":
|
||||
case "GREATERTHAN":
|
||||
case ">":
|
||||
return fieldValue > expectedValue;
|
||||
case "lessThan":
|
||||
return Number(fieldValue) > Number(expectedValue);
|
||||
|
||||
case "LESS_THAN":
|
||||
case "LESSTHAN":
|
||||
case "<":
|
||||
return fieldValue < expectedValue;
|
||||
case "contains":
|
||||
return String(fieldValue).includes(String(expectedValue));
|
||||
return Number(fieldValue) < Number(expectedValue);
|
||||
|
||||
case "GREATER_THAN_OR_EQUAL":
|
||||
case "GREATERTHANOREQUAL":
|
||||
case ">=":
|
||||
return Number(fieldValue) >= Number(expectedValue);
|
||||
|
||||
case "LESS_THAN_OR_EQUAL":
|
||||
case "LESSTHANOREQUAL":
|
||||
case "<=":
|
||||
return Number(fieldValue) <= Number(expectedValue);
|
||||
|
||||
case "LIKE":
|
||||
case "CONTAINS":
|
||||
return String(fieldValue)
|
||||
.toLowerCase()
|
||||
.includes(String(expectedValue).toLowerCase());
|
||||
|
||||
case "NOT_LIKE":
|
||||
case "NOTLIKE":
|
||||
return !String(fieldValue)
|
||||
.toLowerCase()
|
||||
.includes(String(expectedValue).toLowerCase());
|
||||
|
||||
case "IN":
|
||||
if (Array.isArray(expectedValue)) {
|
||||
return expectedValue.includes(fieldValue);
|
||||
}
|
||||
// 쉼표로 구분된 문자열
|
||||
const inValues = String(expectedValue)
|
||||
.split(",")
|
||||
.map((v) => v.trim());
|
||||
return inValues.includes(String(fieldValue));
|
||||
|
||||
case "NOT_IN":
|
||||
case "NOTIN":
|
||||
if (Array.isArray(expectedValue)) {
|
||||
return !expectedValue.includes(fieldValue);
|
||||
}
|
||||
const notInValues = String(expectedValue)
|
||||
.split(",")
|
||||
.map((v) => v.trim());
|
||||
return !notInValues.includes(String(fieldValue));
|
||||
|
||||
default:
|
||||
logger.warn(`⚠️ 지원되지 않는 연산자: ${operator}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user