From e308fd0cccde34e92c4915fe6291ef99bbf2400d Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 7 Jan 2026 09:55:19 +0900 Subject: [PATCH] =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=EC=97=90=20?= =?UTF-8?q?=EC=A1=B4=EC=9E=AC=ED=95=98=EB=8A=94=EC=A7=80=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=ED=95=98=EB=8A=94=20=EC=A0=9C=EC=96=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/nodeFlowExecutionService.ts | 163 +++++- .../node-editor/nodes/ConditionNode.tsx | 33 +- .../panels/properties/ConditionProperties.tsx | 545 +++++++++++++++--- frontend/types/node-editor.ts | 37 +- 4 files changed, 659 insertions(+), 119 deletions(-) diff --git a/backend-node/src/services/nodeFlowExecutionService.ts b/backend-node/src/services/nodeFlowExecutionService.ts index 6f481198..616b4564 100644 --- a/backend-node/src/services/nodeFlowExecutionService.ts +++ b/backend-node/src/services/nodeFlowExecutionService.ts @@ -2707,28 +2707,48 @@ export class NodeFlowExecutionService { const trueData: any[] = []; const falseData: any[] = []; - inputData.forEach((item: any) => { - const results = conditions.map((condition: any) => { + // 배열의 각 항목에 대해 조건 평가 (EXISTS 조건은 비동기) + for (const item of inputData) { + const results: boolean[] = []; + + for (const condition of conditions) { const fieldValue = item[condition.field]; - let compareValue = condition.value; - if (condition.valueType === "field") { - compareValue = item[condition.value]; + // EXISTS 계열 연산자 처리 + if ( + condition.operator === "EXISTS_IN" || + condition.operator === "NOT_EXISTS_IN" + ) { + const existsResult = await this.evaluateExistsCondition( + fieldValue, + condition.operator, + condition.lookupTable, + condition.lookupField, + context.companyCode + ); + results.push(existsResult); logger.info( - `🔄 필드 참조 비교: ${condition.field} (${fieldValue}) vs ${condition.value} (${compareValue})` + `🔍 EXISTS 조건: ${condition.field} (${fieldValue}) ${condition.operator} ${condition.lookupTable}.${condition.lookupField} => ${existsResult}` ); } else { - logger.info( - `📊 고정값 비교: ${condition.field} (${fieldValue}) vs ${compareValue}` + // 일반 연산자 처리 + 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}` + ); + } + + results.push( + this.evaluateCondition(fieldValue, condition.operator, compareValue) ); } - - return this.evaluateCondition( - fieldValue, - condition.operator, - compareValue - ); - }); + } const result = logic === "OR" @@ -2740,7 +2760,7 @@ export class NodeFlowExecutionService { } else { falseData.push(item); } - }); + } logger.info( `🔍 조건 필터링 결과: TRUE ${trueData.length}건 / FALSE ${falseData.length}건 (${logic} 로직)` @@ -2755,27 +2775,46 @@ export class NodeFlowExecutionService { } // 단일 객체인 경우 - const results = conditions.map((condition: any) => { + const results: boolean[] = []; + + for (const condition of conditions) { const fieldValue = inputData[condition.field]; - let compareValue = condition.value; - if (condition.valueType === "field") { - compareValue = inputData[condition.value]; + // EXISTS 계열 연산자 처리 + if ( + condition.operator === "EXISTS_IN" || + condition.operator === "NOT_EXISTS_IN" + ) { + const existsResult = await this.evaluateExistsCondition( + fieldValue, + condition.operator, + condition.lookupTable, + condition.lookupField, + context.companyCode + ); + results.push(existsResult); logger.info( - `🔄 필드 참조 비교: ${condition.field} (${fieldValue}) vs ${condition.value} (${compareValue})` + `🔍 EXISTS 조건: ${condition.field} (${fieldValue}) ${condition.operator} ${condition.lookupTable}.${condition.lookupField} => ${existsResult}` ); } else { - logger.info( - `📊 고정값 비교: ${condition.field} (${fieldValue}) vs ${compareValue}` + // 일반 연산자 처리 + let compareValue = condition.value; + if (condition.valueType === "field") { + compareValue = inputData[condition.value]; + logger.info( + `🔄 필드 참조 비교: ${condition.field} (${fieldValue}) vs ${condition.value} (${compareValue})` + ); + } else { + logger.info( + `📊 고정값 비교: ${condition.field} (${fieldValue}) vs ${compareValue}` + ); + } + + results.push( + this.evaluateCondition(fieldValue, condition.operator, compareValue) ); } - - return this.evaluateCondition( - fieldValue, - condition.operator, - compareValue - ); - }); + } const result = logic === "OR" @@ -2784,7 +2823,7 @@ export class NodeFlowExecutionService { logger.info(`🔍 조건 평가 결과: ${result} (${logic} 로직)`); - // ⚠️ 조건 노드는 TRUE/FALSE 브랜치를 위한 특별한 처리 필요 + // 조건 노드는 TRUE/FALSE 브랜치를 위한 특별한 처리 필요 // 조건 결과를 저장하고, 원본 데이터는 항상 반환 // 다음 노드에서 sourceHandle을 기반으로 필터링됨 return { @@ -2795,6 +2834,68 @@ export class NodeFlowExecutionService { }; } + /** + * EXISTS_IN / NOT_EXISTS_IN 조건 평가 + * 다른 테이블에 값이 존재하는지 확인 + */ + private static async evaluateExistsCondition( + fieldValue: any, + operator: string, + lookupTable: string, + lookupField: string, + companyCode?: string + ): Promise { + if (!lookupTable || !lookupField) { + logger.warn("⚠️ EXISTS 조건: lookupTable 또는 lookupField가 없습니다"); + return false; + } + + if (fieldValue === null || fieldValue === undefined || fieldValue === "") { + logger.info( + `⚠️ EXISTS 조건: 필드값이 비어있어 ${operator === "NOT_EXISTS_IN" ? "TRUE" : "FALSE"} 반환` + ); + // 값이 비어있으면: EXISTS_IN은 false, NOT_EXISTS_IN은 true + return operator === "NOT_EXISTS_IN"; + } + + try { + // 멀티테넌시: company_code 필터 적용 여부 확인 + // company_mng 테이블은 제외 + const hasCompanyCode = lookupTable !== "company_mng" && companyCode; + + let sql: string; + let params: any[]; + + if (hasCompanyCode) { + sql = `SELECT EXISTS(SELECT 1 FROM "${lookupTable}" WHERE "${lookupField}" = $1 AND company_code = $2) as exists_result`; + params = [fieldValue, companyCode]; + } else { + sql = `SELECT EXISTS(SELECT 1 FROM "${lookupTable}" WHERE "${lookupField}" = $1) as exists_result`; + params = [fieldValue]; + } + + logger.info(`🔍 EXISTS 쿼리: ${sql}, params: ${JSON.stringify(params)}`); + + const result = await query(sql, params); + const existsInTable = result[0]?.exists_result === true; + + logger.info( + `🔍 EXISTS 결과: ${fieldValue}이(가) ${lookupTable}.${lookupField}에 ${existsInTable ? "존재함" : "존재하지 않음"}` + ); + + // EXISTS_IN: 존재하면 true + // NOT_EXISTS_IN: 존재하지 않으면 true + if (operator === "EXISTS_IN") { + return existsInTable; + } else { + return !existsInTable; + } + } catch (error: any) { + logger.error(`❌ EXISTS 조건 평가 실패: ${error.message}`); + return false; + } + } + /** * WHERE 절 생성 */ diff --git a/frontend/components/dataflow/node-editor/nodes/ConditionNode.tsx b/frontend/components/dataflow/node-editor/nodes/ConditionNode.tsx index 5418fcab..4cf5e32d 100644 --- a/frontend/components/dataflow/node-editor/nodes/ConditionNode.tsx +++ b/frontend/components/dataflow/node-editor/nodes/ConditionNode.tsx @@ -22,6 +22,13 @@ const OPERATOR_LABELS: Record = { NOT_IN: "NOT IN", IS_NULL: "NULL", IS_NOT_NULL: "NOT NULL", + EXISTS_IN: "EXISTS IN", + NOT_EXISTS_IN: "NOT EXISTS IN", +}; + +// EXISTS 계열 연산자인지 확인 +const isExistsOperator = (operator: string): boolean => { + return operator === "EXISTS_IN" || operator === "NOT_EXISTS_IN"; }; export const ConditionNode = memo(({ data, selected }: NodeProps) => { @@ -54,15 +61,31 @@ export const ConditionNode = memo(({ data, selected }: NodeProps 0 && (
{data.logic}
)} -
+
{condition.field} - + {OPERATOR_LABELS[condition.operator] || condition.operator} - {condition.value !== null && condition.value !== undefined && ( - - {typeof condition.value === "string" ? `"${condition.value}"` : String(condition.value)} + {/* EXISTS 연산자인 경우 테이블.필드 표시 */} + {isExistsOperator(condition.operator) ? ( + + {(condition as any).lookupTableLabel || (condition as any).lookupTable || "..."} + {(condition as any).lookupField && `.${(condition as any).lookupFieldLabel || (condition as any).lookupField}`} + ) : ( + // 일반 연산자인 경우 값 표시 + condition.value !== null && + condition.value !== undefined && ( + + {typeof condition.value === "string" ? `"${condition.value}"` : String(condition.value)} + + ) )}
diff --git a/frontend/components/dataflow/node-editor/panels/properties/ConditionProperties.tsx b/frontend/components/dataflow/node-editor/panels/properties/ConditionProperties.tsx index 87f7f771..a2d060d4 100644 --- a/frontend/components/dataflow/node-editor/panels/properties/ConditionProperties.tsx +++ b/frontend/components/dataflow/node-editor/panels/properties/ConditionProperties.tsx @@ -4,14 +4,18 @@ * 조건 분기 노드 속성 편집 */ -import { useEffect, useState } from "react"; -import { Plus, Trash2 } from "lucide-react"; +import { useEffect, useState, useCallback } from "react"; +import { Plus, Trash2, Database, Search, Check, ChevronsUpDown } from "lucide-react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { useFlowEditorStore } from "@/lib/stores/flowEditorStore"; -import type { ConditionNodeData } from "@/types/node-editor"; +import type { ConditionNodeData, ConditionOperator } from "@/types/node-editor"; +import { tableManagementApi } from "@/lib/api/tableManagement"; +import { cn } from "@/lib/utils"; // 필드 정의 interface FieldDefinition { @@ -20,6 +24,19 @@ interface FieldDefinition { type?: string; } +// 테이블 정보 +interface TableInfo { + tableName: string; + tableLabel: string; +} + +// 테이블 컬럼 정보 +interface ColumnInfo { + columnName: string; + columnLabel: string; + dataType: string; +} + interface ConditionPropertiesProps { nodeId: string; data: ConditionNodeData; @@ -38,8 +55,194 @@ const OPERATORS = [ { value: "NOT_IN", label: "NOT IN" }, { value: "IS_NULL", label: "NULL" }, { value: "IS_NOT_NULL", label: "NOT NULL" }, + { value: "EXISTS_IN", label: "다른 테이블에 존재함" }, + { value: "NOT_EXISTS_IN", label: "다른 테이블에 존재하지 않음" }, ] as const; +// EXISTS 계열 연산자인지 확인 +const isExistsOperator = (operator: string): boolean => { + return operator === "EXISTS_IN" || operator === "NOT_EXISTS_IN"; +}; + +// 테이블 선택용 검색 가능한 Combobox +function TableCombobox({ + tables, + value, + onSelect, + placeholder = "테이블 검색...", +}: { + tables: TableInfo[]; + value: string; + onSelect: (value: string) => void; + placeholder?: string; +}) { + const [open, setOpen] = useState(false); + + const selectedTable = tables.find((t) => t.tableName === value); + + return ( + + + + + + + + + 테이블을 찾을 수 없습니다. + + {tables.map((table) => ( + { + onSelect(table.tableName); + setOpen(false); + }} + className="text-xs" + > + +
+ {table.tableLabel} + {table.tableName} +
+
+ ))} +
+
+
+
+
+ ); +} + +// 컬럼 선택용 검색 가능한 Combobox +function ColumnCombobox({ + columns, + value, + onSelect, + placeholder = "컬럼 검색...", +}: { + columns: ColumnInfo[]; + value: string; + onSelect: (value: string) => void; + placeholder?: string; +}) { + const [open, setOpen] = useState(false); + + const selectedColumn = columns.find((c) => c.columnName === value); + + return ( + + + + + + + + + 컬럼을 찾을 수 없습니다. + + {columns.map((col) => ( + { + onSelect(col.columnName); + setOpen(false); + }} + className="text-xs" + > + + {col.columnLabel} + ({col.columnName}) + + ))} + + + + + + ); +} + +// 컬럼 선택 섹션 (자동 로드 포함) +function ColumnSelectSection({ + lookupTable, + lookupField, + tableColumnsCache, + loadingColumns, + loadTableColumns, + onSelect, +}: { + lookupTable: string; + lookupField: string; + tableColumnsCache: Record; + loadingColumns: Record; + loadTableColumns: (tableName: string) => Promise; + onSelect: (value: string) => void; +}) { + // 캐시에 없고 로딩 중이 아니면 자동으로 로드 + useEffect(() => { + if (lookupTable && !tableColumnsCache[lookupTable] && !loadingColumns[lookupTable]) { + loadTableColumns(lookupTable); + } + }, [lookupTable, tableColumnsCache, loadingColumns, loadTableColumns]); + + const isLoading = loadingColumns[lookupTable]; + const columns = tableColumnsCache[lookupTable]; + + return ( +
+ + {isLoading ? ( +
+ 컬럼 목록 로딩 중... +
+ ) : columns && columns.length > 0 ? ( + + ) : ( +
+ 컬럼 목록을 로드할 수 없습니다 +
+ )} +
+ ); +} + export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) { const { updateNode, nodes, edges } = useFlowEditorStore(); @@ -48,6 +251,12 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) const [logic, setLogic] = useState<"AND" | "OR">(data.logic || "AND"); const [availableFields, setAvailableFields] = useState([]); + // EXISTS 연산자용 상태 + const [allTables, setAllTables] = useState([]); + const [tableColumnsCache, setTableColumnsCache] = useState>({}); + const [loadingTables, setLoadingTables] = useState(false); + const [loadingColumns, setLoadingColumns] = useState>({}); + // 데이터 변경 시 로컬 상태 업데이트 useEffect(() => { setDisplayName(data.displayName || "조건 분기"); @@ -55,6 +264,100 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) setLogic(data.logic || "AND"); }, [data]); + // 전체 테이블 목록 로드 (EXISTS 연산자용) + useEffect(() => { + const loadAllTables = async () => { + // 이미 EXISTS 연산자가 있거나 로드된 적이 있으면 스킵 + if (allTables.length > 0) return; + + // EXISTS 연산자가 하나라도 있으면 테이블 목록 로드 + const hasExistsOperator = conditions.some((c) => isExistsOperator(c.operator)); + if (!hasExistsOperator) return; + + setLoadingTables(true); + try { + const response = await tableManagementApi.getTableList(); + if (response.success && response.data) { + setAllTables( + response.data.map((t: any) => ({ + tableName: t.tableName, + tableLabel: t.tableLabel || t.tableName, + })) + ); + } + } catch (error) { + console.error("테이블 목록 로드 실패:", error); + } finally { + setLoadingTables(false); + } + }; + + loadAllTables(); + }, [conditions, allTables.length]); + + // 테이블 컬럼 로드 함수 + const loadTableColumns = useCallback( + async (tableName: string): Promise => { + // 캐시에 있으면 반환 + if (tableColumnsCache[tableName]) { + return tableColumnsCache[tableName]; + } + + // 이미 로딩 중이면 스킵 + if (loadingColumns[tableName]) { + return []; + } + + // 로딩 상태 설정 + setLoadingColumns((prev) => ({ ...prev, [tableName]: true })); + + try { + // getColumnList 반환: { success, data: { columns, total, ... } } + const response = await tableManagementApi.getColumnList(tableName); + if (response.success && response.data && response.data.columns) { + const columns = response.data.columns.map((c: any) => ({ + columnName: c.columnName, + columnLabel: c.columnLabel || c.columnName, + dataType: c.dataType, + })); + setTableColumnsCache((prev) => ({ ...prev, [tableName]: columns })); + console.log(`✅ 테이블 ${tableName} 컬럼 로드 완료:`, columns.length, "개"); + return columns; + } else { + console.warn(`⚠️ 테이블 ${tableName} 컬럼 조회 실패:`, response); + } + } catch (error) { + console.error(`❌ 테이블 ${tableName} 컬럼 로드 실패:`, error); + } finally { + setLoadingColumns((prev) => ({ ...prev, [tableName]: false })); + } + return []; + }, + [tableColumnsCache, loadingColumns] + ); + + // EXISTS 연산자 선택 시 테이블 목록 강제 로드 + const ensureTablesLoaded = useCallback(async () => { + if (allTables.length > 0) return; + + setLoadingTables(true); + try { + const response = await tableManagementApi.getTableList(); + if (response.success && response.data) { + setAllTables( + response.data.map((t: any) => ({ + tableName: t.tableName, + tableLabel: t.tableLabel || t.tableName, + })) + ); + } + } catch (error) { + console.error("테이블 목록 로드 실패:", error); + } finally { + setLoadingTables(false); + } + }, [allTables.length]); + // 🔥 연결된 소스 노드의 필드를 재귀적으로 수집 useEffect(() => { const getAllSourceFields = (currentNodeId: string, visited: Set = new Set()): FieldDefinition[] => { @@ -170,15 +473,18 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) }, [nodeId, nodes, edges]); const handleAddCondition = () => { - setConditions([ - ...conditions, - { - field: "", - operator: "EQUALS", - value: "", - valueType: "static", // "static" (고정값) 또는 "field" (필드 참조) - }, - ]); + const newCondition = { + field: "", + operator: "EQUALS" as ConditionOperator, + value: "", + valueType: "static" as "static" | "field", + // EXISTS 연산자용 필드는 초기값 없음 + lookupTable: undefined, + lookupTableLabel: undefined, + lookupField: undefined, + lookupFieldLabel: undefined, + }; + setConditions([...conditions, newCondition]); }; const handleRemoveCondition = (index: number) => { @@ -196,9 +502,50 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) }); }; - const handleConditionChange = (index: number, field: string, value: any) => { + const handleConditionChange = async (index: number, field: string, value: any) => { const newConditions = [...conditions]; newConditions[index] = { ...newConditions[index], [field]: value }; + + // EXISTS 연산자로 변경 시 테이블 목록 로드 및 기존 value/valueType 초기화 + if (field === "operator" && isExistsOperator(value)) { + await ensureTablesLoaded(); + // EXISTS 연산자에서는 value, valueType이 필요 없으므로 초기화 + newConditions[index].value = ""; + newConditions[index].valueType = undefined; + } + + // EXISTS 연산자에서 다른 연산자로 변경 시 lookup 필드들 초기화 + if (field === "operator" && !isExistsOperator(value)) { + newConditions[index].lookupTable = undefined; + newConditions[index].lookupTableLabel = undefined; + newConditions[index].lookupField = undefined; + newConditions[index].lookupFieldLabel = undefined; + } + + // lookupTable 변경 시 컬럼 목록 로드 및 라벨 설정 + if (field === "lookupTable" && value) { + const tableInfo = allTables.find((t) => t.tableName === value); + if (tableInfo) { + newConditions[index].lookupTableLabel = tableInfo.tableLabel; + } + // 테이블 변경 시 필드 초기화 + newConditions[index].lookupField = undefined; + newConditions[index].lookupFieldLabel = undefined; + // 컬럼 목록 미리 로드 + await loadTableColumns(value); + } + + // lookupField 변경 시 라벨 설정 + if (field === "lookupField" && value) { + const tableName = newConditions[index].lookupTable; + if (tableName && tableColumnsCache[tableName]) { + const columnInfo = tableColumnsCache[tableName].find((c) => c.columnName === value); + if (columnInfo) { + newConditions[index].lookupFieldLabel = columnInfo.columnLabel; + } + } + } + setConditions(newConditions); updateNode(nodeId, { conditions: newConditions, @@ -329,64 +676,114 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) - {condition.operator !== "IS_NULL" && condition.operator !== "IS_NOT_NULL" && ( + {/* EXISTS 연산자인 경우: 테이블/필드 선택 UI (검색 가능한 Combobox) */} + {isExistsOperator(condition.operator) && ( <>
- - + + {loadingTables ? ( +
+ 테이블 목록 로딩 중... +
+ ) : allTables.length > 0 ? ( + handleConditionChange(index, "lookupTable", value)} + placeholder="테이블 검색..." + /> + ) : ( +
+ 테이블 목록을 로드할 수 없습니다 +
+ )}
-
- - {(condition as any).valueType === "field" ? ( - // 필드 참조: 드롭다운으로 선택 - availableFields.length > 0 ? ( - - ) : ( -
- 소스 노드를 연결하세요 -
- ) - ) : ( - // 고정값: 직접 입력 - handleConditionChange(index, "value", e.target.value)} - placeholder="비교할 값" - className="mt-1 h-8 text-xs" - /> - )} + {(condition as any).lookupTable && ( + handleConditionChange(index, "lookupField", value)} + /> + )} + +
+ {condition.operator === "EXISTS_IN" + ? `소스의 "${condition.field || "..."}" 값이 "${(condition as any).lookupTableLabel || "..."}" 테이블의 "${(condition as any).lookupFieldLabel || "..."}" 컬럼에 존재하면 TRUE` + : `소스의 "${condition.field || "..."}" 값이 "${(condition as any).lookupTableLabel || "..."}" 테이블의 "${(condition as any).lookupFieldLabel || "..."}" 컬럼에 존재하지 않으면 TRUE`}
)} + + {/* 일반 연산자인 경우: 기존 비교값 UI */} + {condition.operator !== "IS_NULL" && + condition.operator !== "IS_NOT_NULL" && + !isExistsOperator(condition.operator) && ( + <> +
+ + +
+ +
+ + {(condition as any).valueType === "field" ? ( + // 필드 참조: 드롭다운으로 선택 + availableFields.length > 0 ? ( + + ) : ( +
+ 소스 노드를 연결하세요 +
+ ) + ) : ( + // 고정값: 직접 입력 + handleConditionChange(index, "value", e.target.value)} + placeholder="비교할 값" + className="mt-1 h-8 text-xs" + /> + )} +
+ + )}
))} @@ -402,20 +799,28 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) {/* 안내 */}
- 🔌 소스 노드 연결: 테이블/외부DB 노드를 연결하면 자동으로 필드 목록이 표시됩니다. + 소스 노드 연결: 테이블/외부DB 노드를 연결하면 자동으로 필드 목록이 표시됩니다.
- 🔄 비교 값 타입:
고정값: 직접 입력한 값과 비교 (예: age > 30) -
필드 참조: 다른 필드의 값과 비교 (예: 주문수량 > 재고수량) + 비교 값 타입:
+ - 고정값: 직접 입력한 값과 비교 (예: age > 30) +
- 필드 참조: 다른 필드의 값과 비교 (예: 주문수량 > 재고수량) +
+
+ 테이블 존재 여부 검사:
+ - 다른 테이블에 존재함: 값이 다른 테이블에 있으면 TRUE +
- 다른 테이블에 존재하지 않음: 값이 다른 테이블에 없으면 TRUE +
+ (예: 품명이 품목정보 테이블에 없으면 자동 등록)
- 💡 AND: 모든 조건이 참이어야 TRUE 출력 + AND: 모든 조건이 참이어야 TRUE 출력
- 💡 OR: 하나라도 참이면 TRUE 출력 + OR: 하나라도 참이면 TRUE 출력
- ⚡ TRUE 출력은 오른쪽 위, FALSE 출력은 오른쪽 아래입니다. + TRUE 출력은 오른쪽 위, FALSE 출력은 오른쪽 아래입니다.
diff --git a/frontend/types/node-editor.ts b/frontend/types/node-editor.ts index 55c8f67e..6eb1bb1c 100644 --- a/frontend/types/node-editor.ts +++ b/frontend/types/node-editor.ts @@ -95,24 +95,35 @@ export interface RestAPISourceNodeData { displayName?: string; } +// 조건 연산자 타입 +export type ConditionOperator = + | "EQUALS" + | "NOT_EQUALS" + | "GREATER_THAN" + | "LESS_THAN" + | "GREATER_THAN_OR_EQUAL" + | "LESS_THAN_OR_EQUAL" + | "LIKE" + | "NOT_LIKE" + | "IN" + | "NOT_IN" + | "IS_NULL" + | "IS_NOT_NULL" + | "EXISTS_IN" // 다른 테이블에 존재함 + | "NOT_EXISTS_IN"; // 다른 테이블에 존재하지 않음 + // 조건 분기 노드 export interface ConditionNodeData { conditions: Array<{ field: string; - operator: - | "EQUALS" - | "NOT_EQUALS" - | "GREATER_THAN" - | "LESS_THAN" - | "GREATER_THAN_OR_EQUAL" - | "LESS_THAN_OR_EQUAL" - | "LIKE" - | "NOT_LIKE" - | "IN" - | "NOT_IN" - | "IS_NULL" - | "IS_NOT_NULL"; + operator: ConditionOperator; value: any; + valueType?: "static" | "field"; // 비교 값 타입 + // EXISTS_IN / NOT_EXISTS_IN 전용 필드 + lookupTable?: string; // 조회할 테이블명 + lookupTableLabel?: string; // 조회할 테이블 라벨 + lookupField?: string; // 조회할 테이블의 비교 필드 + lookupFieldLabel?: string; // 조회할 테이블의 비교 필드 라벨 }>; logic: "AND" | "OR"; displayName?: string;