1137 lines
34 KiB
TypeScript
1137 lines
34 KiB
TypeScript
import { query, queryOne } from "../database/db";
|
|
|
|
export interface ControlCondition {
|
|
id: string;
|
|
type: "condition" | "group-start" | "group-end";
|
|
field?: string;
|
|
value?: any;
|
|
operator?: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN";
|
|
dataType?: "string" | "number" | "date" | "boolean";
|
|
logicalOperator?: "AND" | "OR";
|
|
groupId?: string;
|
|
groupLevel?: number;
|
|
tableType?: "from" | "to";
|
|
}
|
|
|
|
export interface ControlAction {
|
|
id: string;
|
|
name: string;
|
|
actionType: "insert" | "update" | "delete";
|
|
logicalOperator?: "AND" | "OR"; // 액션 간 논리 연산자 (첫 번째 액션 제외)
|
|
conditions: ControlCondition[];
|
|
fieldMappings: {
|
|
sourceField?: string;
|
|
sourceTable?: string;
|
|
targetField: string;
|
|
targetTable: string;
|
|
defaultValue?: any;
|
|
}[];
|
|
splitConfig?: {
|
|
delimiter?: string;
|
|
sourceField?: string;
|
|
targetField?: string;
|
|
};
|
|
// 🆕 다중 커넥션 지원 추가
|
|
fromConnection?: {
|
|
id: number;
|
|
name?: string;
|
|
};
|
|
toConnection?: {
|
|
id: number;
|
|
name?: string;
|
|
};
|
|
targetTable?: string;
|
|
}
|
|
|
|
export interface ControlPlan {
|
|
id: string;
|
|
sourceTable: string;
|
|
actions: ControlAction[];
|
|
}
|
|
|
|
export interface ControlRule {
|
|
id: string;
|
|
triggerType: "insert" | "update" | "delete";
|
|
conditions: ControlCondition[];
|
|
}
|
|
|
|
export class DataflowControlService {
|
|
/**
|
|
* 제어관리 실행 메인 함수
|
|
*/
|
|
async executeDataflowControl(
|
|
diagramId: number,
|
|
relationshipId: string,
|
|
triggerType: "insert" | "update" | "delete",
|
|
sourceData: Record<string, any>,
|
|
tableName: string,
|
|
userId: string = "system"
|
|
): Promise<{
|
|
success: boolean;
|
|
message: string;
|
|
executedActions?: any[];
|
|
errors?: string[];
|
|
}> {
|
|
try {
|
|
console.log(`🎯 제어관리 실행 시작:`, {
|
|
diagramId,
|
|
relationshipId,
|
|
triggerType,
|
|
sourceData,
|
|
tableName,
|
|
userId,
|
|
});
|
|
|
|
// 관계도 정보 조회
|
|
const diagram = await queryOne<any>(
|
|
`SELECT * FROM dataflow_diagrams WHERE diagram_id = $1`,
|
|
[diagramId]
|
|
);
|
|
|
|
if (!diagram) {
|
|
return {
|
|
success: false,
|
|
message: `관계도를 찾을 수 없습니다. (ID: ${diagramId})`,
|
|
};
|
|
}
|
|
|
|
// 제어 규칙과 실행 계획 추출 (기존 구조 + redesigned UI 구조 지원)
|
|
let controlRules: ControlRule[] = [];
|
|
let executionPlans: ControlPlan[] = [];
|
|
|
|
// 🆕 redesigned UI 구조 처리
|
|
if (diagram.relationships && typeof diagram.relationships === "object") {
|
|
const relationships = diagram.relationships as any;
|
|
|
|
// Case 1: redesigned UI 단일 관계 구조
|
|
if (relationships.controlConditions && relationships.fieldMappings) {
|
|
console.log("🔄 Redesigned UI 구조 감지, 기존 구조로 변환 중");
|
|
|
|
// redesigned → 기존 구조 변환
|
|
controlRules = [
|
|
{
|
|
id: relationshipId,
|
|
triggerType: triggerType,
|
|
conditions: relationships.controlConditions || [],
|
|
},
|
|
];
|
|
|
|
executionPlans = [
|
|
{
|
|
id: relationshipId,
|
|
sourceTable: relationships.fromTable || tableName,
|
|
actions: [
|
|
{
|
|
id: "action_1",
|
|
name: "액션 1",
|
|
actionType: relationships.actionType || "insert",
|
|
conditions: relationships.actionConditions || [],
|
|
fieldMappings: relationships.fieldMappings || [],
|
|
fromConnection: relationships.fromConnection,
|
|
toConnection: relationships.toConnection,
|
|
targetTable: relationships.toTable,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
console.log("✅ Redesigned → 기존 구조 변환 완료");
|
|
}
|
|
}
|
|
|
|
// 기존 구조 처리 (하위 호환성)
|
|
if (controlRules.length === 0) {
|
|
controlRules = Array.isArray(diagram.control)
|
|
? (diagram.control as unknown as ControlRule[])
|
|
: [];
|
|
executionPlans = Array.isArray(diagram.plan)
|
|
? (diagram.plan as unknown as ControlPlan[])
|
|
: [];
|
|
}
|
|
|
|
console.log(`📋 제어 규칙:`, controlRules);
|
|
console.log(`📋 실행 계획:`, executionPlans);
|
|
|
|
// 해당 관계의 제어 규칙 찾기
|
|
const targetRule = controlRules.find(
|
|
(rule) => rule.id === relationshipId && rule.triggerType === triggerType
|
|
);
|
|
|
|
if (!targetRule) {
|
|
console.log(
|
|
`⚠️ 해당 관계의 제어 규칙을 찾을 수 없습니다: ${relationshipId}`
|
|
);
|
|
return {
|
|
success: true,
|
|
message: "해당 관계의 제어 규칙이 없습니다.",
|
|
};
|
|
}
|
|
|
|
// 제어 조건 검증
|
|
const conditionResult = await this.evaluateConditions(
|
|
targetRule.conditions,
|
|
sourceData
|
|
);
|
|
|
|
console.log(`🔍 [전체 실행 조건] 검증 결과:`, conditionResult);
|
|
|
|
if (!conditionResult.satisfied) {
|
|
return {
|
|
success: true,
|
|
message: `제어 조건을 만족하지 않습니다: ${conditionResult.reason}`,
|
|
};
|
|
}
|
|
|
|
// 실행 계획 찾기
|
|
const targetPlan = executionPlans.find(
|
|
(plan) => plan.id === relationshipId
|
|
);
|
|
|
|
if (!targetPlan) {
|
|
return {
|
|
success: true,
|
|
message: "실행할 계획이 없습니다.",
|
|
};
|
|
}
|
|
|
|
// 액션 실행 (논리 연산자 지원)
|
|
const executedActions = [];
|
|
const errors = [];
|
|
let previousActionSuccess = false;
|
|
let shouldSkipRemainingActions = false;
|
|
|
|
for (let i = 0; i < targetPlan.actions.length; i++) {
|
|
const action = targetPlan.actions[i];
|
|
|
|
try {
|
|
// 논리 연산자에 따른 실행 여부 결정
|
|
if (
|
|
i > 0 &&
|
|
action.logicalOperator === "OR" &&
|
|
previousActionSuccess
|
|
) {
|
|
console.log(
|
|
`⏭️ OR 조건으로 인해 액션 건너뛰기: ${action.name} (이전 액션 성공)`
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if (shouldSkipRemainingActions && action.logicalOperator === "AND") {
|
|
console.log(
|
|
`⏭️ 이전 액션 실패로 인해 AND 체인 액션 건너뛰기: ${action.name}`
|
|
);
|
|
continue;
|
|
}
|
|
|
|
console.log(`⚡ 액션 실행: ${action.name} (${action.actionType})`);
|
|
console.log(`📋 액션 상세 정보:`, {
|
|
actionId: action.id,
|
|
actionName: action.name,
|
|
actionType: action.actionType,
|
|
logicalOperator: action.logicalOperator,
|
|
conditions: action.conditions,
|
|
fieldMappings: action.fieldMappings,
|
|
fromConnection: (action as any).fromConnection,
|
|
toConnection: (action as any).toConnection,
|
|
targetTable: (action as any).targetTable,
|
|
});
|
|
|
|
// 🆕 다중 커넥션 지원 액션 실행
|
|
const actionResult = await this.executeMultiConnectionAction(
|
|
action,
|
|
sourceData,
|
|
targetPlan.sourceTable,
|
|
userId
|
|
);
|
|
|
|
executedActions.push({
|
|
actionId: action.id,
|
|
actionName: action.name,
|
|
actionType: action.actionType,
|
|
result: actionResult,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
previousActionSuccess = actionResult?.success !== false;
|
|
|
|
// 액션 조건 검증은 이미 위에서 처리됨 (중복 제거)
|
|
} catch (error) {
|
|
console.error(`❌ 액션 실행 오류: ${action.name}`, error);
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : String(error);
|
|
errors.push(`액션 '${action.name}' 실행 오류: ${errorMessage}`);
|
|
|
|
previousActionSuccess = false;
|
|
if (action.logicalOperator === "AND") {
|
|
shouldSkipRemainingActions = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
message: `제어관리 실행 완료. ${executedActions.length}개 액션 실행됨.`,
|
|
executedActions,
|
|
errors: errors.length > 0 ? errors : undefined,
|
|
};
|
|
} catch (error) {
|
|
console.error("❌ 제어관리 실행 오류:", error);
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : String(error);
|
|
return {
|
|
success: false,
|
|
message: `제어관리 실행 중 오류 발생: ${errorMessage}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 🆕 다중 커넥션 액션 실행
|
|
*/
|
|
private async executeMultiConnectionAction(
|
|
action: ControlAction,
|
|
sourceData: Record<string, any>,
|
|
sourceTable: string,
|
|
userId: string = "system"
|
|
): Promise<any> {
|
|
try {
|
|
const extendedAction = action as any; // redesigned UI 구조 접근
|
|
|
|
// 연결 정보 추출
|
|
const fromConnection = extendedAction.fromConnection || { id: 0 };
|
|
const toConnection = extendedAction.toConnection || { id: 0 };
|
|
const targetTable = extendedAction.targetTable || sourceTable;
|
|
|
|
console.log(`🔗 다중 커넥션 액션 실행:`, {
|
|
actionType: action.actionType,
|
|
fromConnectionId: fromConnection.id,
|
|
toConnectionId: toConnection.id,
|
|
sourceTable,
|
|
targetTable,
|
|
});
|
|
|
|
// MultiConnectionQueryService import 필요
|
|
const { MultiConnectionQueryService } = await import(
|
|
"./multiConnectionQueryService"
|
|
);
|
|
const multiConnService = new MultiConnectionQueryService();
|
|
|
|
switch (action.actionType) {
|
|
case "insert":
|
|
return await this.executeMultiConnectionInsert(
|
|
action,
|
|
sourceData,
|
|
sourceTable,
|
|
targetTable,
|
|
fromConnection.id,
|
|
toConnection.id,
|
|
multiConnService,
|
|
userId
|
|
);
|
|
|
|
case "update":
|
|
return await this.executeMultiConnectionUpdate(
|
|
action,
|
|
sourceData,
|
|
sourceTable,
|
|
targetTable,
|
|
fromConnection.id,
|
|
toConnection.id,
|
|
multiConnService,
|
|
userId
|
|
);
|
|
|
|
case "delete":
|
|
return await this.executeMultiConnectionDelete(
|
|
action,
|
|
sourceData,
|
|
sourceTable,
|
|
targetTable,
|
|
fromConnection.id,
|
|
toConnection.id,
|
|
multiConnService,
|
|
userId
|
|
);
|
|
|
|
default:
|
|
throw new Error(`지원되지 않는 액션 타입: ${action.actionType}`);
|
|
}
|
|
} catch (error) {
|
|
console.error(`❌ 다중 커넥션 액션 실행 실패:`, error);
|
|
return {
|
|
success: false,
|
|
message: error instanceof Error ? error.message : String(error),
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 🆕 다중 커넥션 INSERT 실행
|
|
*/
|
|
private async executeMultiConnectionInsert(
|
|
action: ControlAction,
|
|
sourceData: Record<string, any>,
|
|
sourceTable: string,
|
|
targetTable: string,
|
|
fromConnectionId: number,
|
|
toConnectionId: number,
|
|
multiConnService: any,
|
|
userId: string = "system"
|
|
): Promise<any> {
|
|
try {
|
|
// 필드 매핑 적용
|
|
const mappedData: Record<string, any> = {};
|
|
|
|
for (const mapping of action.fieldMappings) {
|
|
const sourceField = mapping.sourceField;
|
|
const targetField = mapping.targetField;
|
|
|
|
if (mapping.defaultValue !== undefined) {
|
|
// 기본값 사용
|
|
mappedData[targetField] = mapping.defaultValue;
|
|
} else if (sourceField && sourceData[sourceField] !== undefined) {
|
|
// 소스 데이터에서 매핑
|
|
mappedData[targetField] = sourceData[sourceField];
|
|
}
|
|
}
|
|
|
|
// 🆕 변경자 정보 추가
|
|
if (!mappedData.created_by) {
|
|
mappedData.created_by = userId;
|
|
}
|
|
if (!mappedData.updated_by) {
|
|
mappedData.updated_by = userId;
|
|
}
|
|
|
|
console.log(`📋 매핑된 데이터:`, mappedData);
|
|
|
|
// 대상 연결에 데이터 삽입
|
|
const result = await multiConnService.insertDataToConnection(
|
|
toConnectionId,
|
|
targetTable,
|
|
mappedData
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
message: `${targetTable}에 데이터 삽입 완료`,
|
|
insertedCount: 1,
|
|
data: result,
|
|
};
|
|
} catch (error) {
|
|
console.error(`❌ INSERT 실행 실패:`, error);
|
|
return {
|
|
success: false,
|
|
message: error instanceof Error ? error.message : String(error),
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 🆕 다중 커넥션 UPDATE 실행
|
|
*/
|
|
private async executeMultiConnectionUpdate(
|
|
action: ControlAction,
|
|
sourceData: Record<string, any>,
|
|
sourceTable: string,
|
|
targetTable: string,
|
|
fromConnectionId: number,
|
|
toConnectionId: number,
|
|
multiConnService: any,
|
|
userId: string = "system"
|
|
): Promise<any> {
|
|
try {
|
|
// 필드 매핑 적용
|
|
const mappedData: Record<string, any> = {};
|
|
|
|
for (const mapping of action.fieldMappings) {
|
|
const sourceField = mapping.sourceField;
|
|
const targetField = mapping.targetField;
|
|
|
|
if (mapping.defaultValue !== undefined) {
|
|
mappedData[targetField] = mapping.defaultValue;
|
|
} else if (sourceField && sourceData[sourceField] !== undefined) {
|
|
mappedData[targetField] = sourceData[sourceField];
|
|
}
|
|
}
|
|
|
|
// 🆕 변경자 정보 추가
|
|
if (!mappedData.updated_by) {
|
|
mappedData.updated_by = userId;
|
|
}
|
|
|
|
console.log(`📋 UPDATE 매핑된 데이터:`, mappedData);
|
|
console.log(`⚠️ UPDATE 액션은 향후 구현 예정`);
|
|
|
|
return {
|
|
success: true,
|
|
message: "UPDATE 액션 실행됨 (향후 구현)",
|
|
updatedCount: 0,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
message: error instanceof Error ? error.message : String(error),
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 🆕 다중 커넥션 DELETE 실행
|
|
*/
|
|
private async executeMultiConnectionDelete(
|
|
action: ControlAction,
|
|
sourceData: Record<string, any>,
|
|
sourceTable: string,
|
|
targetTable: string,
|
|
fromConnectionId: number,
|
|
toConnectionId: number,
|
|
multiConnService: any,
|
|
userId: string = "system"
|
|
): Promise<any> {
|
|
try {
|
|
console.log(`⚠️ DELETE 액션은 향후 구현 예정 (변경자: ${userId})`);
|
|
return {
|
|
success: true,
|
|
message: "DELETE 액션 실행됨 (향후 구현)",
|
|
deletedCount: 0,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
message: error instanceof Error ? error.message : String(error),
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 액션별 조건 평가 (동적 테이블 지원)
|
|
*/
|
|
private async evaluateActionConditions(
|
|
action: ControlAction,
|
|
sourceData: Record<string, any>,
|
|
sourceTable: string
|
|
): Promise<{ satisfied: boolean; reason?: string }> {
|
|
if (!action.conditions || action.conditions.length === 0) {
|
|
return { satisfied: true };
|
|
}
|
|
|
|
try {
|
|
// 조건별로 테이블 타입에 따라 데이터 소스 결정
|
|
for (const condition of action.conditions) {
|
|
if (!condition.field || condition.value === undefined) {
|
|
continue;
|
|
}
|
|
|
|
let dataToCheck: Record<string, any>;
|
|
let tableName: string;
|
|
|
|
// UPDATE/DELETE 액션의 경우 조건은 항상 대상 테이블에서 확인 (업데이트/삭제할 기존 데이터를 찾는 용도)
|
|
if (
|
|
action.actionType === "update" ||
|
|
action.actionType === "delete" ||
|
|
condition.tableType === "to"
|
|
) {
|
|
// 대상 테이블(to)에서 조건 확인
|
|
const targetTable = action.fieldMappings?.[0]?.targetTable;
|
|
if (!targetTable) {
|
|
console.error("❌ 대상 테이블을 찾을 수 없습니다:", action);
|
|
return {
|
|
satisfied: false,
|
|
reason: "대상 테이블 정보가 없습니다.",
|
|
};
|
|
}
|
|
|
|
tableName = targetTable;
|
|
console.log(
|
|
`🔍 대상 테이블(${tableName})에서 조건 확인: ${condition.field} = ${condition.value} (${action.actionType.toUpperCase()} 액션)`
|
|
);
|
|
|
|
// 대상 테이블에서 컬럼 존재 여부 먼저 확인
|
|
const columnExists = await this.checkColumnExists(
|
|
tableName,
|
|
condition.field
|
|
);
|
|
|
|
if (!columnExists) {
|
|
console.error(
|
|
`❌ 컬럼이 존재하지 않습니다: ${tableName}.${condition.field}`
|
|
);
|
|
return {
|
|
satisfied: false,
|
|
reason: `컬럼이 존재하지 않습니다: ${tableName}.${condition.field}`,
|
|
};
|
|
}
|
|
|
|
// 대상 테이블에서 조건에 맞는 데이터 조회
|
|
const queryResult = await query<Record<string, any>>(
|
|
`SELECT ${condition.field} FROM ${tableName} WHERE ${condition.field} = $1 LIMIT 1`,
|
|
[condition.value]
|
|
);
|
|
|
|
dataToCheck =
|
|
Array.isArray(queryResult) && queryResult.length > 0
|
|
? (queryResult[0] as Record<string, any>)
|
|
: {};
|
|
} else {
|
|
// 소스 테이블(from) 또는 기본값에서 조건 확인
|
|
tableName = sourceTable;
|
|
dataToCheck = sourceData;
|
|
console.log(
|
|
`🔍 소스 테이블(${tableName})에서 조건 확인: ${condition.field} = ${condition.value}`
|
|
);
|
|
}
|
|
|
|
const fieldValue = dataToCheck[condition.field];
|
|
console.log(
|
|
`🔍 [액션 실행 조건] 조건 평가 결과: ${condition.field} = ${condition.value} (테이블 ${tableName} 실제값: ${fieldValue})`
|
|
);
|
|
|
|
// 액션 실행 조건 평가
|
|
if (
|
|
action.actionType === "update" ||
|
|
action.actionType === "delete" ||
|
|
condition.tableType === "to"
|
|
) {
|
|
// UPDATE/DELETE 액션이거나 대상 테이블의 경우 데이터 존재 여부로 판단
|
|
if (!fieldValue || fieldValue !== condition.value) {
|
|
return {
|
|
satisfied: false,
|
|
reason: `[액션 실행 조건] 조건 미충족: ${tableName}.${condition.field} = ${condition.value} (테이블 실제값: ${fieldValue})`,
|
|
};
|
|
}
|
|
} else {
|
|
// 소스 테이블의 경우 값 비교
|
|
if (fieldValue !== condition.value) {
|
|
return {
|
|
satisfied: false,
|
|
reason: `[액션 실행 조건] 조건 미충족: ${tableName}.${condition.field} = ${condition.value} (테이블 실제값: ${fieldValue})`,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return { satisfied: true };
|
|
} catch (error) {
|
|
console.error("❌ 액션 조건 평가 오류:", error);
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : String(error);
|
|
return {
|
|
satisfied: false,
|
|
reason: `액션 조건 평가 오류: ${errorMessage}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 조건 평가
|
|
*/
|
|
private async evaluateConditions(
|
|
conditions: ControlCondition[],
|
|
data: Record<string, any>
|
|
): Promise<{ satisfied: boolean; reason?: string }> {
|
|
if (!conditions || conditions.length === 0) {
|
|
return { satisfied: true };
|
|
}
|
|
|
|
try {
|
|
// 조건을 SQL WHERE 절로 변환
|
|
const whereClause = this.buildWhereClause(conditions, data);
|
|
console.log(`🔍 [전체 실행 조건] 생성된 WHERE 절:`, whereClause);
|
|
|
|
// 전체 실행 조건 평가 (폼 데이터 기반)
|
|
for (const condition of conditions) {
|
|
if (
|
|
condition.type === "condition" &&
|
|
condition.field &&
|
|
condition.operator
|
|
) {
|
|
const fieldValue = data[condition.field];
|
|
const conditionValue = condition.value;
|
|
|
|
console.log(
|
|
`🔍 [전체 실행 조건] 조건 평가: ${condition.field} ${condition.operator} ${conditionValue} (폼 데이터 실제값: ${fieldValue})`
|
|
);
|
|
|
|
const result = this.evaluateSingleCondition(
|
|
fieldValue,
|
|
condition.operator,
|
|
conditionValue,
|
|
condition.dataType || "string"
|
|
);
|
|
|
|
if (!result) {
|
|
return {
|
|
satisfied: false,
|
|
reason: `[전체 실행 조건] 조건 미충족: ${condition.field} ${condition.operator} ${conditionValue} (폼 데이터 기준)`,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return { satisfied: true };
|
|
} catch (error) {
|
|
console.error("조건 평가 오류:", error);
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : String(error);
|
|
return {
|
|
satisfied: false,
|
|
reason: `조건 평가 오류: ${errorMessage}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 단일 조건 평가
|
|
*/
|
|
private evaluateSingleCondition(
|
|
fieldValue: any,
|
|
operator: string,
|
|
conditionValue: any,
|
|
dataType: string
|
|
): boolean {
|
|
// 타입 변환
|
|
let actualValue = fieldValue;
|
|
let expectedValue = conditionValue;
|
|
|
|
if (dataType === "number") {
|
|
actualValue = parseFloat(fieldValue) || 0;
|
|
expectedValue = parseFloat(conditionValue) || 0;
|
|
} else if (dataType === "string") {
|
|
actualValue = String(fieldValue || "");
|
|
expectedValue = String(conditionValue || "");
|
|
}
|
|
|
|
// 연산자별 평가
|
|
switch (operator) {
|
|
case "=":
|
|
return actualValue === expectedValue;
|
|
case "!=":
|
|
return actualValue !== expectedValue;
|
|
case ">":
|
|
return actualValue > expectedValue;
|
|
case "<":
|
|
return actualValue < expectedValue;
|
|
case ">=":
|
|
return actualValue >= expectedValue;
|
|
case "<=":
|
|
return actualValue <= expectedValue;
|
|
case "LIKE":
|
|
return String(actualValue).includes(String(expectedValue));
|
|
default:
|
|
console.warn(`지원되지 않는 연산자: ${operator}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* WHERE 절 생성 (복잡한 그룹 조건 처리)
|
|
*/
|
|
private buildWhereClause(
|
|
conditions: ControlCondition[],
|
|
data: Record<string, any>
|
|
): string {
|
|
// 실제로는 더 복잡한 그룹 처리 로직이 필요
|
|
// 현재는 간단한 AND/OR 처리만 구현
|
|
const clauses = [];
|
|
|
|
for (const condition of conditions) {
|
|
if (condition.type === "condition") {
|
|
const clause = `${condition.field} ${condition.operator} '${condition.value}'`;
|
|
clauses.push(clause);
|
|
}
|
|
}
|
|
|
|
return clauses.join(" AND ");
|
|
}
|
|
|
|
/**
|
|
* 액션 실행
|
|
*/
|
|
private async executeAction(
|
|
action: ControlAction,
|
|
sourceData: Record<string, any>
|
|
): Promise<any> {
|
|
console.log(`🚀 액션 실행: ${action.actionType}`, action);
|
|
|
|
switch (action.actionType) {
|
|
case "insert":
|
|
return await this.executeInsertAction(action, sourceData);
|
|
case "update":
|
|
return await this.executeUpdateAction(action, sourceData);
|
|
case "delete":
|
|
return await this.executeDeleteAction(action, sourceData);
|
|
default:
|
|
throw new Error(`지원되지 않는 액션 타입: ${action.actionType}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* INSERT 액션 실행
|
|
*/
|
|
private async executeInsertAction(
|
|
action: ControlAction,
|
|
sourceData: Record<string, any>
|
|
): Promise<any> {
|
|
const results = [];
|
|
|
|
for (const mapping of action.fieldMappings) {
|
|
const { targetTable, targetField, defaultValue, sourceField } = mapping;
|
|
|
|
// 삽입할 데이터 준비
|
|
const insertData: Record<string, any> = {};
|
|
|
|
if (sourceField && sourceData[sourceField]) {
|
|
insertData[targetField] = sourceData[sourceField];
|
|
} else if (defaultValue !== undefined) {
|
|
insertData[targetField] = defaultValue;
|
|
}
|
|
|
|
// 기본 필드 추가
|
|
insertData.created_at = new Date();
|
|
insertData.updated_at = new Date();
|
|
|
|
console.log(`📝 INSERT 실행: ${targetTable}.${targetField}`, insertData);
|
|
|
|
try {
|
|
// 동적 테이블 INSERT 실행
|
|
const placeholders = Object.keys(insertData)
|
|
.map((_, i) => `$${i + 1}`)
|
|
.join(", ");
|
|
|
|
const result = await query(
|
|
`INSERT INTO ${targetTable} (${Object.keys(insertData).join(", ")})
|
|
VALUES (${placeholders})`,
|
|
Object.values(insertData)
|
|
);
|
|
|
|
results.push({
|
|
table: targetTable,
|
|
field: targetField,
|
|
data: insertData,
|
|
result,
|
|
});
|
|
|
|
console.log(`✅ INSERT 성공: ${targetTable}.${targetField}`);
|
|
} catch (error) {
|
|
console.error(`❌ INSERT 실패: ${targetTable}.${targetField}`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* UPDATE 액션 실행
|
|
*/
|
|
private async executeUpdateAction(
|
|
action: ControlAction,
|
|
sourceData: Record<string, any>
|
|
): Promise<any> {
|
|
console.log(`🔄 UPDATE 액션 실행: ${action.name}`);
|
|
console.log(`📋 액션 정보:`, JSON.stringify(action, null, 2));
|
|
console.log(`📋 소스 데이터:`, JSON.stringify(sourceData, null, 2));
|
|
|
|
// fieldMappings에서 대상 테이블과 필드 정보 추출
|
|
if (!action.fieldMappings || action.fieldMappings.length === 0) {
|
|
console.error("❌ fieldMappings가 없습니다:", action);
|
|
throw new Error("UPDATE 액션에는 fieldMappings가 필요합니다.");
|
|
}
|
|
|
|
console.log(`🎯 처리할 매핑 개수: ${action.fieldMappings.length}`);
|
|
|
|
const results = [];
|
|
|
|
// 각 필드 매핑별로 개별 UPDATE 실행
|
|
for (let i = 0; i < action.fieldMappings.length; i++) {
|
|
const mapping = action.fieldMappings[i];
|
|
const targetTable = mapping.targetTable;
|
|
const targetField = mapping.targetField;
|
|
const updateValue =
|
|
mapping.defaultValue ||
|
|
(mapping.sourceField ? sourceData[mapping.sourceField] : null);
|
|
|
|
console.log(`🎯 매핑 ${i + 1}/${action.fieldMappings.length}:`, {
|
|
targetTable,
|
|
targetField,
|
|
updateValue,
|
|
defaultValue: mapping.defaultValue,
|
|
sourceField: mapping.sourceField,
|
|
});
|
|
|
|
if (!targetTable || !targetField) {
|
|
console.error("❌ 필수 필드가 없습니다:", { targetTable, targetField });
|
|
continue; // 다음 매핑으로 계속
|
|
}
|
|
|
|
try {
|
|
// WHERE 조건 구성
|
|
let whereClause = "";
|
|
const whereValues: any[] = [];
|
|
|
|
// action.conditions에서 WHERE 조건 생성 (PostgreSQL 형식)
|
|
let conditionParamIndex = 2; // $1은 SET 값용, $2부터 WHERE 조건용
|
|
|
|
if (action.conditions && Array.isArray(action.conditions)) {
|
|
const conditions = action.conditions
|
|
.filter((cond) => cond.field && cond.value !== undefined)
|
|
.map((cond) => `${cond.field} = $${conditionParamIndex++}`);
|
|
|
|
if (conditions.length > 0) {
|
|
whereClause = conditions.join(" AND ");
|
|
whereValues.push(
|
|
...action.conditions
|
|
.filter((cond) => cond.field && cond.value !== undefined)
|
|
.map((cond) => cond.value)
|
|
);
|
|
}
|
|
}
|
|
|
|
// WHERE 조건이 없으면 기본 조건 사용 (같은 필드로 찾기)
|
|
if (!whereClause) {
|
|
whereClause = `${targetField} = $${conditionParamIndex}`;
|
|
whereValues.push("김철수"); // 기존 값으로 찾기
|
|
}
|
|
|
|
console.log(
|
|
`📝 UPDATE 쿼리 준비 (${i + 1}/${action.fieldMappings.length}):`,
|
|
{
|
|
targetTable,
|
|
targetField,
|
|
updateValue,
|
|
whereClause,
|
|
whereValues,
|
|
}
|
|
);
|
|
|
|
// 동적 테이블 UPDATE 실행 (PostgreSQL 형식)
|
|
const updateQuery = `UPDATE ${targetTable} SET ${targetField} = $1 WHERE ${whereClause}`;
|
|
const allValues = [updateValue, ...whereValues];
|
|
|
|
console.log(
|
|
`🚀 실행할 쿼리 (${i + 1}/${action.fieldMappings.length}):`,
|
|
updateQuery
|
|
);
|
|
console.log(`📊 쿼리 파라미터:`, allValues);
|
|
|
|
const result = await query(updateQuery, allValues);
|
|
|
|
console.log(
|
|
`✅ UPDATE 성공 (${i + 1}/${action.fieldMappings.length}):`,
|
|
{
|
|
table: targetTable,
|
|
field: targetField,
|
|
value: updateValue,
|
|
affectedRows: result,
|
|
}
|
|
);
|
|
|
|
results.push({
|
|
message: `UPDATE 성공: ${targetTable}.${targetField} = ${updateValue}`,
|
|
affectedRows: result,
|
|
targetTable,
|
|
targetField,
|
|
updateValue,
|
|
});
|
|
} catch (error) {
|
|
console.error(
|
|
`❌ UPDATE 실패 (${i + 1}/${action.fieldMappings.length}):`,
|
|
{
|
|
table: targetTable,
|
|
field: targetField,
|
|
value: updateValue,
|
|
error: error,
|
|
}
|
|
);
|
|
|
|
// 에러가 발생해도 다음 매핑은 계속 처리
|
|
results.push({
|
|
message: `UPDATE 실패: ${targetTable}.${targetField} = ${updateValue}`,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
targetTable,
|
|
targetField,
|
|
updateValue,
|
|
});
|
|
}
|
|
}
|
|
|
|
// 전체 결과 반환
|
|
const successCount = results.filter((r) => !r.error).length;
|
|
const totalCount = results.length;
|
|
|
|
console.log(`🎯 전체 UPDATE 결과: ${successCount}/${totalCount} 성공`);
|
|
|
|
return {
|
|
message: `UPDATE 완료: ${successCount}/${totalCount} 성공`,
|
|
results,
|
|
successCount,
|
|
totalCount,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* DELETE 액션 실행 - 보안상 외부 DB 비활성화
|
|
*/
|
|
private async executeDeleteAction(
|
|
action: ControlAction,
|
|
sourceData: Record<string, any>
|
|
): Promise<any> {
|
|
// 보안상 외부 DB에 대한 DELETE 작업은 비활성화
|
|
throw new Error(
|
|
"보안상 외부 데이터베이스에 대한 DELETE 작업은 허용되지 않습니다. SELECT 쿼리만 사용해주세요."
|
|
);
|
|
|
|
const results = [];
|
|
|
|
// 조건에서 테이블별로 그룹화하여 삭제 실행
|
|
const tableGroups = new Map<string, any[]>();
|
|
|
|
for (const condition of action.conditions) {
|
|
if (
|
|
condition.type === "condition" &&
|
|
condition.field &&
|
|
condition.value !== undefined
|
|
) {
|
|
// 조건에서 테이블명을 추출 (테이블명.필드명 형식이거나 기본 소스 테이블)
|
|
const parts = condition.field!.split(".");
|
|
let tableName: string;
|
|
let fieldName: string;
|
|
|
|
if (parts.length === 2) {
|
|
// "테이블명.필드명" 형식
|
|
tableName = parts[0];
|
|
fieldName = parts[1];
|
|
} else {
|
|
// 필드명만 있는 경우, 조건에 명시된 테이블 또는 소스 테이블 사용
|
|
// fieldMappings이 있다면 targetTable 사용, 없다면 에러
|
|
if (action.fieldMappings && action.fieldMappings.length > 0) {
|
|
tableName = action.fieldMappings[0].targetTable;
|
|
} else {
|
|
throw new Error(
|
|
`DELETE 조건에서 테이블을 결정할 수 없습니다. 필드를 "테이블명.필드명" 형식으로 지정하거나 fieldMappings에 targetTable을 설정하세요.`
|
|
);
|
|
}
|
|
fieldName = condition.field!;
|
|
}
|
|
|
|
if (!tableGroups.has(tableName)) {
|
|
tableGroups.set(tableName, []);
|
|
}
|
|
|
|
tableGroups.get(tableName)!.push({
|
|
field: fieldName,
|
|
value: condition.value,
|
|
operator: condition.operator || "=",
|
|
});
|
|
}
|
|
}
|
|
|
|
if (tableGroups.size === 0) {
|
|
throw new Error("DELETE 액션에서 유효한 조건을 찾을 수 없습니다.");
|
|
}
|
|
|
|
console.log(
|
|
`🎯 삭제 대상 테이블: ${Array.from(tableGroups.keys()).join(", ")}`
|
|
);
|
|
|
|
// 각 테이블별로 DELETE 실행
|
|
for (const [tableName, conditions] of tableGroups) {
|
|
try {
|
|
console.log(`🗑️ ${tableName} 테이블에서 삭제 실행:`, conditions);
|
|
|
|
// WHERE 조건 구성
|
|
let conditionParamIndex = 1;
|
|
const whereConditions = conditions.map(
|
|
(cond) => `${cond.field} ${cond.operator} $${conditionParamIndex++}`
|
|
);
|
|
const whereClause = whereConditions.join(" AND ");
|
|
const whereValues = conditions.map((cond) => cond.value);
|
|
|
|
console.log(`📝 DELETE 쿼리 준비:`, {
|
|
tableName,
|
|
whereClause,
|
|
whereValues,
|
|
});
|
|
|
|
// 동적 테이블 DELETE 실행 (PostgreSQL 형식)
|
|
const deleteQuery = `DELETE FROM ${tableName} WHERE ${whereClause}`;
|
|
|
|
console.log(`🚀 실행할 쿼리:`, deleteQuery);
|
|
console.log(`📊 쿼리 파라미터:`, whereValues);
|
|
|
|
const result = await query(deleteQuery, whereValues);
|
|
|
|
console.log(`✅ DELETE 성공:`, {
|
|
table: tableName,
|
|
affectedRows: result,
|
|
whereClause,
|
|
});
|
|
|
|
results.push({
|
|
message: `DELETE 성공: ${tableName}에서 ${result}개 행 삭제`,
|
|
affectedRows: result,
|
|
targetTable: tableName,
|
|
whereClause,
|
|
});
|
|
} catch (error: unknown) {
|
|
console.error(`❌ DELETE 실패:`, {
|
|
table: tableName,
|
|
error: error,
|
|
});
|
|
|
|
const userFriendlyMessage =
|
|
error instanceof Error ? (error as Error).message : String(error);
|
|
|
|
results.push({
|
|
message: `DELETE 실패: ${tableName}`,
|
|
error: userFriendlyMessage,
|
|
targetTable: tableName,
|
|
});
|
|
}
|
|
}
|
|
|
|
// 전체 결과 반환
|
|
const successCount = results.filter((r) => !r.error).length;
|
|
const totalCount = results.length;
|
|
|
|
console.log(`🎯 전체 DELETE 결과: ${successCount}/${totalCount} 성공`);
|
|
|
|
return {
|
|
message: `DELETE 완료: ${successCount}/${totalCount} 성공`,
|
|
results,
|
|
successCount,
|
|
totalCount,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 테이블에 특정 컬럼이 존재하는지 확인
|
|
*/
|
|
private async checkColumnExists(
|
|
tableName: string,
|
|
columnName: string
|
|
): Promise<boolean> {
|
|
try {
|
|
const result = await query<{ exists: boolean }>(
|
|
`SELECT EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_name = $1
|
|
AND column_name = $2
|
|
AND table_schema = 'public'
|
|
) as exists`,
|
|
[tableName, columnName]
|
|
);
|
|
|
|
return result[0]?.exists || false;
|
|
} catch (error) {
|
|
console.error(
|
|
`❌ 컬럼 존재 여부 확인 오류: ${tableName}.${columnName}`,
|
|
error
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
}
|