제어관리 노드 작동 방식 수정

This commit is contained in:
kjs
2025-10-13 17:47:24 +09:00
parent 9d5ac1716d
commit 0dc4d53876
15 changed files with 1567 additions and 407 deletions

View File

@@ -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;
}
}