diff --git a/backend-node/src/services/dataflowControlService.ts b/backend-node/src/services/dataflowControlService.ts index a04e5eee..d706935f 100644 --- a/backend-node/src/services/dataflowControlService.ts +++ b/backend-node/src/services/dataflowControlService.ts @@ -19,6 +19,7 @@ export interface ControlAction { id: string; name: string; actionType: "insert" | "update" | "delete"; + logicalOperator?: "AND" | "OR"; // 액션 간 논리 연산자 (첫 번째 액션 제외) conditions: ControlCondition[]; fieldMappings: { sourceField?: string; @@ -136,17 +137,41 @@ export class DataflowControlService { }; } - // 액션 실행 + // 액션 실행 (논리 연산자 지원) const executedActions = []; const errors = []; + let previousActionSuccess = false; + let shouldSkipRemainingActions = false; + + for (let i = 0; i < targetPlan.actions.length; i++) { + const action = targetPlan.actions[i]; - for (const action of targetPlan.actions) { 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, }); @@ -163,6 +188,10 @@ export class DataflowControlService { console.log( `⚠️ 액션 조건 미충족: ${actionConditionResult.reason}` ); + previousActionSuccess = false; + if (action.logicalOperator === "AND") { + shouldSkipRemainingActions = true; + } continue; } } @@ -173,11 +202,19 @@ export class DataflowControlService { actionName: action.name, result: actionResult, }); + + previousActionSuccess = true; + shouldSkipRemainingActions = 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; + } } } diff --git a/frontend/components/dataflow/DataFlowDesigner.tsx b/frontend/components/dataflow/DataFlowDesigner.tsx index daf2b454..d1eb0003 100644 --- a/frontend/components/dataflow/DataFlowDesigner.tsx +++ b/frontend/components/dataflow/DataFlowDesigner.tsx @@ -669,6 +669,7 @@ export const DataFlowDesigner: React.FC = ({ id: action.id as string, name: action.name as string, actionType: action.actionType as "insert" | "update" | "delete" | "upsert", + logicalOperator: action.logicalOperator as "AND" | "OR" | undefined, // 논리 연산자 추가 fieldMappings: ((action.fieldMappings as Record[]) || []).map( (mapping: Record) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/frontend/components/dataflow/connection/DataSaveSettings.tsx b/frontend/components/dataflow/connection/DataSaveSettings.tsx index 8fd3937e..6b867c99 100644 --- a/frontend/components/dataflow/connection/DataSaveSettings.tsx +++ b/frontend/components/dataflow/connection/DataSaveSettings.tsx @@ -38,6 +38,8 @@ export const DataSaveSettings: React.FC = ({ id: `action_${settings.actions.length + 1}`, name: `액션 ${settings.actions.length + 1}`, actionType: "insert" as const, + // 첫 번째 액션이 아니면 기본적으로 AND 연산자 추가 + ...(settings.actions.length > 0 && { logicalOperator: "AND" as const }), fieldMappings: [], conditions: [], splitConfig: { @@ -60,6 +62,12 @@ export const DataSaveSettings: React.FC = ({ const removeAction = (actionIndex: number) => { const newActions = settings.actions.filter((_, i) => i !== actionIndex); + + // 첫 번째 액션을 삭제했다면, 새로운 첫 번째 액션의 logicalOperator 제거 + if (actionIndex === 0 && newActions.length > 0) { + delete newActions[0].logicalOperator; + } + onSettingsChange({ ...settings, actions: newActions }); }; @@ -87,104 +95,132 @@ export const DataSaveSettings: React.FC = ({ ) : (
{settings.actions.map((action, actionIndex) => ( -
-
- updateAction(actionIndex, "name", e.target.value)} - className="h-7 flex-1 text-xs font-medium" - placeholder="액션 이름" - /> - -
- -
- {/* 액션 타입 */} -
- - +
+ {/* 첫 번째 액션이 아닌 경우 논리 연산자 표시 */} + {actionIndex > 0 && ( +
+
+ 이전 액션과의 관계: + +
-
- - {/* 액션별 개별 실행 조건 */} - - - {/* 데이터 분할 설정 - DELETE 액션은 제외 */} - {action.actionType !== "delete" && ( - )} - {/* 필드 매핑 - DELETE 액션은 제외 */} - {action.actionType !== "delete" && ( - +
+ updateAction(actionIndex, "name", e.target.value)} + className="h-7 flex-1 text-xs font-medium" + placeholder="액션 이름" + /> + +
+ +
+ {/* 액션 타입 */} +
+ + +
+
+ + {/* 액션별 개별 실행 조건 */} + - )} - {/* DELETE 액션일 때 안내 메시지 */} - {action.actionType === "delete" && ( -
-
-
- ℹ️ -
-
DELETE 액션 정보
-
- DELETE 액션은 실행조건만 필요합니다. -
- • 데이터 분할 설정: 불필요 (삭제 작업에는 분할이 의미 없음) -
- • 필드 매핑: 불필요 (조건에 맞는 데이터를 통째로 삭제) -
- 위에서 설정한 실행조건에 맞는 모든 데이터가 삭제됩니다. + {/* 데이터 분할 설정 - DELETE 액션은 제외 */} + {action.actionType !== "delete" && ( + + )} + + {/* 필드 매핑 - DELETE 액션은 제외 */} + {action.actionType !== "delete" && ( + + )} + + {/* DELETE 액션일 때 안내 메시지 */} + {action.actionType === "delete" && ( +
+
+
+ ℹ️ +
+
DELETE 액션 정보
+
+ DELETE 액션은 실행조건만 필요합니다. +
+ • 데이터 분할 설정: 불필요 (삭제 작업에는 분할이 의미 없음) +
+ • 필드 매핑: 불필요 (조건에 맞는 데이터를 통째로 삭제) +
+ 위에서 설정한 실행조건에 맞는 모든 데이터가 삭제됩니다. +
-
- )} + )} +
))}
diff --git a/frontend/types/connectionTypes.ts b/frontend/types/connectionTypes.ts index 237a5435..18c36cde 100644 --- a/frontend/types/connectionTypes.ts +++ b/frontend/types/connectionTypes.ts @@ -48,6 +48,7 @@ export interface DataSaveSettings { id: string; name: string; actionType: "insert" | "update" | "delete" | "upsert"; + logicalOperator?: "AND" | "OR"; // 액션 간 논리 연산자 (첫 번째 액션 제외) conditions?: ConditionNode[]; fieldMappings: Array<{ sourceTable?: string;