실행 조건 구현

This commit is contained in:
hyeonsu 2025-09-12 11:33:54 +09:00
parent f50dd520ae
commit 3344a5785c
5 changed files with 79 additions and 177 deletions

View File

@ -16,7 +16,7 @@ export async function testConditionalConnection(
const { diagramId } = req.params; const { diagramId } = req.params;
const { testData } = req.body; const { testData } = req.body;
const companyCode = req.user?.company_code; const companyCode = req.user?.companyCode;
if (!companyCode) { if (!companyCode) {
const response: ApiResponse<null> = { const response: ApiResponse<null> = {
@ -86,7 +86,7 @@ export async function executeConditionalActions(
const { diagramId } = req.params; const { diagramId } = req.params;
const { triggerType, tableName, data } = req.body; const { triggerType, tableName, data } = req.body;
const companyCode = req.user?.company_code; const companyCode = req.user?.companyCode;
if (!companyCode) { if (!companyCode) {
const response: ApiResponse<null> = { const response: ApiResponse<null> = {

View File

@ -1,5 +1,5 @@
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
import { logger } from "../config/logger.js"; import { logger } from "../utils/logger";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@ -9,24 +9,7 @@ interface ConditionNode {
operator?: "AND" | "OR"; operator?: "AND" | "OR";
children?: ConditionNode[]; children?: ConditionNode[];
field?: string; field?: string;
operator_type?: operator_type?: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
| "="
| "!="
| ">"
| "<"
| ">="
| "<="
| "LIKE"
| "NOT_LIKE"
| "CONTAINS"
| "STARTS_WITH"
| "ENDS_WITH"
| "IN"
| "NOT_IN"
| "IS_NULL"
| "IS_NOT_NULL"
| "BETWEEN"
| "NOT_BETWEEN";
value?: any; value?: any;
dataType?: string; dataType?: string;
} }
@ -164,9 +147,9 @@ export class EventTriggerService {
const errors: string[] = []; const errors: string[] = [];
try { try {
const control = diagram.control as ConditionControl; const control = diagram.control as unknown as ConditionControl;
const category = diagram.category as ConnectionCategory; const category = diagram.category as unknown as ConnectionCategory;
const plan = diagram.plan as ExecutionPlan; const plan = diagram.plan as unknown as ExecutionPlan;
logger.info( logger.info(
`Executing diagram ${diagram.diagram_id} (${diagram.diagram_name})` `Executing diagram ${diagram.diagram_id} (${diagram.diagram_name})`
@ -302,38 +285,6 @@ export class EventTriggerService {
return Number(fieldValue) <= Number(value); return Number(fieldValue) <= Number(value);
case "LIKE": case "LIKE":
return String(fieldValue).includes(String(value)); return String(fieldValue).includes(String(value));
case "NOT_LIKE":
return !String(fieldValue).includes(String(value));
case "CONTAINS":
return String(fieldValue)
.toLowerCase()
.includes(String(value).toLowerCase());
case "STARTS_WITH":
return String(fieldValue).startsWith(String(value));
case "ENDS_WITH":
return String(fieldValue).endsWith(String(value));
case "IN":
return Array.isArray(value) && value.includes(fieldValue);
case "NOT_IN":
return Array.isArray(value) && !value.includes(fieldValue);
case "IS_NULL":
return fieldValue == null || fieldValue === undefined;
case "IS_NOT_NULL":
return fieldValue != null && fieldValue !== undefined;
case "BETWEEN":
if (Array.isArray(value) && value.length === 2) {
const numValue = Number(fieldValue);
return numValue >= Number(value[0]) && numValue <= Number(value[1]);
}
return false;
case "NOT_BETWEEN":
if (Array.isArray(value) && value.length === 2) {
const numValue = Number(fieldValue);
return !(
numValue >= Number(value[0]) && numValue <= Number(value[1])
);
}
return false;
default: default:
return false; return false;
} }
@ -553,7 +504,7 @@ export class EventTriggerService {
throw new Error(`Diagram ${diagramId} not found`); throw new Error(`Diagram ${diagramId} not found`);
} }
const control = diagram.control as ConditionControl; const control = diagram.control as unknown as ConditionControl;
// 조건 평가만 수행 // 조건 평가만 수행
const conditionMet = control.conditionTree const conditionMet = control.conditionTree

View File

@ -2,13 +2,13 @@
## 📋 프로젝트 개요 ## 📋 프로젝트 개요
현재 DataFlow 시스템에서 3가지 연결 종류를 지원하고 있으며, 이 중 **데이터 저장**과 **외부 호출** 기능에 조건부 실행 로직을 추가해야 합니다. 현재 DataFlow 시스템에서 3가지 연결 종류를 지원하고 있으며, 이 중 **데이터 저장**과 **외부 호출** 기능에 실행 조건 로직을 추가해야 합니다.
### 현재 연결 종류 ### 현재 연결 종류
1. **단순 키값 연결** - 조건 설정 불필요 (기존 방식 유지) 1. **단순 키값 연결** - 조건 설정 불필요 (기존 방식 유지)
2. **데이터 저장** - 조건부 실행 필요 ✨ 2. **데이터 저장** - 실행 조건 설정 필요 ✨
3. **외부 호출** - 조건부 실행 필요 ✨ 3. **외부 호출** - 실행 조건 설정 필요 ✨
## 🎯 기능 요구사항 ## 🎯 기능 요구사항
@ -78,9 +78,7 @@ CREATE INDEX idx_dataflow_category_type ON dataflow_diagrams USING GIN ((categor
```json ```json
{ {
"type": "data-save", // "simple-key" | "data-save" | "external-call" "type": "data-save" // "simple-key" | "data-save" | "external-call"
"rollbackOnError": true,
"enableLogging": true
} }
``` ```
@ -265,7 +263,6 @@ router.post("/diagrams/:id/execute-actions", async (req, res) => {
- 트랜잭션 롤백 지원 - 트랜잭션 롤백 지원
- 부분 실패 시 복구 메커니즘 - 부분 실패 시 복구 메커니즘
- 상세한 실행 로그 저장
### 3. 보안 ### 3. 보안

View File

@ -126,10 +126,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
const [selectedToColumns, setSelectedToColumns] = useState<string[]>([]); const [selectedToColumns, setSelectedToColumns] = useState<string[]>([]);
// 조건부 연결을 위한 새로운 상태들 // 조건부 연결을 위한 새로운 상태들
const [triggerType, setTriggerType] = useState<"insert" | "update" | "delete" | "insert_update">("insert");
const [conditions, setConditions] = useState<ConditionNode[]>([]); const [conditions, setConditions] = useState<ConditionNode[]>([]);
const [rollbackOnError, setRollbackOnError] = useState(true);
const [enableLogging, setEnableLogging] = useState(true);
// 테이블 목록 로드 // 테이블 목록 로드
useEffect(() => { useEffect(() => {
@ -291,7 +288,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
const conditionalSettings = isConditionalConnection() const conditionalSettings = isConditionalConnection()
? { ? {
control: { control: {
triggerType, triggerType: "insert",
conditionTree: conditionTree:
conditions.length > 0 conditions.length > 0
? { ? {
@ -303,8 +300,6 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
}, },
category: { category: {
type: config.connectionType, type: config.connectionType,
rollbackOnError,
enableLogging,
}, },
plan: { plan: {
sourceTable: fromTableName, sourceTable: fromTableName,
@ -378,6 +373,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
operator_type: "=", operator_type: "=",
value: "", value: "",
dataType: "string", dataType: "string",
operator: "AND", // 기본값으로 AND 설정
}; };
setConditions([...conditions, newCondition]); setConditions([...conditions, newCondition]);
}; };
@ -399,28 +395,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
<div className="rounded-lg border border-l-4 border-l-purple-500 bg-purple-50/30 p-4"> <div className="rounded-lg border border-l-4 border-l-purple-500 bg-purple-50/30 p-4">
<div className="mb-4 flex items-center gap-2"> <div className="mb-4 flex items-center gap-2">
<Zap className="h-4 w-4 text-purple-500" /> <Zap className="h-4 w-4 text-purple-500" />
<span className="text-sm font-medium"> </span> <span className="text-sm font-medium"> </span>
</div>
{/* 트리거 타입 선택 */}
<div className="mb-4">
<Label htmlFor="triggerType" className="mb-2 block text-sm font-medium">
</Label>
<Select
value={triggerType}
onValueChange={(value: "insert" | "update" | "delete" | "insert_update") => setTriggerType(value)}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="insert"> (INSERT)</SelectItem>
<SelectItem value="update"> (UPDATE)</SelectItem>
<SelectItem value="delete"> (DELETE)</SelectItem>
<SelectItem value="insert_update">/ (INSERT/UPDATE)</SelectItem>
</SelectContent>
</Select>
</div> </div>
{/* 실행 조건 설정 */} {/* 실행 조건 설정 */}
@ -443,7 +418,27 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
</div> </div>
) : ( ) : (
conditions.map((condition, index) => ( conditions.map((condition, index) => (
<div key={index} className="flex items-center gap-2 rounded border bg-white p-2"> <div key={index}>
{/* 첫 번째 조건이 아닐 때 AND/OR 연산자를 위에 표시 */}
{index > 0 && (
<div className="flex justify-center py-1">
<Select
value={conditions[index - 1]?.operator || "AND"}
onValueChange={(value: "AND" | "OR") => updateCondition(index - 1, "operator", value)}
>
<SelectTrigger className="h-8 w-20 border-blue-200 bg-blue-50 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="AND">AND</SelectItem>
<SelectItem value="OR">OR</SelectItem>
</SelectContent>
</Select>
</div>
)}
{/* 조건 행 */}
<div className="flex items-center gap-2 rounded border bg-white p-2">
<Select <Select
value={condition.field || ""} value={condition.field || ""}
onValueChange={(value) => updateCondition(index, "field", value)} onValueChange={(value) => updateCondition(index, "field", value)}
@ -475,8 +470,6 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
<SelectItem value=">=">&gt;=</SelectItem> <SelectItem value=">=">&gt;=</SelectItem>
<SelectItem value="<=">&lt;=</SelectItem> <SelectItem value="<=">&lt;=</SelectItem>
<SelectItem value="LIKE">LIKE</SelectItem> <SelectItem value="LIKE">LIKE</SelectItem>
<SelectItem value="IN">IN</SelectItem>
<SelectItem value="IS_NULL">IS NULL</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
@ -491,33 +484,11 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
</div> </div>
</div>
)) ))
)} )}
</div> </div>
</div> </div>
{/* 추가 옵션 */}
<div className="grid grid-cols-2 gap-4 border-t pt-2">
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={rollbackOnError}
onChange={(e) => setRollbackOnError(e.target.checked)}
className="rounded"
/>
</label>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={enableLogging}
onChange={(e) => setEnableLogging(e.target.checked)}
className="rounded"
/>
</label>
</div>
</div> </div>
); );
}; };

View File

@ -13,24 +13,7 @@ export interface ConditionNode {
operator?: "AND" | "OR"; operator?: "AND" | "OR";
children?: ConditionNode[]; children?: ConditionNode[];
field?: string; field?: string;
operator_type?: operator_type?: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
| "="
| "!="
| ">"
| "<"
| ">="
| "<="
| "LIKE"
| "NOT_LIKE"
| "CONTAINS"
| "STARTS_WITH"
| "ENDS_WITH"
| "IN"
| "NOT_IN"
| "IS_NULL"
| "IS_NOT_NULL"
| "BETWEEN"
| "NOT_BETWEEN";
value?: any; value?: any;
dataType?: string; dataType?: string;
} }