From af08b673318bdbcbfe66a77af27c0d96050a348b Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Mon, 15 Sep 2025 10:11:22 +0900 Subject: [PATCH] =?UTF-8?q?=EB=85=BC=EB=A6=AC=EC=97=B0=EC=82=B0=EC=9E=90?= =?UTF-8?q?=20input=20=ED=81=AC=EA=B8=B0=20=EC=A1=B0=EC=A0=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/eventTriggerService.ts | 101 +- .../dataflow/ConnectionSetupModal.tsx | 1004 ++++++++++------- frontend/lib/api/dataflow.ts | 12 +- 3 files changed, 713 insertions(+), 404 deletions(-) diff --git a/backend-node/src/services/eventTriggerService.ts b/backend-node/src/services/eventTriggerService.ts index a332d57a..a5437b7d 100644 --- a/backend-node/src/services/eventTriggerService.ts +++ b/backend-node/src/services/eventTriggerService.ts @@ -35,7 +35,17 @@ interface TargetAction { targetTable: string; enabled: boolean; fieldMappings: FieldMapping[]; - conditions?: ConditionNode; + conditions?: Array<{ + field: string; + operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE"; + value: string; + logicalOperator?: "AND" | "OR"; + }>; + splitConfig?: { + sourceField: string; + delimiter: string; + targetField: string; + }; description?: string; } @@ -255,6 +265,77 @@ export class EventTriggerService { return false; } + /** + * 액션별 조건들 평가 (AND/OR 연산자 지원) + */ + private static async evaluateActionConditions( + conditions: Array<{ + field: string; + operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE"; + value: string; + logicalOperator?: "AND" | "OR"; + }>, + data: Record + ): Promise { + if (conditions.length === 0) { + return true; + } + + let result = await this.evaluateActionCondition(conditions[0], data); + + for (let i = 1; i < conditions.length; i++) { + const prevCondition = conditions[i - 1]; + const currentCondition = conditions[i]; + const currentResult = await this.evaluateActionCondition( + currentCondition, + data + ); + + if (prevCondition.logicalOperator === "OR") { + result = result || currentResult; + } else { + // 기본값은 AND + result = result && currentResult; + } + } + + return result; + } + + /** + * 액션 단일 조건 평가 + */ + private static async evaluateActionCondition( + condition: { + field: string; + operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE"; + value: string; + }, + data: Record + ): Promise { + const fieldValue = data[condition.field]; + const conditionValue = condition.value; + + switch (condition.operator) { + case "=": + return fieldValue == conditionValue; + case "!=": + return fieldValue != conditionValue; + case ">": + return Number(fieldValue) > Number(conditionValue); + case "<": + return Number(fieldValue) < Number(conditionValue); + case ">=": + return Number(fieldValue) >= Number(conditionValue); + case "<=": + return Number(fieldValue) <= Number(conditionValue); + case "LIKE": + return String(fieldValue).includes(String(conditionValue)); + default: + return false; + } + } + /** * 단일 조건 평가 */ @@ -298,6 +379,20 @@ export class EventTriggerService { sourceData: Record, companyCode: string ): Promise { + // 액션별 조건 평가 + if (action.conditions && action.conditions.length > 0) { + const conditionMet = await this.evaluateActionConditions( + action.conditions, + sourceData + ); + if (!conditionMet) { + logger.info( + `Action conditions not met for action ${action.id}, skipping execution` + ); + return; + } + } + // 필드 매핑을 통해 대상 데이터 생성 const targetData: Record = {}; @@ -329,14 +424,14 @@ export class EventTriggerService { await this.executeUpdateAction( action.targetTable, targetData, - action.conditions + null // 액션별 조건은 이미 평가했으므로 WHERE 조건은 null ); break; case "delete": await this.executeDeleteAction( action.targetTable, targetData, - action.conditions + null // 액션별 조건은 이미 평가했으므로 WHERE 조건은 null ); break; case "upsert": diff --git a/frontend/components/dataflow/ConnectionSetupModal.tsx b/frontend/components/dataflow/ConnectionSetupModal.tsx index 036688af..9609782d 100644 --- a/frontend/components/dataflow/ConnectionSetupModal.tsx +++ b/frontend/components/dataflow/ConnectionSetupModal.tsx @@ -58,7 +58,6 @@ interface SimpleKeySettings { // 데이터 저장 설정 interface DataSaveSettings { - saveMode: "simple" | "conditional" | "split"; // 저장 방식 actions: Array<{ id: string; name: string; @@ -67,6 +66,7 @@ interface DataSaveSettings { field: string; operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE"; value: string; + logicalOperator?: "AND" | "OR"; }>; fieldMappings: Array<{ sourceTable?: string; @@ -124,7 +124,6 @@ export const ConnectionSetupModal: React.FC = ({ }); const [dataSaveSettings, setDataSaveSettings] = useState({ - saveMode: "simple", actions: [], }); @@ -199,17 +198,9 @@ export const ConnectionSetupModal: React.FC = ({ notes: `${fromDisplayName}과 ${toDisplayName} 간의 키값 연결`, }); - // 데이터 저장 기본값 설정 + // 데이터 저장 기본값 설정 (빈 배열로 시작) setDataSaveSettings({ - saveMode: "simple", - actions: [ - { - id: "action_1", - name: `${fromDisplayName}에서 ${toDisplayName}로 데이터 저장`, - actionType: "insert", - fieldMappings: [], - }, - ], + actions: [], }); // 외부 호출 기본값 설정 @@ -508,72 +499,136 @@ export const ConnectionSetupModal: React.FC = ({ ) : ( conditions.map((condition, index) => ( -
- {/* 첫 번째 조건이 아닐 때 AND/OR 연산자를 위에 표시 */} +
+ {/* 첫 번째 조건이 아닐 때 AND/OR 연산자를 앞에 표시 */} {index > 0 && ( -
- -
- )} - - {/* 조건 행 */} -
- - + )} - updateCondition(index, "value", e.target.value)} - className="h-8 flex-1 text-xs" - /> + {/* 조건 필드들 */} + - -
+ + + {/* 데이터 타입에 따른 적절한 입력 컴포넌트 */} + {(() => { + const selectedColumn = fromTableColumns.find((col) => col.columnName === condition.field); + const dataType = selectedColumn?.dataType?.toLowerCase() || "string"; + + if (dataType.includes("timestamp") || dataType.includes("datetime") || dataType.includes("date")) { + return ( + updateCondition(index, "value", e.target.value)} + className="h-8 flex-1 text-xs" + /> + ); + } else if (dataType.includes("time")) { + return ( + updateCondition(index, "value", e.target.value)} + className="h-8 flex-1 text-xs" + /> + ); + } else if (dataType.includes("date")) { + return ( + updateCondition(index, "value", e.target.value)} + className="h-8 flex-1 text-xs" + /> + ); + } else if ( + dataType.includes("int") || + dataType.includes("numeric") || + dataType.includes("decimal") || + dataType.includes("float") || + dataType.includes("double") + ) { + return ( + updateCondition(index, "value", e.target.value)} + className="h-8 flex-1 text-xs" + /> + ); + } else if (dataType.includes("bool")) { + return ( + + ); + } else { + return ( + updateCondition(index, "value", e.target.value)} + className="h-8 flex-1 text-xs" + /> + ); + } + })()} + +
)) )} @@ -619,50 +674,6 @@ export const ConnectionSetupModal: React.FC = ({ 데이터 저장 설정
- {/* 저장 방식 선택 */} -
- - -
- - {/* 저장 방식별 설명 */} -
- {dataSaveSettings.saveMode === "simple" && ( -
- 단순 저장: From 테이블의 데이터를 To 테이블( - {selectedToTable || "선택된 테이블"})에 그대로 저장합니다. -
- )} - {dataSaveSettings.saveMode === "conditional" && ( -
- 조건부 저장: 조건에 따라 다른 테이블에 저장합니다. -
- 예: 평일 주문 → 당일배송, 주말 주문 → 월요일배송 -
- )} - {dataSaveSettings.saveMode === "split" && ( -
- 분할 저장: 하나의 필드를 분할하여 여러 레코드로 저장합니다. -
- 예: "컴퓨터,마우스,키보드" → 3개의 별도 레코드로 저장 -
- )} -
- {/* 액션 목록 */}
@@ -676,12 +687,12 @@ export const ConnectionSetupModal: React.FC = ({ name: `액션 ${dataSaveSettings.actions.length + 1}`, actionType: "insert" as const, fieldMappings: [], - ...(dataSaveSettings.saveMode === "conditional" ? { conditions: [] } : {}), - ...(dataSaveSettings.saveMode === "split" - ? { - splitConfig: { sourceField: "", delimiter: ",", targetField: "" }, - } - : {}), + conditions: [], + splitConfig: { + sourceField: "", + delimiter: ",", + targetField: "", + }, }; setDataSaveSettings({ ...dataSaveSettings, @@ -752,162 +763,368 @@ export const ConnectionSetupModal: React.FC = ({
- {/* 액션별 개별 실행 조건 (조건부 저장일 때만) */} - {dataSaveSettings.saveMode !== "simple" && ( -
-
- - -
- {action.conditions && action.conditions.length > 0 && ( -
- {action.conditions.map((condition, condIndex) => ( -
- - - { - const newActions = [...dataSaveSettings.actions]; - newActions[actionIndex].conditions![condIndex].value = e.target.value; - setDataSaveSettings({ ...dataSaveSettings, actions: newActions }); - }} - className="h-6 text-xs" - placeholder="값" - /> - -
- ))} -
- )} -
- )} - - {/* 분할 저장일 때 분할 설정 */} - {dataSaveSettings.saveMode === "split" && action.splitConfig && ( -
- -
-
- - -
-
- - { + + + )} + +
+
+
-
- - { + {action.conditions && action.conditions.length > 0 && ( +
+ {action.conditions.map((condition, condIndex) => ( +
+ {/* 첫 번째 조건이 아닐 때 AND/OR 연산자를 앞에 표시 */} + {condIndex > 0 && ( + + )} + + {/* 조건 필드들 */} + + + {/* 데이터 타입에 따른 적절한 입력 컴포넌트 */} + {(() => { + const selectedColumn = fromTableColumns.find( + (col) => col.columnName === condition.field, + ); + const dataType = selectedColumn?.dataType?.toLowerCase() || "string"; + + if ( + dataType.includes("timestamp") || + dataType.includes("datetime") || + dataType.includes("date") + ) { + return ( + { + const newActions = [...dataSaveSettings.actions]; + newActions[actionIndex].conditions![condIndex].value = e.target.value; + setDataSaveSettings({ ...dataSaveSettings, actions: newActions }); + }} + className="h-6 flex-1 text-xs" + /> + ); + } else if (dataType.includes("time")) { + return ( + { + const newActions = [...dataSaveSettings.actions]; + newActions[actionIndex].conditions![condIndex].value = e.target.value; + setDataSaveSettings({ ...dataSaveSettings, actions: newActions }); + }} + className="h-6 flex-1 text-xs" + /> + ); + } else if (dataType.includes("date")) { + return ( + { + const newActions = [...dataSaveSettings.actions]; + newActions[actionIndex].conditions![condIndex].value = e.target.value; + setDataSaveSettings({ ...dataSaveSettings, actions: newActions }); + }} + className="h-6 flex-1 text-xs" + /> + ); + } else if ( + dataType.includes("int") || + dataType.includes("numeric") || + dataType.includes("decimal") || + dataType.includes("float") || + dataType.includes("double") + ) { + return ( + { + const newActions = [...dataSaveSettings.actions]; + newActions[actionIndex].conditions![condIndex].value = e.target.value; + setDataSaveSettings({ ...dataSaveSettings, actions: newActions }); + }} + className="h-6 flex-1 text-xs" + /> + ); + } else if (dataType.includes("bool")) { + return ( + + ); + } else { + return ( + { + const newActions = [...dataSaveSettings.actions]; + newActions[actionIndex].conditions![condIndex].value = e.target.value; + setDataSaveSettings({ ...dataSaveSettings, actions: newActions }); + }} + className="h-6 flex-1 text-xs" + /> + ); + } + })()} + +
+ ))} +
+ )} +
+ +
+ + {/* 데이터 분할 설정 (선택사항) */} +
+
+ +
+ ✂️ 데이터 분할 설정 (선택사항) + {action.splitConfig && action.splitConfig.sourceField && ( + + 설정됨 + + )} +
+ {action.splitConfig && action.splitConfig.sourceField && ( + + )} +
+
+ +
+
+ + +
+
+ + { + const newActions = [...dataSaveSettings.actions]; + if (!newActions[actionIndex].splitConfig) { + newActions[actionIndex].splitConfig = { + sourceField: "", + delimiter: ",", + targetField: "", + }; + } + newActions[actionIndex].splitConfig!.delimiter = e.target.value; + setDataSaveSettings({ ...dataSaveSettings, actions: newActions }); + }} + className="h-6 text-xs" + placeholder="," + /> +
+
+ + +
-
- )} + +
{/* 필드 매핑 */}
@@ -936,151 +1153,138 @@ export const ConnectionSetupModal: React.FC = ({
{action.fieldMappings.map((mapping, mappingIndex) => ( -
- {/* 필드 매핑 영역 */} -
-
- {/* 소스 테이블 */} -
- - -
- - {/* 소스 컬럼 */} -
- - -
- - {/* 화살표 */} -
-
-
- - {/* 타겟 테이블 */} -
- - -
- - {/* 타겟 컬럼 */} -
- - -
-
-
- - {/* 기본값 및 삭제 버튼 */} -
-
- - { +
+ {/* 컴팩트한 매핑 표시 */} +
+ {/* 소스 */} +
+ + . +
+ +
+ + {/* 타겟 */} +
+ + . + +
+ + {/* 기본값 (인라인) */} + { + const newActions = [...dataSaveSettings.actions]; + newActions[actionIndex].fieldMappings[mappingIndex].defaultValue = e.target.value; + setDataSaveSettings({ ...dataSaveSettings, actions: newActions }); + }} + className="h-6 w-20 text-xs" + placeholder="기본값" + /> + + {/* 삭제 버튼 */} +
))} diff --git a/frontend/lib/api/dataflow.ts b/frontend/lib/api/dataflow.ts index 8e99db36..cb3e739f 100644 --- a/frontend/lib/api/dataflow.ts +++ b/frontend/lib/api/dataflow.ts @@ -36,7 +36,17 @@ export interface TargetAction { targetTable: string; enabled: boolean; fieldMappings: FieldMapping[]; - conditions?: ConditionNode; + conditions?: Array<{ + field: string; + operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE"; + value: string; + logicalOperator?: "AND" | "OR"; // 다음 조건과의 논리 연산자 + }>; + splitConfig?: { + sourceField: string; + delimiter: string; + targetField: string; + }; description?: string; }