제어 관리 저장 액션에 논리연산자 추가
This commit is contained in:
@@ -19,6 +19,7 @@ export interface ControlAction {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
actionType: "insert" | "update" | "delete";
|
actionType: "insert" | "update" | "delete";
|
||||||
|
logicalOperator?: "AND" | "OR"; // 액션 간 논리 연산자 (첫 번째 액션 제외)
|
||||||
conditions: ControlCondition[];
|
conditions: ControlCondition[];
|
||||||
fieldMappings: {
|
fieldMappings: {
|
||||||
sourceField?: string;
|
sourceField?: string;
|
||||||
@@ -136,17 +137,41 @@ export class DataflowControlService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 액션 실행
|
// 액션 실행 (논리 연산자 지원)
|
||||||
const executedActions = [];
|
const executedActions = [];
|
||||||
const errors = [];
|
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 {
|
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(`⚡ 액션 실행: ${action.name} (${action.actionType})`);
|
||||||
console.log(`📋 액션 상세 정보:`, {
|
console.log(`📋 액션 상세 정보:`, {
|
||||||
actionId: action.id,
|
actionId: action.id,
|
||||||
actionName: action.name,
|
actionName: action.name,
|
||||||
actionType: action.actionType,
|
actionType: action.actionType,
|
||||||
|
logicalOperator: action.logicalOperator,
|
||||||
conditions: action.conditions,
|
conditions: action.conditions,
|
||||||
fieldMappings: action.fieldMappings,
|
fieldMappings: action.fieldMappings,
|
||||||
});
|
});
|
||||||
@@ -163,6 +188,10 @@ export class DataflowControlService {
|
|||||||
console.log(
|
console.log(
|
||||||
`⚠️ 액션 조건 미충족: ${actionConditionResult.reason}`
|
`⚠️ 액션 조건 미충족: ${actionConditionResult.reason}`
|
||||||
);
|
);
|
||||||
|
previousActionSuccess = false;
|
||||||
|
if (action.logicalOperator === "AND") {
|
||||||
|
shouldSkipRemainingActions = true;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,11 +202,19 @@ export class DataflowControlService {
|
|||||||
actionName: action.name,
|
actionName: action.name,
|
||||||
result: actionResult,
|
result: actionResult,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
previousActionSuccess = true;
|
||||||
|
shouldSkipRemainingActions = false; // 성공했으므로 다시 실행 가능
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ 액션 실행 오류: ${action.name}`, error);
|
console.error(`❌ 액션 실행 오류: ${action.name}`, error);
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : String(error);
|
error instanceof Error ? error.message : String(error);
|
||||||
errors.push(`액션 '${action.name}' 실행 오류: ${errorMessage}`);
|
errors.push(`액션 '${action.name}' 실행 오류: ${errorMessage}`);
|
||||||
|
|
||||||
|
previousActionSuccess = false;
|
||||||
|
if (action.logicalOperator === "AND") {
|
||||||
|
shouldSkipRemainingActions = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -669,6 +669,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||||||
id: action.id as string,
|
id: action.id as string,
|
||||||
name: action.name as string,
|
name: action.name as string,
|
||||||
actionType: action.actionType as "insert" | "update" | "delete" | "upsert",
|
actionType: action.actionType as "insert" | "update" | "delete" | "upsert",
|
||||||
|
logicalOperator: action.logicalOperator as "AND" | "OR" | undefined, // 논리 연산자 추가
|
||||||
fieldMappings: ((action.fieldMappings as Record<string, unknown>[]) || []).map(
|
fieldMappings: ((action.fieldMappings as Record<string, unknown>[]) || []).map(
|
||||||
(mapping: Record<string, unknown>) => {
|
(mapping: Record<string, unknown>) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export const DataSaveSettings: React.FC<DataSaveSettingsProps> = ({
|
|||||||
id: `action_${settings.actions.length + 1}`,
|
id: `action_${settings.actions.length + 1}`,
|
||||||
name: `액션 ${settings.actions.length + 1}`,
|
name: `액션 ${settings.actions.length + 1}`,
|
||||||
actionType: "insert" as const,
|
actionType: "insert" as const,
|
||||||
|
// 첫 번째 액션이 아니면 기본적으로 AND 연산자 추가
|
||||||
|
...(settings.actions.length > 0 && { logicalOperator: "AND" as const }),
|
||||||
fieldMappings: [],
|
fieldMappings: [],
|
||||||
conditions: [],
|
conditions: [],
|
||||||
splitConfig: {
|
splitConfig: {
|
||||||
@@ -60,6 +62,12 @@ export const DataSaveSettings: React.FC<DataSaveSettingsProps> = ({
|
|||||||
|
|
||||||
const removeAction = (actionIndex: number) => {
|
const removeAction = (actionIndex: number) => {
|
||||||
const newActions = settings.actions.filter((_, i) => i !== actionIndex);
|
const newActions = settings.actions.filter((_, i) => i !== actionIndex);
|
||||||
|
|
||||||
|
// 첫 번째 액션을 삭제했다면, 새로운 첫 번째 액션의 logicalOperator 제거
|
||||||
|
if (actionIndex === 0 && newActions.length > 0) {
|
||||||
|
delete newActions[0].logicalOperator;
|
||||||
|
}
|
||||||
|
|
||||||
onSettingsChange({ ...settings, actions: newActions });
|
onSettingsChange({ ...settings, actions: newActions });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87,104 +95,132 @@ export const DataSaveSettings: React.FC<DataSaveSettingsProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{settings.actions.map((action, actionIndex) => (
|
{settings.actions.map((action, actionIndex) => (
|
||||||
<div key={action.id} className="rounded border bg-white p-3">
|
<div key={action.id}>
|
||||||
<div className="mb-3 flex items-center justify-between">
|
{/* 첫 번째 액션이 아닌 경우 논리 연산자 표시 */}
|
||||||
<Input
|
{actionIndex > 0 && (
|
||||||
value={action.name}
|
<div className="mb-2 flex items-center justify-center">
|
||||||
onChange={(e) => updateAction(actionIndex, "name", e.target.value)}
|
<div className="flex items-center gap-2 rounded-lg bg-gray-100 px-3 py-1">
|
||||||
className="h-7 flex-1 text-xs font-medium"
|
<span className="text-xs text-gray-600">이전 액션과의 관계:</span>
|
||||||
placeholder="액션 이름"
|
<Select
|
||||||
/>
|
value={action.logicalOperator || "AND"}
|
||||||
<Button size="sm" variant="ghost" onClick={() => removeAction(actionIndex)} className="h-7 w-7 p-0">
|
onValueChange={(value: "AND" | "OR") => updateAction(actionIndex, "logicalOperator", value)}
|
||||||
<Trash2 className="h-3 w-3" />
|
>
|
||||||
</Button>
|
<SelectTrigger className="h-8 w-20 text-sm">
|
||||||
</div>
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<SelectContent>
|
||||||
{/* 액션 타입 */}
|
<SelectItem value="AND">AND</SelectItem>
|
||||||
<div>
|
<SelectItem value="OR">OR</SelectItem>
|
||||||
<Label className="text-xs">액션 타입</Label>
|
</SelectContent>
|
||||||
<Select
|
</Select>
|
||||||
value={action.actionType}
|
</div>
|
||||||
onValueChange={(value: "insert" | "update" | "delete" | "upsert") =>
|
|
||||||
updateAction(actionIndex, "actionType", value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-7 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="insert">INSERT</SelectItem>
|
|
||||||
<SelectItem value="update">UPDATE</SelectItem>
|
|
||||||
<SelectItem value="delete">DELETE</SelectItem>
|
|
||||||
<SelectItem value="upsert">UPSERT</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 액션별 개별 실행 조건 */}
|
|
||||||
<ActionConditionsSection
|
|
||||||
action={action}
|
|
||||||
actionIndex={actionIndex}
|
|
||||||
settings={settings}
|
|
||||||
onSettingsChange={onSettingsChange}
|
|
||||||
fromTableColumns={fromTableColumns}
|
|
||||||
toTableColumns={toTableColumns}
|
|
||||||
fromTableName={fromTableName}
|
|
||||||
toTableName={toTableName}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 데이터 분할 설정 - DELETE 액션은 제외 */}
|
|
||||||
{action.actionType !== "delete" && (
|
|
||||||
<ActionSplitConfig
|
|
||||||
action={action}
|
|
||||||
actionIndex={actionIndex}
|
|
||||||
settings={settings}
|
|
||||||
onSettingsChange={onSettingsChange}
|
|
||||||
fromTableColumns={fromTableColumns}
|
|
||||||
toTableColumns={toTableColumns}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 필드 매핑 - DELETE 액션은 제외 */}
|
<div className="rounded border bg-white p-3">
|
||||||
{action.actionType !== "delete" && (
|
<div className="mb-3 flex items-center justify-between">
|
||||||
<ActionFieldMappings
|
<Input
|
||||||
|
value={action.name}
|
||||||
|
onChange={(e) => updateAction(actionIndex, "name", e.target.value)}
|
||||||
|
className="h-7 flex-1 text-xs font-medium"
|
||||||
|
placeholder="액션 이름"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => removeAction(actionIndex)}
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-3">
|
||||||
|
{/* 액션 타입 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">액션 타입</Label>
|
||||||
|
<Select
|
||||||
|
value={action.actionType}
|
||||||
|
onValueChange={(value: "insert" | "update" | "delete" | "upsert") =>
|
||||||
|
updateAction(actionIndex, "actionType", value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-7 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="insert">INSERT</SelectItem>
|
||||||
|
<SelectItem value="update">UPDATE</SelectItem>
|
||||||
|
<SelectItem value="delete">DELETE</SelectItem>
|
||||||
|
<SelectItem value="upsert">UPSERT</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 액션별 개별 실행 조건 */}
|
||||||
|
<ActionConditionsSection
|
||||||
action={action}
|
action={action}
|
||||||
actionIndex={actionIndex}
|
actionIndex={actionIndex}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
onSettingsChange={onSettingsChange}
|
onSettingsChange={onSettingsChange}
|
||||||
availableTables={availableTables}
|
|
||||||
tableColumnsCache={tableColumnsCache}
|
|
||||||
fromTableColumns={fromTableColumns}
|
fromTableColumns={fromTableColumns}
|
||||||
toTableColumns={toTableColumns}
|
toTableColumns={toTableColumns}
|
||||||
fromTableName={fromTableName}
|
fromTableName={fromTableName}
|
||||||
toTableName={toTableName}
|
toTableName={toTableName}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* DELETE 액션일 때 안내 메시지 */}
|
{/* 데이터 분할 설정 - DELETE 액션은 제외 */}
|
||||||
{action.actionType === "delete" && (
|
{action.actionType !== "delete" && (
|
||||||
<div className="mt-3">
|
<ActionSplitConfig
|
||||||
<div className="rounded border border-blue-200 bg-blue-50 p-3 text-xs text-blue-700">
|
action={action}
|
||||||
<div className="flex items-start gap-2">
|
actionIndex={actionIndex}
|
||||||
<span>ℹ️</span>
|
settings={settings}
|
||||||
<div>
|
onSettingsChange={onSettingsChange}
|
||||||
<div className="font-medium">DELETE 액션 정보</div>
|
fromTableColumns={fromTableColumns}
|
||||||
<div className="mt-1">
|
toTableColumns={toTableColumns}
|
||||||
DELETE 액션은 <strong>실행조건만</strong> 필요합니다.
|
/>
|
||||||
<br />
|
)}
|
||||||
• 데이터 분할 설정: 불필요 (삭제 작업에는 분할이 의미 없음)
|
|
||||||
<br />
|
{/* 필드 매핑 - DELETE 액션은 제외 */}
|
||||||
• 필드 매핑: 불필요 (조건에 맞는 데이터를 통째로 삭제)
|
{action.actionType !== "delete" && (
|
||||||
<br />
|
<ActionFieldMappings
|
||||||
위에서 설정한 실행조건에 맞는 모든 데이터가 삭제됩니다.
|
action={action}
|
||||||
|
actionIndex={actionIndex}
|
||||||
|
settings={settings}
|
||||||
|
onSettingsChange={onSettingsChange}
|
||||||
|
availableTables={availableTables}
|
||||||
|
tableColumnsCache={tableColumnsCache}
|
||||||
|
fromTableColumns={fromTableColumns}
|
||||||
|
toTableColumns={toTableColumns}
|
||||||
|
fromTableName={fromTableName}
|
||||||
|
toTableName={toTableName}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* DELETE 액션일 때 안내 메시지 */}
|
||||||
|
{action.actionType === "delete" && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<div className="rounded border border-blue-200 bg-blue-50 p-3 text-xs text-blue-700">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span>ℹ️</span>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">DELETE 액션 정보</div>
|
||||||
|
<div className="mt-1">
|
||||||
|
DELETE 액션은 <strong>실행조건만</strong> 필요합니다.
|
||||||
|
<br />
|
||||||
|
• 데이터 분할 설정: 불필요 (삭제 작업에는 분할이 의미 없음)
|
||||||
|
<br />
|
||||||
|
• 필드 매핑: 불필요 (조건에 맞는 데이터를 통째로 삭제)
|
||||||
|
<br />
|
||||||
|
위에서 설정한 실행조건에 맞는 모든 데이터가 삭제됩니다.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export interface DataSaveSettings {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
actionType: "insert" | "update" | "delete" | "upsert";
|
actionType: "insert" | "update" | "delete" | "upsert";
|
||||||
|
logicalOperator?: "AND" | "OR"; // 액션 간 논리 연산자 (첫 번째 액션 제외)
|
||||||
conditions?: ConditionNode[];
|
conditions?: ConditionNode[];
|
||||||
fieldMappings: Array<{
|
fieldMappings: Array<{
|
||||||
sourceTable?: string;
|
sourceTable?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user