실행 조건 구현
This commit is contained in:
parent
f50dd520ae
commit
3344a5785c
|
|
@ -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> = {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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. 보안
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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=">=">>=</SelectItem>
|
<SelectItem value=">=">>=</SelectItem>
|
||||||
<SelectItem value="<="><=</SelectItem>
|
<SelectItem value="<="><=</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue