Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/report
This commit is contained in:
332
docs/node-action-target-selection-plan.md
Normal file
332
docs/node-action-target-selection-plan.md
Normal file
@@ -0,0 +1,332 @@
|
||||
# 액션 노드 타겟 선택 시스템 개선 계획
|
||||
|
||||
## 📋 현재 문제점
|
||||
|
||||
### 1. 타겟 타입 구분 부재
|
||||
- INSERT/UPDATE/DELETE/UPSERT 액션 노드가 타겟 테이블만 선택 가능
|
||||
- 내부 DB인지, 외부 DB인지, REST API인지 구분 없음
|
||||
- 실행 시 항상 내부 DB로만 동작
|
||||
|
||||
### 2. 외부 시스템 연동 불가
|
||||
- 외부 DB에 데이터 저장 불가
|
||||
- 외부 REST API 호출 불가
|
||||
- 하이브리드 플로우 구성 불가 (내부 → 외부 데이터 전송)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 개선 목표
|
||||
|
||||
액션 노드에서 다음 3가지 타겟 타입을 선택할 수 있도록 개선:
|
||||
|
||||
### 1. 내부 데이터베이스 (Internal DB)
|
||||
- 현재 시스템의 PostgreSQL
|
||||
- 기존 동작 유지
|
||||
|
||||
### 2. 외부 데이터베이스 (External DB)
|
||||
- 외부 커넥션 관리에서 설정한 DB
|
||||
- PostgreSQL, MySQL, Oracle, MSSQL, MariaDB 지원
|
||||
|
||||
### 3. REST API
|
||||
- 외부 REST API 호출
|
||||
- HTTP 메서드: POST, PUT, PATCH, DELETE
|
||||
- 인증: None, Basic, Bearer Token, API Key
|
||||
|
||||
---
|
||||
|
||||
## 📐 타입 정의 확장
|
||||
|
||||
### TargetType 추가
|
||||
```typescript
|
||||
export type TargetType = "internal" | "external" | "api";
|
||||
|
||||
export interface BaseActionNodeData {
|
||||
displayName: string;
|
||||
targetType: TargetType; // 🔥 새로 추가
|
||||
|
||||
// targetType === "internal"
|
||||
targetTable?: string;
|
||||
targetTableLabel?: string;
|
||||
|
||||
// targetType === "external"
|
||||
externalConnectionId?: number;
|
||||
externalConnectionName?: string;
|
||||
externalDbType?: string;
|
||||
externalTargetTable?: string;
|
||||
externalTargetSchema?: string;
|
||||
|
||||
// targetType === "api"
|
||||
apiEndpoint?: string;
|
||||
apiMethod?: "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
apiAuthType?: "none" | "basic" | "bearer" | "apikey";
|
||||
apiAuthConfig?: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
apiKey?: string;
|
||||
apiKeyHeader?: string;
|
||||
};
|
||||
apiHeaders?: Record<string, string>;
|
||||
apiBodyTemplate?: string; // JSON 템플릿
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI 설계
|
||||
|
||||
### 1. 타겟 타입 선택 (공통)
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 타겟 타입 │
|
||||
│ ○ 내부 데이터베이스 (기본) │
|
||||
│ ○ 외부 데이터베이스 │
|
||||
│ ○ REST API │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2-A. 내부 DB 선택 시 (기존 UI 유지)
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 테이블 선택: [검색 가능 Combobox] │
|
||||
│ 필드 매핑: │
|
||||
│ • source_field → target_field │
|
||||
│ • ... │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2-B. 외부 DB 선택 시
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 외부 커넥션: [🐘 PostgreSQL - 운영DB]│
|
||||
│ 스키마: [public ▼] │
|
||||
│ 테이블: [users ▼] │
|
||||
│ 필드 매핑: │
|
||||
│ • source_field → target_field │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2-C. REST API 선택 시
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ API 엔드포인트: │
|
||||
│ [https://api.example.com/users] │
|
||||
│ │
|
||||
│ HTTP 메서드: [POST ▼] │
|
||||
│ │
|
||||
│ 인증 타입: [Bearer Token ▼] │
|
||||
│ Token: [••••••••••••••] │
|
||||
│ │
|
||||
│ 헤더 (선택): │
|
||||
│ Content-Type: application/json │
|
||||
│ + 헤더 추가 │
|
||||
│ │
|
||||
│ 바디 템플릿: │
|
||||
│ { │
|
||||
│ "name": "{{source.name}}", │
|
||||
│ "email": "{{source.email}}" │
|
||||
│ } │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 구현 단계
|
||||
|
||||
### Phase 1: 타입 정의 및 기본 UI (1-2시간)
|
||||
- [ ] `types/node-editor.ts`에 `TargetType` 추가
|
||||
- [ ] `InsertActionNodeData` 등 인터페이스 확장
|
||||
- [ ] 속성 패널에 타겟 타입 선택 라디오 버튼 추가
|
||||
|
||||
### Phase 2: 내부 DB 타입 (기존 유지)
|
||||
- [ ] `targetType === "internal"` 처리
|
||||
- [ ] 기존 로직 그대로 유지
|
||||
- [ ] 기본값으로 설정
|
||||
|
||||
### Phase 3: 외부 DB 타입 (2-3시간)
|
||||
- [ ] 외부 커넥션 선택 UI
|
||||
- [ ] 외부 테이블/컬럼 로드 (기존 API 재사용)
|
||||
- [ ] 백엔드: `nodeFlowExecutionService.ts`에 외부 DB 실행 로직 추가
|
||||
- [ ] `DatabaseConnectorFactory` 활용
|
||||
|
||||
### Phase 4: REST API 타입 (3-4시간)
|
||||
- [ ] API 엔드포인트 설정 UI
|
||||
- [ ] HTTP 메서드 선택
|
||||
- [ ] 인증 타입별 설정 UI
|
||||
- [ ] 바디 템플릿 에디터 (변수 치환 지원)
|
||||
- [ ] 백엔드: Axios를 사용한 API 호출 로직
|
||||
- [ ] 응답 처리 및 에러 핸들링
|
||||
|
||||
### Phase 5: 노드 시각화 개선 (1시간)
|
||||
- [ ] 노드에 타겟 타입 아이콘 표시
|
||||
- 내부 DB: 💾
|
||||
- 외부 DB: 🔌 + DB 타입 아이콘
|
||||
- REST API: 🌐
|
||||
- [ ] 노드 색상 구분
|
||||
|
||||
### Phase 6: 검증 및 테스트 (2시간)
|
||||
- [ ] 타겟 타입별 필수 값 검증
|
||||
- [ ] 연결 규칙 업데이트
|
||||
- [ ] 통합 테스트
|
||||
|
||||
---
|
||||
|
||||
## 🔍 백엔드 실행 로직
|
||||
|
||||
### nodeFlowExecutionService.ts
|
||||
```typescript
|
||||
private static async executeInsertAction(
|
||||
node: FlowNode,
|
||||
inputData: any[],
|
||||
context: ExecutionContext
|
||||
): Promise<any[]> {
|
||||
const { targetType } = node.data;
|
||||
|
||||
switch (targetType) {
|
||||
case "internal":
|
||||
return this.executeInternalInsert(node, inputData);
|
||||
|
||||
case "external":
|
||||
return this.executeExternalInsert(node, inputData);
|
||||
|
||||
case "api":
|
||||
return this.executeApiInsert(node, inputData);
|
||||
|
||||
default:
|
||||
throw new Error(`지원하지 않는 타겟 타입: ${targetType}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 외부 DB INSERT
|
||||
private static async executeExternalInsert(
|
||||
node: FlowNode,
|
||||
inputData: any[]
|
||||
): Promise<any[]> {
|
||||
const { externalConnectionId, externalTargetTable, fieldMappings } = node.data;
|
||||
|
||||
const connector = await DatabaseConnectorFactory.getConnector(
|
||||
externalConnectionId!,
|
||||
node.data.externalDbType!
|
||||
);
|
||||
|
||||
const results = [];
|
||||
for (const row of inputData) {
|
||||
const values = fieldMappings.map(m => row[m.sourceField]);
|
||||
const columns = fieldMappings.map(m => m.targetField);
|
||||
|
||||
const result = await connector.executeQuery(
|
||||
`INSERT INTO ${externalTargetTable} (${columns.join(", ")}) VALUES (${...})`,
|
||||
values
|
||||
);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
await connector.disconnect();
|
||||
return results;
|
||||
}
|
||||
|
||||
// 🔥 REST API INSERT (POST)
|
||||
private static async executeApiInsert(
|
||||
node: FlowNode,
|
||||
inputData: any[]
|
||||
): Promise<any[]> {
|
||||
const {
|
||||
apiEndpoint,
|
||||
apiMethod,
|
||||
apiAuthType,
|
||||
apiAuthConfig,
|
||||
apiHeaders,
|
||||
apiBodyTemplate
|
||||
} = node.data;
|
||||
|
||||
const axios = require("axios");
|
||||
const headers = { ...apiHeaders };
|
||||
|
||||
// 인증 헤더 추가
|
||||
if (apiAuthType === "bearer" && apiAuthConfig?.token) {
|
||||
headers["Authorization"] = `Bearer ${apiAuthConfig.token}`;
|
||||
} else if (apiAuthType === "apikey" && apiAuthConfig?.apiKey) {
|
||||
headers[apiAuthConfig.apiKeyHeader || "X-API-Key"] = apiAuthConfig.apiKey;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const row of inputData) {
|
||||
// 템플릿 변수 치환
|
||||
const body = this.replaceTemplateVariables(apiBodyTemplate, row);
|
||||
|
||||
const response = await axios({
|
||||
method: apiMethod || "POST",
|
||||
url: apiEndpoint,
|
||||
headers,
|
||||
data: JSON.parse(body),
|
||||
});
|
||||
|
||||
results.push(response.data);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 우선순위
|
||||
|
||||
### High Priority
|
||||
1. **Phase 1**: 타입 정의 및 기본 UI
|
||||
2. **Phase 2**: 내부 DB 타입 (기존 유지)
|
||||
3. **Phase 3**: 외부 DB 타입
|
||||
|
||||
### Medium Priority
|
||||
4. **Phase 4**: REST API 타입
|
||||
5. **Phase 5**: 노드 시각화
|
||||
|
||||
### Low Priority
|
||||
6. **Phase 6**: 고급 기능 (재시도, 배치 처리 등)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 예상 효과
|
||||
|
||||
### 1. 유연성 증가
|
||||
- 다양한 시스템 간 데이터 연동 가능
|
||||
- 하이브리드 플로우 구성 (내부 → 외부 → API)
|
||||
|
||||
### 2. 사용 사례 확장
|
||||
```
|
||||
[사례 1] 내부 DB → 외부 DB 동기화
|
||||
TableSource(내부)
|
||||
→ DataTransform
|
||||
→ INSERT(외부 DB)
|
||||
|
||||
[사례 2] 내부 DB → REST API 전송
|
||||
TableSource(내부)
|
||||
→ DataTransform
|
||||
→ INSERT(REST API)
|
||||
|
||||
[사례 3] 복합 플로우
|
||||
TableSource(내부)
|
||||
→ INSERT(외부 DB)
|
||||
→ INSERT(REST API - 알림)
|
||||
```
|
||||
|
||||
### 3. 기존 기능과의 호환
|
||||
- 기본값: `targetType = "internal"`
|
||||
- 기존 플로우 마이그레이션 불필요
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 주의사항
|
||||
|
||||
### 1. 보안
|
||||
- API 인증 정보 암호화 저장
|
||||
- 민감 데이터 로깅 방지
|
||||
|
||||
### 2. 에러 핸들링
|
||||
- 외부 시스템 타임아웃 처리
|
||||
- 재시도 로직 (선택적)
|
||||
- 부분 실패 처리 (이미 구현됨)
|
||||
|
||||
### 3. 성능
|
||||
- 외부 DB 연결 풀 관리 (이미 구현됨)
|
||||
- REST API Rate Limiting 고려
|
||||
|
||||
481
docs/노드_구조_개선안.md
Normal file
481
docs/노드_구조_개선안.md
Normal file
@@ -0,0 +1,481 @@
|
||||
# 노드 구조 개선안 - FROM/TO 테이블 명확화
|
||||
|
||||
**작성일**: 2025-01-02
|
||||
**버전**: 1.0
|
||||
**상태**: 🤔 검토 중
|
||||
|
||||
---
|
||||
|
||||
## 📋 문제 인식
|
||||
|
||||
### 현재 설계의 한계
|
||||
|
||||
```
|
||||
현재 플로우:
|
||||
TableSource(user_info) → FieldMapping → InsertAction(targetTable: "orders")
|
||||
```
|
||||
|
||||
**문제점**:
|
||||
|
||||
1. 타겟 테이블(orders)이 노드로 표현되지 않음
|
||||
2. InsertAction의 속성으로만 존재 → 시각적으로 불명확
|
||||
3. FROM(user_info)과 TO(orders)의 관계가 직관적이지 않음
|
||||
4. 타겟 테이블의 스키마 정보를 참조하기 어려움
|
||||
|
||||
---
|
||||
|
||||
## 💡 개선 방안
|
||||
|
||||
### 옵션 1: TableTarget 노드 추가 (권장 ⭐)
|
||||
|
||||
**새로운 플로우**:
|
||||
|
||||
```
|
||||
TableSource(user_info) → FieldMapping → TableTarget(orders) → InsertAction
|
||||
```
|
||||
|
||||
**노드 추가**:
|
||||
|
||||
- `TableTarget` - 타겟 테이블을 명시적으로 표현
|
||||
|
||||
**장점**:
|
||||
|
||||
- ✅ FROM/TO가 시각적으로 명확
|
||||
- ✅ 타겟 테이블 스키마를 미리 로드 가능
|
||||
- ✅ FieldMapping에서 타겟 필드 자동 완성 가능
|
||||
- ✅ 데이터 흐름이 직관적
|
||||
|
||||
**단점**:
|
||||
|
||||
- ⚠️ 노드 개수 증가 (복잡도 증가)
|
||||
- ⚠️ 기존 설계와 호환성 문제
|
||||
|
||||
---
|
||||
|
||||
### 옵션 2: Action 노드에 Target 속성 유지 (현재 방식)
|
||||
|
||||
**현재 플로우 유지**:
|
||||
|
||||
```
|
||||
TableSource(user_info) → FieldMapping → InsertAction(targetTable: "orders")
|
||||
```
|
||||
|
||||
**개선 방법**:
|
||||
|
||||
- Action 노드에서 타겟 테이블을 더 명확히 표시
|
||||
- 노드 UI에 타겟 테이블명을 크게 표시
|
||||
- Properties Panel에서 타겟 테이블 선택 시 스키마 정보 제공
|
||||
|
||||
**장점**:
|
||||
|
||||
- ✅ 기존 설계 유지 (구현 완료된 상태)
|
||||
- ✅ 노드 개수가 적음 (간결함)
|
||||
- ✅ 빠른 플로우 구성 가능
|
||||
|
||||
**단점**:
|
||||
|
||||
- ❌ 시각적으로 FROM/TO 관계가 불명확
|
||||
- ❌ FieldMapping 단계에서 타겟 필드 정보 접근이 어려움
|
||||
|
||||
---
|
||||
|
||||
### 옵션 3: 가상 노드 자동 표시 (신규 제안 ⭐⭐)
|
||||
|
||||
**개념**:
|
||||
Action 노드에서 targetTable 속성을 설정하면, **시각적으로만** 타겟 테이블 노드를 자동 생성
|
||||
|
||||
**실제 플로우 (저장되는 구조)**:
|
||||
|
||||
```
|
||||
TableSource(user_info) → FieldMapping → InsertAction(targetTable: "orders")
|
||||
```
|
||||
|
||||
**시각적 표시 (화면에 보이는 모습)**:
|
||||
|
||||
```
|
||||
TableSource(user_info)
|
||||
→ FieldMapping
|
||||
→ InsertAction(targetTable: "orders")
|
||||
→ 👻 orders (가상 노드, 자동 생성)
|
||||
```
|
||||
|
||||
**특징**:
|
||||
|
||||
- 가상 노드는 선택/이동/삭제 불가능
|
||||
- 반투명하게 표시하여 가상임을 명확히 표시
|
||||
- Action 노드의 targetTable 속성 변경 시 자동 업데이트
|
||||
- 저장 시에는 가상 노드 제외
|
||||
|
||||
**장점**:
|
||||
|
||||
- ✅ 사용자는 기존대로 사용 (노드 추가 불필요)
|
||||
- ✅ 시각적으로 FROM/TO 관계 명확
|
||||
- ✅ 기존 설계 100% 유지
|
||||
- ✅ 구현 복잡도 낮음
|
||||
- ✅ 기존 플로우와 완벽 호환
|
||||
|
||||
**단점**:
|
||||
|
||||
- ⚠️ 가상 노드의 상호작용 제한 필요
|
||||
- ⚠️ "왜 클릭이 안 되지?" 혼란 가능성
|
||||
- ⚠️ 가상 노드 렌더링 로직 추가
|
||||
|
||||
---
|
||||
|
||||
### 옵션 4: 하이브리드 방식
|
||||
|
||||
**조건부 사용**:
|
||||
|
||||
```
|
||||
// 단순 케이스: TableTarget 생략
|
||||
TableSource → FieldMapping → InsertAction(targetTable 지정)
|
||||
|
||||
// 복잡한 케이스: TableTarget 사용
|
||||
TableSource → FieldMapping → TableTarget → InsertAction
|
||||
```
|
||||
|
||||
**장점**:
|
||||
|
||||
- ✅ 유연성 제공
|
||||
- ✅ 단순/복잡한 케이스 모두 대응
|
||||
|
||||
**단점**:
|
||||
|
||||
- ❌ 사용자 혼란 가능성
|
||||
- ❌ 검증 로직 복잡
|
||||
|
||||
---
|
||||
|
||||
## 🎯 권장 방안 비교
|
||||
|
||||
### 옵션 재평가
|
||||
|
||||
| 항목 | 옵션 1<br/>(TableTarget) | 옵션 2<br/>(현재 방식) | 옵션 3<br/>(가상 노드) ⭐ |
|
||||
| ----------------- | ------------------------ | ---------------------- | ------------------------- |
|
||||
| **시각적 명확성** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| **구현 복잡도** | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
|
||||
| **사용자 편의성** | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| **기존 호환성** | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| **자동 완성** | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
|
||||
| **유지보수성** | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
|
||||
| **학습 곡선** | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
|
||||
### 최종 권장: **옵션 3 (가상 노드 자동 표시)** ⭐⭐
|
||||
|
||||
**선택 이유**:
|
||||
|
||||
1. ✅ **최고의 시각적 명확성** - FROM/TO 관계가 한눈에 보임
|
||||
2. ✅ **사용자 편의성** - 기존 방식 그대로, 노드 추가 불필요
|
||||
3. ✅ **완벽한 호환성** - 기존 플로우 수정 불필요
|
||||
4. ✅ **낮은 학습 곡선** - 새로운 노드 타입 학습 불필요
|
||||
5. ✅ **적절한 구현 복잡도** - React Flow의 커스텀 렌더링 활용
|
||||
|
||||
**구현 방식**:
|
||||
|
||||
```typescript
|
||||
// Action 노드가 있으면 자동으로 가상 타겟 노드 생성
|
||||
function generateVirtualTargetNodes(nodes: FlowNode[]): VirtualNode[] {
|
||||
return nodes
|
||||
.filter((node) => isActionNode(node.type) && node.data.targetTable)
|
||||
.map((actionNode) => ({
|
||||
id: `virtual-target-${actionNode.id}`,
|
||||
type: "virtualTarget",
|
||||
position: {
|
||||
x: actionNode.position.x,
|
||||
y: actionNode.position.y + 150,
|
||||
},
|
||||
data: {
|
||||
tableName: actionNode.data.targetTable,
|
||||
sourceActionId: actionNode.id,
|
||||
isVirtual: true,
|
||||
},
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 대안: 옵션 1 (TableTarget 추가)
|
||||
|
||||
### 새로운 노드 타입 추가
|
||||
|
||||
#### TableTarget 노드
|
||||
|
||||
**타입**: `tableTarget`
|
||||
|
||||
**데이터 구조**:
|
||||
|
||||
```typescript
|
||||
interface TableTargetNodeData {
|
||||
tableName: string; // 타겟 테이블명
|
||||
schema?: string; // 스키마 (선택)
|
||||
columns?: Array<{
|
||||
// 타겟 컬럼 정보
|
||||
name: string;
|
||||
type: string;
|
||||
nullable: boolean;
|
||||
primaryKey: boolean;
|
||||
}>;
|
||||
displayName?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**특징**:
|
||||
|
||||
- 입력: FieldMapping, DataTransform 등에서 받음
|
||||
- 출력: Action 노드로 전달
|
||||
- 타겟 테이블 스키마를 미리 로드하여 검증 가능
|
||||
|
||||
**시각적 표현**:
|
||||
|
||||
```
|
||||
┌────────────────────┐
|
||||
│ 📊 Table Target │
|
||||
├────────────────────┤
|
||||
│ orders │
|
||||
│ schema: public │
|
||||
├────────────────────┤
|
||||
│ 컬럼: │
|
||||
│ • order_id (PK) │
|
||||
│ • customer_id │
|
||||
│ • order_date │
|
||||
│ • total_amount │
|
||||
└────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 개선된 연결 규칙
|
||||
|
||||
#### TableTarget 추가 시 연결 규칙
|
||||
|
||||
**허용되는 연결**:
|
||||
|
||||
```
|
||||
✅ FieldMapping → TableTarget
|
||||
✅ DataTransform → TableTarget
|
||||
✅ Condition → TableTarget
|
||||
✅ TableTarget → InsertAction
|
||||
✅ TableTarget → UpdateAction
|
||||
✅ TableTarget → UpsertAction
|
||||
```
|
||||
|
||||
**금지되는 연결**:
|
||||
|
||||
```
|
||||
❌ TableSource → TableTarget (직접 연결 불가)
|
||||
❌ TableTarget → DeleteAction (DELETE는 타겟 불필요)
|
||||
❌ TableTarget → TableTarget
|
||||
```
|
||||
|
||||
**새로운 검증 규칙**:
|
||||
|
||||
1. Action 노드는 TableTarget 또는 targetTable 속성 중 하나 필수
|
||||
2. TableTarget이 있으면 Action의 targetTable 속성 무시
|
||||
3. FieldMapping 이후에 TableTarget이 오면 자동 필드 매칭 제안
|
||||
|
||||
---
|
||||
|
||||
### 실제 사용 예시
|
||||
|
||||
#### 예시 1: 단순 데이터 복사
|
||||
|
||||
**기존 방식**:
|
||||
|
||||
```
|
||||
TableSource(user_info)
|
||||
→ FieldMapping(user_id → customer_id, user_name → name)
|
||||
→ InsertAction(targetTable: "customers")
|
||||
```
|
||||
|
||||
**개선 방식**:
|
||||
|
||||
```
|
||||
TableSource(user_info)
|
||||
→ FieldMapping(user_id → customer_id)
|
||||
→ TableTarget(customers)
|
||||
→ InsertAction
|
||||
```
|
||||
|
||||
**장점**:
|
||||
|
||||
- customers 테이블 스키마를 FieldMapping에서 참조 가능
|
||||
- 필드 자동 완성 제공
|
||||
|
||||
---
|
||||
|
||||
#### 예시 2: 조건부 데이터 처리
|
||||
|
||||
**개선 방식**:
|
||||
|
||||
```
|
||||
TableSource(user_info)
|
||||
→ Condition(age >= 18)
|
||||
├─ TRUE → TableTarget(adult_users) → InsertAction
|
||||
└─ FALSE → TableTarget(minor_users) → InsertAction
|
||||
```
|
||||
|
||||
**장점**:
|
||||
|
||||
- TRUE/FALSE 분기마다 다른 타겟 테이블 명확히 표시
|
||||
|
||||
---
|
||||
|
||||
#### 예시 3: 멀티 소스 + 단일 타겟
|
||||
|
||||
**개선 방식**:
|
||||
|
||||
```
|
||||
┌─ TableSource(users) ────┐
|
||||
│ ↓
|
||||
└─ ExternalDB(orders) ─→ FieldMapping → TableTarget(user_orders) → InsertAction
|
||||
```
|
||||
|
||||
**장점**:
|
||||
|
||||
- 여러 소스에서 데이터를 받아 하나의 타겟으로 통합
|
||||
- 타겟 테이블이 시각적으로 명확
|
||||
|
||||
---
|
||||
|
||||
## 🔧 구현 계획
|
||||
|
||||
### Phase 1: TableTarget 노드 구현
|
||||
|
||||
**작업 항목**:
|
||||
|
||||
1. ✅ `TableTargetNodeData` 인터페이스 정의
|
||||
2. ✅ `TableTargetNode.tsx` 컴포넌트 생성
|
||||
3. ✅ `TableTargetProperties.tsx` 속성 패널 생성
|
||||
4. ✅ Node Palette에 추가
|
||||
5. ✅ FlowEditor에 등록
|
||||
|
||||
**예상 시간**: 2시간
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: 연결 규칙 업데이트
|
||||
|
||||
**작업 항목**:
|
||||
|
||||
1. ✅ `validateConnection`에 TableTarget 규칙 추가
|
||||
2. ✅ Action 노드가 TableTarget 입력을 받도록 수정
|
||||
3. ✅ 검증 로직 업데이트
|
||||
|
||||
**예상 시간**: 1시간
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: 자동 필드 매핑 개선
|
||||
|
||||
**작업 항목**:
|
||||
|
||||
1. ✅ TableTarget이 연결되면 타겟 스키마 자동 로드
|
||||
2. ✅ FieldMapping에서 타겟 필드 자동 완성 제공
|
||||
3. ✅ 필드 타입 호환성 검증
|
||||
|
||||
**예상 시간**: 2시간
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: 기존 플로우 마이그레이션
|
||||
|
||||
**작업 항목**:
|
||||
|
||||
1. ✅ 기존 InsertAction의 targetTable을 TableTarget으로 변환
|
||||
2. ✅ 자동 마이그레이션 스크립트 작성
|
||||
3. ✅ 호환성 유지 모드 제공
|
||||
|
||||
**예상 시간**: 2시간
|
||||
|
||||
---
|
||||
|
||||
## 🤔 고려사항
|
||||
|
||||
### 1. 기존 플로우와의 호환성
|
||||
|
||||
**문제**: 이미 저장된 플로우는 TableTarget 없이 구성됨
|
||||
|
||||
**해결 방안**:
|
||||
|
||||
- **옵션 A**: 자동 마이그레이션
|
||||
|
||||
- 플로우 로드 시 InsertAction의 targetTable을 TableTarget 노드로 변환
|
||||
- 기존 데이터는 보존
|
||||
|
||||
- **옵션 B**: 호환성 모드
|
||||
- TableTarget 없이도 동작하도록 유지
|
||||
- 새 플로우만 TableTarget 사용 권장
|
||||
|
||||
**권장**: 옵션 B (호환성 모드)
|
||||
|
||||
---
|
||||
|
||||
### 2. 사용자 경험
|
||||
|
||||
**우려**: 노드가 하나 더 추가되어 복잡해짐
|
||||
|
||||
**완화 방안**:
|
||||
|
||||
- 템플릿 제공: "TableSource → FieldMapping → TableTarget → InsertAction" 세트를 템플릿으로 제공
|
||||
- 자동 생성: InsertAction 생성 시 TableTarget 자동 생성 옵션
|
||||
- 가이드: 처음 사용자를 위한 튜토리얼
|
||||
|
||||
---
|
||||
|
||||
### 3. 성능
|
||||
|
||||
**우려**: TableTarget이 스키마를 로드하면 성능 저하 가능성
|
||||
|
||||
**완화 방안**:
|
||||
|
||||
- 캐싱: 한 번 로드한 스키마는 캐싱
|
||||
- 지연 로딩: 필요할 때만 스키마 로드
|
||||
- 백그라운드 로딩: 비동기로 스키마 로드
|
||||
|
||||
---
|
||||
|
||||
## 📊 비교 분석
|
||||
|
||||
| 항목 | 옵션 1 (TableTarget) | 옵션 2 (현재 방식) |
|
||||
| ------------------- | -------------------- | ------------------ |
|
||||
| **시각적 명확성** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
|
||||
| **구현 복잡도** | ⭐⭐⭐⭐ | ⭐⭐ |
|
||||
| **사용자 학습곡선** | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| **자동 완성 지원** | ⭐⭐⭐⭐⭐ | ⭐⭐ |
|
||||
| **유지보수성** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
|
||||
| **기존 호환성** | ⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 결론
|
||||
|
||||
### 권장 사항: **옵션 1 (TableTarget 추가)**
|
||||
|
||||
**이유**:
|
||||
|
||||
1. ✅ 데이터 흐름이 시각적으로 명확
|
||||
2. ✅ 스키마 기반 자동 완성 가능
|
||||
3. ✅ 향후 확장성 우수
|
||||
4. ✅ 복잡한 데이터 흐름에서 특히 유용
|
||||
|
||||
**단계적 도입**:
|
||||
|
||||
- Phase 1: TableTarget 노드 추가 (선택 사항)
|
||||
- Phase 2: 기존 방식과 공존
|
||||
- Phase 3: 사용자 피드백 수집
|
||||
- Phase 4: 장기적으로 TableTarget 방식 권장
|
||||
|
||||
---
|
||||
|
||||
## 📝 다음 단계
|
||||
|
||||
1. **의사 결정**: 옵션 1 vs 옵션 2 선택
|
||||
2. **프로토타입**: TableTarget 노드 간단히 구현
|
||||
3. **테스트**: 실제 사용 시나리오로 검증
|
||||
4. **문서화**: 사용 가이드 작성
|
||||
5. **배포**: 단계적 릴리스
|
||||
|
||||
---
|
||||
|
||||
**피드백 환영**: 이 설계에 대한 의견을 주시면 개선하겠습니다! 💬
|
||||
1920
docs/노드_기반_제어_시스템_개선_계획.md
Normal file
1920
docs/노드_기반_제어_시스템_개선_계획.md
Normal file
File diff suppressed because it is too large
Load Diff
939
docs/노드_시스템_버튼_통합_분석.md
Normal file
939
docs/노드_시스템_버튼_통합_분석.md
Normal file
@@ -0,0 +1,939 @@
|
||||
# 노드 시스템 - 버튼 통합 호환성 분석
|
||||
|
||||
**작성일**: 2025-01-02
|
||||
**버전**: 1.0
|
||||
**상태**: 🔍 분석 완료
|
||||
|
||||
---
|
||||
|
||||
## 📋 목차
|
||||
|
||||
1. [개요](#개요)
|
||||
2. [현재 시스템 분석](#현재-시스템-분석)
|
||||
3. [호환성 분석](#호환성-분석)
|
||||
4. [통합 전략](#통합-전략)
|
||||
5. [마이그레이션 계획](#마이그레이션-계획)
|
||||
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
### 목적
|
||||
|
||||
화면관리의 버튼 컴포넌트에 할당된 기존 제어 시스템을 새로운 노드 기반 제어 시스템으로 전환하기 위한 호환성 분석
|
||||
|
||||
### 비교 대상
|
||||
|
||||
- **현재**: `relationshipId` 기반 제어 시스템
|
||||
- **신규**: `flowId` 기반 노드 제어 시스템
|
||||
|
||||
---
|
||||
|
||||
## 현재 시스템 분석
|
||||
|
||||
### 1. 데이터 구조
|
||||
|
||||
#### ButtonDataflowConfig
|
||||
|
||||
```typescript
|
||||
interface ButtonDataflowConfig {
|
||||
controlMode: "relationship" | "none";
|
||||
|
||||
relationshipConfig?: {
|
||||
relationshipId: string; // 🔑 핵심: 관계 ID
|
||||
relationshipName: string;
|
||||
executionTiming: "before" | "after" | "replace";
|
||||
contextData?: Record<string, any>;
|
||||
};
|
||||
|
||||
controlDataSource?: "form" | "table-selection" | "both";
|
||||
executionOptions?: ExecutionOptions;
|
||||
}
|
||||
```
|
||||
|
||||
#### 관계 데이터 구조
|
||||
|
||||
```typescript
|
||||
{
|
||||
relationshipId: "rel-123",
|
||||
conditions: [
|
||||
{
|
||||
field: "status",
|
||||
operator: "equals",
|
||||
value: "active"
|
||||
}
|
||||
],
|
||||
actionGroups: [
|
||||
{
|
||||
name: "메인 액션",
|
||||
actions: [
|
||||
{
|
||||
type: "database",
|
||||
operation: "INSERT",
|
||||
tableName: "users",
|
||||
fields: [...]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 실행 흐름
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 1. 버튼 클릭 │
|
||||
│ OptimizedButtonComponent.tsx │
|
||||
└─────────────┬───────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 2. executeButtonAction() │
|
||||
│ ImprovedButtonActionExecutor.ts │
|
||||
│ - executionPlan 생성 │
|
||||
│ - before/after/replace 구분 │
|
||||
└─────────────┬───────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 3. executeControls() │
|
||||
│ - relationshipId로 관계 조회 │
|
||||
│ - 조건 검증 │
|
||||
└─────────────┬───────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 4. evaluateConditions() │
|
||||
│ - formData 검증 │
|
||||
│ - selectedRowsData 검증 │
|
||||
└─────────────┬───────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 5. executeDataAction() │
|
||||
│ - INSERT/UPDATE/DELETE 실행 │
|
||||
│ - 순차적 액션 실행 │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 데이터 전달 방식
|
||||
|
||||
#### 입력 데이터
|
||||
|
||||
```typescript
|
||||
{
|
||||
formData: {
|
||||
name: "김철수",
|
||||
email: "test@example.com",
|
||||
status: "active"
|
||||
},
|
||||
selectedRowsData: [
|
||||
{ id: 1, name: "이영희" },
|
||||
{ id: 2, name: "박민수" }
|
||||
],
|
||||
context: {
|
||||
buttonId: "btn-1",
|
||||
screenId: 123,
|
||||
companyCode: "COMPANY_A",
|
||||
userId: "user-1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 액션 실행 시
|
||||
|
||||
```typescript
|
||||
// 각 액션에 전체 데이터 전달
|
||||
executeDataAction(action, {
|
||||
formData,
|
||||
selectedRowsData,
|
||||
context,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 새로운 노드 시스템 분석
|
||||
|
||||
### 1. 데이터 구조
|
||||
|
||||
#### FlowData
|
||||
|
||||
```typescript
|
||||
interface FlowData {
|
||||
flowId: number;
|
||||
flowName: string;
|
||||
flowDescription: string;
|
||||
nodes: FlowNode[]; // 🔑 핵심: 노드 배열
|
||||
edges: FlowEdge[]; // 🔑 핵심: 연결 정보
|
||||
}
|
||||
```
|
||||
|
||||
#### 노드 예시
|
||||
|
||||
```typescript
|
||||
// 소스 노드
|
||||
{
|
||||
id: "source-1",
|
||||
type: "tableSource",
|
||||
data: {
|
||||
tableName: "users",
|
||||
schema: "public",
|
||||
outputFields: [...]
|
||||
}
|
||||
}
|
||||
|
||||
// 조건 노드
|
||||
{
|
||||
id: "condition-1",
|
||||
type: "condition",
|
||||
data: {
|
||||
conditions: [{
|
||||
field: "status",
|
||||
operator: "equals",
|
||||
value: "active"
|
||||
}],
|
||||
logic: "AND"
|
||||
}
|
||||
}
|
||||
|
||||
// 액션 노드
|
||||
{
|
||||
id: "insert-1",
|
||||
type: "insertAction",
|
||||
data: {
|
||||
targetTable: "users",
|
||||
fieldMappings: [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 연결 예시
|
||||
|
||||
```typescript
|
||||
// 엣지 (노드 간 연결)
|
||||
{
|
||||
id: "edge-1",
|
||||
source: "source-1",
|
||||
target: "condition-1"
|
||||
},
|
||||
{
|
||||
id: "edge-2",
|
||||
source: "condition-1",
|
||||
target: "insert-1",
|
||||
sourceHandle: "true" // TRUE 분기
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 실행 흐름
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 1. 버튼 클릭 │
|
||||
│ FlowEditor 또는 Button Component │
|
||||
└─────────────┬───────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 2. executeFlow() │
|
||||
│ - flowId로 플로우 조회 │
|
||||
│ - nodes + edges 로드 │
|
||||
└─────────────┬───────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 3. topologicalSort() │
|
||||
│ - 노드 의존성 분석 │
|
||||
│ - 실행 순서 결정 │
|
||||
│ Result: [["source"], ["insert", "update"]] │
|
||||
└─────────────┬───────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 4. executeLevel() │
|
||||
│ - 같은 레벨 노드 병렬 실행 │
|
||||
│ - Promise.allSettled 사용 │
|
||||
└─────────────┬───────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 5. executeNode() │
|
||||
│ - 부모 노드 상태 확인 │
|
||||
│ - 실패 시 스킵 │
|
||||
└─────────────┬───────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 6. executeActionWithTransaction() │
|
||||
│ - 독립 트랜잭션 시작 │
|
||||
│ - 액션 실행 │
|
||||
│ - 성공 시 커밋, 실패 시 롤백 │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 데이터 전달 방식
|
||||
|
||||
#### ExecutionContext
|
||||
|
||||
```typescript
|
||||
{
|
||||
sourceData: [
|
||||
{ id: 1, name: "김철수", status: "active" },
|
||||
{ id: 2, name: "이영희", status: "inactive" }
|
||||
],
|
||||
nodeResults: Map<string, NodeResult> {
|
||||
"source-1" => { status: "success", data: [...] },
|
||||
"condition-1" => { status: "success", data: true },
|
||||
"insert-1" => { status: "success", data: { insertedCount: 1 } }
|
||||
},
|
||||
executionOrder: ["source-1", "condition-1", "insert-1"]
|
||||
}
|
||||
```
|
||||
|
||||
#### 노드 실행 시
|
||||
|
||||
```typescript
|
||||
// 부모 노드 결과 전달
|
||||
const inputData = prepareInputData(node, parents, context);
|
||||
|
||||
// 부모가 하나면 부모의 결과 데이터
|
||||
// 부모가 여러 개면 모든 부모의 데이터 병합
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 호환성 분석
|
||||
|
||||
### ✅ 호환 가능한 부분
|
||||
|
||||
#### 1. 조건 검증
|
||||
|
||||
**현재**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
field: "status",
|
||||
operator: "equals",
|
||||
value: "active"
|
||||
}
|
||||
```
|
||||
|
||||
**신규**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "condition",
|
||||
data: {
|
||||
conditions: [
|
||||
{
|
||||
field: "status",
|
||||
operator: "equals",
|
||||
value: "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**결론**: ✅ **조건 구조가 거의 동일** → 마이그레이션 쉬움
|
||||
|
||||
---
|
||||
|
||||
#### 2. 액션 실행
|
||||
|
||||
**현재**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "database",
|
||||
operation: "INSERT",
|
||||
tableName: "users",
|
||||
fields: [
|
||||
{ name: "name", value: "김철수" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**신규**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "insertAction",
|
||||
data: {
|
||||
targetTable: "users",
|
||||
fieldMappings: [
|
||||
{ sourceField: "name", targetField: "name" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**결론**: ✅ **액션 개념이 동일** → 필드명만 변환하면 됨
|
||||
|
||||
---
|
||||
|
||||
#### 3. 데이터 소스
|
||||
|
||||
**현재**:
|
||||
|
||||
```typescript
|
||||
controlDataSource: "form" | "table-selection" | "both";
|
||||
```
|
||||
|
||||
**신규**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "tableSource", // 테이블 선택 데이터
|
||||
// 또는
|
||||
type: "manualInput", // 폼 데이터
|
||||
}
|
||||
```
|
||||
|
||||
**결론**: ✅ **소스 타입 매핑 가능**
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ 차이점 및 주의사항
|
||||
|
||||
#### 1. 실행 타이밍
|
||||
|
||||
**현재**:
|
||||
|
||||
```typescript
|
||||
executionTiming: "before" | "after" | "replace";
|
||||
```
|
||||
|
||||
**신규**:
|
||||
|
||||
```
|
||||
노드 그래프 자체가 실행 순서를 정의
|
||||
타이밍은 노드 연결로 표현됨
|
||||
```
|
||||
|
||||
**문제점**:
|
||||
|
||||
- `before/after` 개념이 노드에 없음
|
||||
- 버튼의 기본 액션과 제어를 어떻게 조합할지?
|
||||
|
||||
**해결 방안**:
|
||||
|
||||
```
|
||||
Option A: 버튼 액션을 노드로 표현
|
||||
Button → [Before Nodes] → [Button Action Node] → [After Nodes]
|
||||
|
||||
Option B: 실행 시점 지정
|
||||
flowConfig: {
|
||||
flowId: 123,
|
||||
timing: "before" | "after" | "replace"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2. ActionGroups vs 병렬 실행
|
||||
|
||||
**현재**:
|
||||
|
||||
```typescript
|
||||
actionGroups: [
|
||||
{
|
||||
name: "그룹1",
|
||||
actions: [action1, action2], // 순차 실행
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
**신규**:
|
||||
|
||||
```
|
||||
소스
|
||||
↓
|
||||
├─→ INSERT (병렬)
|
||||
├─→ UPDATE (병렬)
|
||||
└─→ DELETE (병렬)
|
||||
```
|
||||
|
||||
**문제점**:
|
||||
|
||||
- 현재는 "그룹 내 순차, 그룹 간 조건부"
|
||||
- 신규는 "레벨별 병렬, 연쇄 중단"
|
||||
|
||||
**해결 방안**:
|
||||
|
||||
```
|
||||
노드 연결로 순차/병렬 표현:
|
||||
|
||||
순차: INSERT → UPDATE → DELETE
|
||||
병렬: Source → INSERT
|
||||
→ UPDATE
|
||||
→ DELETE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 3. 데이터 전달 방식
|
||||
|
||||
**현재**:
|
||||
|
||||
```typescript
|
||||
// 모든 액션에 동일한 데이터 전달
|
||||
executeDataAction(action, {
|
||||
formData,
|
||||
selectedRowsData,
|
||||
context,
|
||||
});
|
||||
```
|
||||
|
||||
**신규**:
|
||||
|
||||
```typescript
|
||||
// 부모 노드 결과를 자식에게 전달
|
||||
const inputData = parentResult.data || sourceData;
|
||||
```
|
||||
|
||||
**문제점**:
|
||||
|
||||
- 현재는 "원본 데이터 공유"
|
||||
- 신규는 "결과 데이터 체이닝"
|
||||
|
||||
**해결 방안**:
|
||||
|
||||
```typescript
|
||||
// 버튼 실행 시 초기 데이터 설정
|
||||
context.sourceData = {
|
||||
formData,
|
||||
selectedRowsData,
|
||||
};
|
||||
|
||||
// 각 노드는 필요에 따라 선택
|
||||
- formData 사용
|
||||
- 부모 결과 사용
|
||||
- 둘 다 사용
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 4. 컨텍스트 정보
|
||||
|
||||
**현재**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
buttonId: "btn-1",
|
||||
screenId: 123,
|
||||
companyCode: "COMPANY_A",
|
||||
userId: "user-1"
|
||||
}
|
||||
```
|
||||
|
||||
**신규**:
|
||||
|
||||
```typescript
|
||||
// ExecutionContext에 추가 필요
|
||||
{
|
||||
sourceData: [...],
|
||||
nodeResults: Map(),
|
||||
// 🆕 추가 필요
|
||||
buttonContext?: {
|
||||
buttonId: string,
|
||||
screenId: number,
|
||||
companyCode: string,
|
||||
userId: string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**결론**: ✅ **컨텍스트 확장 가능**
|
||||
|
||||
---
|
||||
|
||||
## 통합 전략
|
||||
|
||||
### 전략 1: 하이브리드 방식 (권장 ⭐⭐⭐)
|
||||
|
||||
#### 개념
|
||||
|
||||
버튼 설정에서 `relationshipId` 대신 `flowId`를 저장하고, 기존 타이밍 개념 유지
|
||||
|
||||
#### 버튼 설정
|
||||
|
||||
```typescript
|
||||
interface ButtonDataflowConfig {
|
||||
controlMode: "flow"; // 🆕 신규 모드
|
||||
|
||||
flowConfig?: {
|
||||
flowId: number; // 🔑 노드 플로우 ID
|
||||
flowName: string;
|
||||
executionTiming: "before" | "after" | "replace"; // 기존 유지
|
||||
contextData?: Record<string, any>;
|
||||
};
|
||||
|
||||
controlDataSource?: "form" | "table-selection" | "both";
|
||||
}
|
||||
```
|
||||
|
||||
#### 실행 로직
|
||||
|
||||
```typescript
|
||||
async function executeButtonWithFlow(
|
||||
buttonConfig: ButtonDataflowConfig,
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
) {
|
||||
const { flowConfig } = buttonConfig;
|
||||
|
||||
// 1. 플로우 조회
|
||||
const flow = await getNodeFlow(flowConfig.flowId);
|
||||
|
||||
// 2. 초기 데이터 준비
|
||||
const executionContext: ExecutionContext = {
|
||||
sourceData: prepareSourceData(formData, context),
|
||||
nodeResults: new Map(),
|
||||
executionOrder: [],
|
||||
buttonContext: {
|
||||
// 🆕 버튼 컨텍스트 추가
|
||||
buttonId: context.buttonId,
|
||||
screenId: context.screenId,
|
||||
companyCode: context.companyCode,
|
||||
userId: context.userId,
|
||||
},
|
||||
};
|
||||
|
||||
// 3. 타이밍에 따라 실행
|
||||
switch (flowConfig.executionTiming) {
|
||||
case "before":
|
||||
await executeFlow(flow, executionContext);
|
||||
await executeOriginalButtonAction(buttonConfig, context);
|
||||
break;
|
||||
|
||||
case "after":
|
||||
await executeOriginalButtonAction(buttonConfig, context);
|
||||
await executeFlow(flow, executionContext);
|
||||
break;
|
||||
|
||||
case "replace":
|
||||
await executeFlow(flow, executionContext);
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 소스 데이터 준비
|
||||
|
||||
```typescript
|
||||
function prepareSourceData(
|
||||
formData: Record<string, any>,
|
||||
context: ButtonExecutionContext
|
||||
): any[] {
|
||||
const { controlDataSource, selectedRowsData } = context;
|
||||
|
||||
switch (controlDataSource) {
|
||||
case "form":
|
||||
return [formData]; // 폼 데이터를 배열로
|
||||
|
||||
case "table-selection":
|
||||
return selectedRowsData || []; // 테이블 선택 데이터
|
||||
|
||||
case "both":
|
||||
return [
|
||||
{ source: "form", data: formData },
|
||||
{ source: "table", data: selectedRowsData },
|
||||
];
|
||||
|
||||
default:
|
||||
return [formData];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 전략 2: 완전 전환 방식
|
||||
|
||||
#### 개념
|
||||
|
||||
버튼 액션 자체를 노드로 표현 (버튼 = 플로우 트리거)
|
||||
|
||||
#### 플로우 구조
|
||||
|
||||
```
|
||||
ManualInput (formData)
|
||||
↓
|
||||
Condition (status == "active")
|
||||
↓
|
||||
┌─┴─┐
|
||||
TRUE FALSE
|
||||
↓ ↓
|
||||
INSERT CANCEL
|
||||
↓
|
||||
ButtonAction (원래 버튼 액션)
|
||||
```
|
||||
|
||||
#### 장점
|
||||
|
||||
- ✅ 시스템 단순화 (노드만 존재)
|
||||
- ✅ 시각적으로 명확
|
||||
- ✅ 유연한 워크플로우
|
||||
|
||||
#### 단점
|
||||
|
||||
- ⚠️ 기존 버튼 개념 변경
|
||||
- ⚠️ 마이그레이션 복잡
|
||||
- ⚠️ UI 학습 곡선
|
||||
|
||||
---
|
||||
|
||||
## 마이그레이션 계획
|
||||
|
||||
### Phase 1: 하이브리드 지원
|
||||
|
||||
#### 목표
|
||||
|
||||
기존 `relationshipId` 방식과 새로운 `flowId` 방식 모두 지원
|
||||
|
||||
#### 작업
|
||||
|
||||
1. **ButtonDataflowConfig 확장**
|
||||
|
||||
```typescript
|
||||
interface ButtonDataflowConfig {
|
||||
controlMode: "relationship" | "flow" | "none";
|
||||
|
||||
// 기존 (하위 호환)
|
||||
relationshipConfig?: {
|
||||
relationshipId: string;
|
||||
executionTiming: "before" | "after" | "replace";
|
||||
};
|
||||
|
||||
// 🆕 신규
|
||||
flowConfig?: {
|
||||
flowId: number;
|
||||
executionTiming: "before" | "after" | "replace";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
2. **실행 로직 분기**
|
||||
|
||||
```typescript
|
||||
if (buttonConfig.controlMode === "flow") {
|
||||
await executeButtonWithFlow(buttonConfig, formData, context);
|
||||
} else if (buttonConfig.controlMode === "relationship") {
|
||||
await executeButtonWithRelationship(buttonConfig, formData, context);
|
||||
}
|
||||
```
|
||||
|
||||
3. **UI 업데이트**
|
||||
|
||||
- 버튼 설정에 "제어 방식 선택" 추가
|
||||
- "기존 관계" vs "노드 플로우" 선택 가능
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: 마이그레이션 도구
|
||||
|
||||
#### 관계 → 플로우 변환기
|
||||
|
||||
```typescript
|
||||
async function migrateRelationshipToFlow(
|
||||
relationshipId: string
|
||||
): Promise<number> {
|
||||
// 1. 기존 관계 조회
|
||||
const relationship = await getRelationship(relationshipId);
|
||||
|
||||
// 2. 노드 생성
|
||||
const nodes: FlowNode[] = [];
|
||||
const edges: FlowEdge[] = [];
|
||||
|
||||
// 소스 노드 (formData 또는 table)
|
||||
const sourceNode = {
|
||||
id: "source-1",
|
||||
type: "manualInput",
|
||||
data: { fields: extractFields(relationship) },
|
||||
};
|
||||
nodes.push(sourceNode);
|
||||
|
||||
// 조건 노드
|
||||
if (relationship.conditions.length > 0) {
|
||||
const conditionNode = {
|
||||
id: "condition-1",
|
||||
type: "condition",
|
||||
data: {
|
||||
conditions: relationship.conditions,
|
||||
logic: relationship.logic || "AND",
|
||||
},
|
||||
};
|
||||
nodes.push(conditionNode);
|
||||
edges.push({ id: "e1", source: "source-1", target: "condition-1" });
|
||||
}
|
||||
|
||||
// 액션 노드들
|
||||
let lastNodeId =
|
||||
relationship.conditions.length > 0 ? "condition-1" : "source-1";
|
||||
|
||||
relationship.actionGroups.forEach((group, groupIdx) => {
|
||||
group.actions.forEach((action, actionIdx) => {
|
||||
const actionNodeId = `action-${groupIdx}-${actionIdx}`;
|
||||
const actionNode = convertActionToNode(action, actionNodeId);
|
||||
nodes.push(actionNode);
|
||||
|
||||
edges.push({
|
||||
id: `e-${actionNodeId}`,
|
||||
source: lastNodeId,
|
||||
target: actionNodeId,
|
||||
});
|
||||
|
||||
// 순차 실행인 경우
|
||||
if (group.sequential) {
|
||||
lastNodeId = actionNodeId;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 3. 플로우 저장
|
||||
const flowData = {
|
||||
flowName: `Migrated: ${relationship.name}`,
|
||||
flowDescription: `Migrated from relationship ${relationshipId}`,
|
||||
flowData: JSON.stringify({ nodes, edges }),
|
||||
};
|
||||
|
||||
const { flowId } = await createNodeFlow(flowData);
|
||||
|
||||
// 4. 버튼 설정 업데이트
|
||||
await updateButtonConfig(relationshipId, {
|
||||
controlMode: "flow",
|
||||
flowConfig: {
|
||||
flowId,
|
||||
executionTiming: relationship.timing || "before",
|
||||
},
|
||||
});
|
||||
|
||||
return flowId;
|
||||
}
|
||||
```
|
||||
|
||||
#### 액션 변환 로직
|
||||
|
||||
```typescript
|
||||
function convertActionToNode(action: DataflowAction, nodeId: string): FlowNode {
|
||||
switch (action.operation) {
|
||||
case "INSERT":
|
||||
return {
|
||||
id: nodeId,
|
||||
type: "insertAction",
|
||||
data: {
|
||||
targetTable: action.tableName,
|
||||
fieldMappings: action.fields.map((f) => ({
|
||||
sourceField: f.name,
|
||||
targetField: f.name,
|
||||
staticValue: f.type === "static" ? f.value : undefined,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
case "UPDATE":
|
||||
return {
|
||||
id: nodeId,
|
||||
type: "updateAction",
|
||||
data: {
|
||||
targetTable: action.tableName,
|
||||
whereConditions: action.conditions,
|
||||
fieldMappings: action.fields.map((f) => ({
|
||||
sourceField: f.name,
|
||||
targetField: f.name,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
case "DELETE":
|
||||
return {
|
||||
id: nodeId,
|
||||
type: "deleteAction",
|
||||
data: {
|
||||
targetTable: action.tableName,
|
||||
whereConditions: action.conditions,
|
||||
},
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported operation: ${action.operation}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: 완전 전환
|
||||
|
||||
#### 목표
|
||||
|
||||
모든 버튼이 노드 플로우 방식 사용
|
||||
|
||||
#### 작업
|
||||
|
||||
1. **마이그레이션 스크립트 실행**
|
||||
|
||||
```sql
|
||||
-- 모든 관계를 플로우로 변환
|
||||
SELECT migrate_all_relationships_to_flows();
|
||||
```
|
||||
|
||||
2. **UI에서 관계 모드 제거**
|
||||
|
||||
```typescript
|
||||
// controlMode에서 "relationship" 제거
|
||||
type ControlMode = "flow" | "none";
|
||||
```
|
||||
|
||||
3. **레거시 코드 정리**
|
||||
|
||||
- `executeButtonWithRelationship()` 제거
|
||||
- `RelationshipService` 제거 (또는 읽기 전용)
|
||||
|
||||
---
|
||||
|
||||
## 결론
|
||||
|
||||
### ✅ 호환 가능
|
||||
|
||||
노드 시스템과 버튼 제어 시스템은 **충분히 호환 가능**합니다!
|
||||
|
||||
### 🎯 권장 방안
|
||||
|
||||
**하이브리드 방식 (전략 1)**으로 점진적 마이그레이션
|
||||
|
||||
#### 이유
|
||||
|
||||
1. ✅ **기존 시스템 유지** - 서비스 중단 없음
|
||||
2. ✅ **점진적 전환** - 리스크 최소화
|
||||
3. ✅ **유연성** - 두 방식 모두 활용 가능
|
||||
4. ✅ **학습 곡선** - 사용자가 천천히 적응
|
||||
|
||||
### 📋 다음 단계
|
||||
|
||||
1. **Phase 1 구현** (예상: 2일)
|
||||
|
||||
- `ButtonDataflowConfig` 확장
|
||||
- `executeButtonWithFlow()` 구현
|
||||
- UI 선택 옵션 추가
|
||||
|
||||
2. **Phase 2 도구 개발** (예상: 1일)
|
||||
|
||||
- 마이그레이션 스크립트
|
||||
- 자동 변환 로직
|
||||
|
||||
3. **Phase 3 전환** (예상: 1일)
|
||||
- 데이터 마이그레이션
|
||||
- 레거시 제거
|
||||
|
||||
### 총 소요 시간
|
||||
|
||||
**약 4일**
|
||||
|
||||
---
|
||||
|
||||
**참고 문서**:
|
||||
|
||||
- [노드\_실행\_엔진\_설계.md](./노드_실행_엔진_설계.md)
|
||||
- [노드\_기반\_제어\_시스템\_개선\_계획.md](./노드_기반_제어_시스템_개선_계획.md)
|
||||
617
docs/노드_실행_엔진_설계.md
Normal file
617
docs/노드_실행_엔진_설계.md
Normal file
@@ -0,0 +1,617 @@
|
||||
# 노드 실행 엔진 설계
|
||||
|
||||
**작성일**: 2025-01-02
|
||||
**버전**: 1.0
|
||||
**상태**: ✅ 확정
|
||||
|
||||
---
|
||||
|
||||
## 📋 목차
|
||||
|
||||
1. [개요](#개요)
|
||||
2. [실행 방식](#실행-방식)
|
||||
3. [데이터 흐름](#데이터-흐름)
|
||||
4. [오류 처리](#오류-처리)
|
||||
5. [구현 계획](#구현-계획)
|
||||
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
### 목적
|
||||
|
||||
노드 기반 데이터 플로우의 실행 엔진을 설계하여:
|
||||
|
||||
- 효율적인 병렬 처리
|
||||
- 안정적인 오류 처리
|
||||
- 명확한 데이터 흐름
|
||||
|
||||
### 핵심 원칙
|
||||
|
||||
1. **독립적 트랜잭션**: 각 액션 노드는 독립적인 트랜잭션
|
||||
2. **부분 실패 허용**: 일부 실패해도 성공한 노드는 커밋
|
||||
3. **연쇄 중단**: 부모 노드 실패 시 자식 노드 스킵
|
||||
4. **병렬 실행**: 의존성 없는 노드는 병렬 실행
|
||||
|
||||
---
|
||||
|
||||
## 실행 방식
|
||||
|
||||
### 1. 기본 구조
|
||||
|
||||
```typescript
|
||||
interface ExecutionContext {
|
||||
sourceData: any[]; // 원본 데이터
|
||||
nodeResults: Map<string, NodeResult>; // 각 노드 실행 결과
|
||||
executionOrder: string[]; // 실행 순서
|
||||
}
|
||||
|
||||
interface NodeResult {
|
||||
nodeId: string;
|
||||
status: "pending" | "success" | "failed" | "skipped";
|
||||
data?: any;
|
||||
error?: Error;
|
||||
startTime: number;
|
||||
endTime?: number;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 실행 단계
|
||||
|
||||
#### Step 1: 위상 정렬 (Topological Sort)
|
||||
|
||||
노드 간 의존성을 파악하여 실행 순서 결정
|
||||
|
||||
```typescript
|
||||
function topologicalSort(nodes: FlowNode[], edges: FlowEdge[]): string[][] {
|
||||
// DAG(Directed Acyclic Graph) 순회
|
||||
// 같은 레벨의 노드들은 배열로 그룹화
|
||||
|
||||
return [
|
||||
["tableSource-1"], // Level 0: 소스
|
||||
["insert-1", "update-1", "delete-1"], // Level 1: 병렬 실행 가능
|
||||
["update-2"], // Level 2: insert-1에 의존
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2: 레벨별 실행
|
||||
|
||||
```typescript
|
||||
async function executeFlow(
|
||||
nodes: FlowNode[],
|
||||
edges: FlowEdge[]
|
||||
): Promise<ExecutionResult> {
|
||||
const levels = topologicalSort(nodes, edges);
|
||||
const context: ExecutionContext = {
|
||||
sourceData: [],
|
||||
nodeResults: new Map(),
|
||||
executionOrder: [],
|
||||
};
|
||||
|
||||
for (const level of levels) {
|
||||
// 같은 레벨의 노드들은 병렬 실행
|
||||
await executeLevel(level, nodes, context);
|
||||
}
|
||||
|
||||
return generateExecutionReport(context);
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 3: 레벨 내 병렬 실행
|
||||
|
||||
```typescript
|
||||
async function executeLevel(
|
||||
nodeIds: string[],
|
||||
nodes: FlowNode[],
|
||||
context: ExecutionContext
|
||||
): Promise<void> {
|
||||
// Promise.allSettled로 병렬 실행
|
||||
const results = await Promise.allSettled(
|
||||
nodeIds.map((nodeId) => executeNode(nodeId, nodes, context))
|
||||
);
|
||||
|
||||
// 결과 저장
|
||||
results.forEach((result, index) => {
|
||||
const nodeId = nodeIds[index];
|
||||
if (result.status === "fulfilled") {
|
||||
context.nodeResults.set(nodeId, result.value);
|
||||
} else {
|
||||
context.nodeResults.set(nodeId, {
|
||||
nodeId,
|
||||
status: "failed",
|
||||
error: result.reason,
|
||||
startTime: Date.now(),
|
||||
endTime: Date.now(),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 데이터 흐름
|
||||
|
||||
### 1. 소스 노드 실행
|
||||
|
||||
```typescript
|
||||
async function executeSourceNode(node: TableSourceNode): Promise<any[]> {
|
||||
const { tableName, schema, whereConditions } = node.data;
|
||||
|
||||
// 데이터베이스 쿼리 실행
|
||||
const query = buildSelectQuery(tableName, schema, whereConditions);
|
||||
const data = await executeQuery(query);
|
||||
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
**결과 예시**:
|
||||
|
||||
```json
|
||||
[
|
||||
{ "id": 1, "name": "김철수", "age": 30 },
|
||||
{ "id": 2, "name": "이영희", "age": 25 },
|
||||
{ "id": 3, "name": "박민수", "age": 35 }
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 액션 노드 실행
|
||||
|
||||
#### 데이터 전달 방식
|
||||
|
||||
```typescript
|
||||
async function executeNode(
|
||||
nodeId: string,
|
||||
nodes: FlowNode[],
|
||||
context: ExecutionContext
|
||||
): Promise<NodeResult> {
|
||||
const node = nodes.find((n) => n.id === nodeId);
|
||||
const parents = getParentNodes(nodeId, edges);
|
||||
|
||||
// 1️⃣ 부모 노드 상태 확인
|
||||
const parentFailed = parents.some((p) => {
|
||||
const parentResult = context.nodeResults.get(p.id);
|
||||
return parentResult?.status === "failed";
|
||||
});
|
||||
|
||||
if (parentFailed) {
|
||||
return {
|
||||
nodeId,
|
||||
status: "skipped",
|
||||
error: new Error("Parent node failed"),
|
||||
startTime: Date.now(),
|
||||
endTime: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
// 2️⃣ 입력 데이터 준비
|
||||
const inputData = prepareInputData(node, parents, context);
|
||||
|
||||
// 3️⃣ 액션 실행 (독립 트랜잭션)
|
||||
return await executeActionWithTransaction(node, inputData);
|
||||
}
|
||||
```
|
||||
|
||||
#### 입력 데이터 준비
|
||||
|
||||
```typescript
|
||||
function prepareInputData(
|
||||
node: FlowNode,
|
||||
parents: FlowNode[],
|
||||
context: ExecutionContext
|
||||
): any {
|
||||
if (parents.length === 0) {
|
||||
// 소스 노드
|
||||
return null;
|
||||
} else if (parents.length === 1) {
|
||||
// 단일 부모: 부모의 결과 데이터 전달
|
||||
const parentResult = context.nodeResults.get(parents[0].id);
|
||||
return parentResult?.data || context.sourceData;
|
||||
} else {
|
||||
// 다중 부모: 모든 부모의 데이터 병합
|
||||
return parents.map((p) => {
|
||||
const result = context.nodeResults.get(p.id);
|
||||
return result?.data || context.sourceData;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 병렬 실행 예시
|
||||
|
||||
```
|
||||
TableSource
|
||||
(100개 레코드)
|
||||
↓
|
||||
┌──────┼──────┐
|
||||
↓ ↓ ↓
|
||||
INSERT UPDATE DELETE
|
||||
(독립) (독립) (독립)
|
||||
```
|
||||
|
||||
**실행 과정**:
|
||||
|
||||
```typescript
|
||||
// 1. TableSource 실행
|
||||
const sourceData = await executeTableSource();
|
||||
// → [100개 레코드]
|
||||
|
||||
// 2. 병렬 실행 (Promise.allSettled)
|
||||
const results = await Promise.allSettled([
|
||||
executeInsertAction(insertNode, sourceData),
|
||||
executeUpdateAction(updateNode, sourceData),
|
||||
executeDeleteAction(deleteNode, sourceData),
|
||||
]);
|
||||
|
||||
// 3. 각 액션은 독립 트랜잭션
|
||||
// - INSERT 실패 → INSERT만 롤백
|
||||
// - UPDATE 성공 → UPDATE 커밋
|
||||
// - DELETE 성공 → DELETE 커밋
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 연쇄 실행 예시
|
||||
|
||||
```
|
||||
TableSource
|
||||
↓
|
||||
INSERT
|
||||
❌ (실패)
|
||||
↓
|
||||
UPDATE-2
|
||||
⏭️ (스킵)
|
||||
```
|
||||
|
||||
**실행 과정**:
|
||||
|
||||
```typescript
|
||||
// 1. TableSource 실행
|
||||
const sourceData = await executeTableSource();
|
||||
// → 성공 ✅
|
||||
|
||||
// 2. INSERT 실행
|
||||
const insertResult = await executeInsertAction(insertNode, sourceData);
|
||||
// → 실패 ❌ (롤백됨)
|
||||
|
||||
// 3. UPDATE-2 실행 시도
|
||||
const parentFailed = insertResult.status === "failed";
|
||||
if (parentFailed) {
|
||||
return {
|
||||
status: "skipped",
|
||||
reason: "Parent INSERT failed",
|
||||
};
|
||||
// → 스킬 ⏭️
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 오류 처리
|
||||
|
||||
### 1. 독립 트랜잭션
|
||||
|
||||
각 액션 노드는 자체 트랜잭션을 가짐
|
||||
|
||||
```typescript
|
||||
async function executeActionWithTransaction(
|
||||
node: FlowNode,
|
||||
inputData: any
|
||||
): Promise<NodeResult> {
|
||||
// 트랜잭션 시작
|
||||
const transaction = await db.beginTransaction();
|
||||
|
||||
try {
|
||||
const result = await performAction(node, inputData, transaction);
|
||||
|
||||
// 성공 시 커밋
|
||||
await transaction.commit();
|
||||
|
||||
return {
|
||||
nodeId: node.id,
|
||||
status: "success",
|
||||
data: result,
|
||||
startTime: Date.now(),
|
||||
endTime: Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
// 실패 시 롤백
|
||||
await transaction.rollback();
|
||||
|
||||
return {
|
||||
nodeId: node.id,
|
||||
status: "failed",
|
||||
error: error,
|
||||
startTime: Date.now(),
|
||||
endTime: Date.now(),
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 부분 실패 허용
|
||||
|
||||
```typescript
|
||||
// Promise.allSettled 사용
|
||||
const results = await Promise.allSettled([action1(), action2(), action3()]);
|
||||
|
||||
// 결과 수집
|
||||
const summary = {
|
||||
total: results.length,
|
||||
success: results.filter((r) => r.status === "fulfilled").length,
|
||||
failed: results.filter((r) => r.status === "rejected").length,
|
||||
details: results,
|
||||
};
|
||||
```
|
||||
|
||||
**예시 결과**:
|
||||
|
||||
```json
|
||||
{
|
||||
"total": 3,
|
||||
"success": 2,
|
||||
"failed": 1,
|
||||
"details": [
|
||||
{ "status": "rejected", "reason": "Duplicate key error" },
|
||||
{ "status": "fulfilled", "value": { "updatedCount": 100 } },
|
||||
{ "status": "fulfilled", "value": { "deletedCount": 50 } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 연쇄 중단
|
||||
|
||||
부모 노드 실패 시 자식 노드 자동 스킵
|
||||
|
||||
```typescript
|
||||
function shouldSkipNode(node: FlowNode, context: ExecutionContext): boolean {
|
||||
const parents = getParentNodes(node.id);
|
||||
|
||||
return parents.some((parent) => {
|
||||
const parentResult = context.nodeResults.get(parent.id);
|
||||
return parentResult?.status === "failed";
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 오류 메시지
|
||||
|
||||
```typescript
|
||||
interface ExecutionError {
|
||||
nodeId: string;
|
||||
nodeName: string;
|
||||
errorType: "validation" | "execution" | "connection" | "timeout";
|
||||
message: string;
|
||||
details?: any;
|
||||
timestamp: number;
|
||||
}
|
||||
```
|
||||
|
||||
**오류 메시지 예시**:
|
||||
|
||||
```json
|
||||
{
|
||||
"nodeId": "insert-1",
|
||||
"nodeName": "INSERT 액션",
|
||||
"errorType": "execution",
|
||||
"message": "Duplicate key error: 'email' already exists",
|
||||
"details": {
|
||||
"table": "users",
|
||||
"constraint": "users_email_unique",
|
||||
"value": "test@example.com"
|
||||
},
|
||||
"timestamp": 1704182400000
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 구현 계획
|
||||
|
||||
### Phase 1: 기본 실행 엔진 (우선순위: 높음)
|
||||
|
||||
**작업 항목**:
|
||||
|
||||
1. ✅ 위상 정렬 알고리즘 구현
|
||||
2. ✅ 레벨별 실행 로직
|
||||
3. ✅ Promise.allSettled 기반 병렬 실행
|
||||
4. ✅ 독립 트랜잭션 처리
|
||||
5. ✅ 연쇄 중단 로직
|
||||
|
||||
**예상 시간**: 1일
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: 소스 노드 실행 (우선순위: 높음)
|
||||
|
||||
**작업 항목**:
|
||||
|
||||
1. ✅ TableSource 실행기
|
||||
2. ✅ ExternalDBSource 실행기
|
||||
3. ✅ RestAPISource 실행기
|
||||
4. ✅ 데이터 캐싱
|
||||
|
||||
**예상 시간**: 1일
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: 액션 노드 실행 (우선순위: 높음)
|
||||
|
||||
**작업 항목**:
|
||||
|
||||
1. ✅ INSERT 액션 실행기
|
||||
2. ✅ UPDATE 액션 실행기
|
||||
3. ✅ DELETE 액션 실행기
|
||||
4. ✅ UPSERT 액션 실행기
|
||||
5. ✅ 필드 매핑 적용
|
||||
|
||||
**예상 시간**: 2일
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: 변환 노드 실행 (우선순위: 중간)
|
||||
|
||||
**작업 항목**:
|
||||
|
||||
1. ✅ FieldMapping 실행기
|
||||
2. ✅ DataTransform 실행기
|
||||
3. ✅ Condition 분기 처리
|
||||
|
||||
**예상 시간**: 1일
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: 오류 처리 및 모니터링 (우선순위: 중간)
|
||||
|
||||
**작업 항목**:
|
||||
|
||||
1. ✅ 상세 오류 메시지
|
||||
2. ✅ 실행 결과 리포트
|
||||
3. ✅ 실행 로그 저장
|
||||
4. ✅ 실시간 진행 상태 표시
|
||||
|
||||
**예상 시간**: 1일
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: 최적화 (우선순위: 낮음)
|
||||
|
||||
**작업 항목**:
|
||||
|
||||
1. ⏳ 데이터 스트리밍 (대용량 데이터)
|
||||
2. ⏳ 배치 처리 최적화
|
||||
3. ⏳ 병렬 처리 튜닝
|
||||
4. ⏳ 캐싱 전략
|
||||
|
||||
**예상 시간**: 2일
|
||||
|
||||
---
|
||||
|
||||
## 실행 결과 예시
|
||||
|
||||
### 성공 케이스
|
||||
|
||||
```json
|
||||
{
|
||||
"flowId": "flow-123",
|
||||
"flowName": "사용자 데이터 동기화",
|
||||
"status": "completed",
|
||||
"startTime": "2025-01-02T10:00:00Z",
|
||||
"endTime": "2025-01-02T10:00:05Z",
|
||||
"duration": 5000,
|
||||
"nodes": [
|
||||
{
|
||||
"nodeId": "source-1",
|
||||
"nodeName": "TableSource",
|
||||
"status": "success",
|
||||
"recordCount": 100,
|
||||
"duration": 500
|
||||
},
|
||||
{
|
||||
"nodeId": "insert-1",
|
||||
"nodeName": "INSERT",
|
||||
"status": "success",
|
||||
"insertedCount": 100,
|
||||
"duration": 2000
|
||||
},
|
||||
{
|
||||
"nodeId": "update-1",
|
||||
"nodeName": "UPDATE",
|
||||
"status": "success",
|
||||
"updatedCount": 80,
|
||||
"duration": 1500
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 3,
|
||||
"success": 3,
|
||||
"failed": 0,
|
||||
"skipped": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 부분 실패 케이스
|
||||
|
||||
```json
|
||||
{
|
||||
"flowId": "flow-124",
|
||||
"flowName": "데이터 처리",
|
||||
"status": "partial_success",
|
||||
"startTime": "2025-01-02T11:00:00Z",
|
||||
"endTime": "2025-01-02T11:00:08Z",
|
||||
"duration": 8000,
|
||||
"nodes": [
|
||||
{
|
||||
"nodeId": "source-1",
|
||||
"nodeName": "TableSource",
|
||||
"status": "success",
|
||||
"recordCount": 100
|
||||
},
|
||||
{
|
||||
"nodeId": "insert-1",
|
||||
"nodeName": "INSERT",
|
||||
"status": "failed",
|
||||
"error": "Duplicate key error",
|
||||
"details": "email 'test@example.com' already exists"
|
||||
},
|
||||
{
|
||||
"nodeId": "update-2",
|
||||
"nodeName": "UPDATE-2",
|
||||
"status": "skipped",
|
||||
"reason": "Parent INSERT failed"
|
||||
},
|
||||
{
|
||||
"nodeId": "update-1",
|
||||
"nodeName": "UPDATE",
|
||||
"status": "success",
|
||||
"updatedCount": 50
|
||||
},
|
||||
{
|
||||
"nodeId": "delete-1",
|
||||
"nodeName": "DELETE",
|
||||
"status": "success",
|
||||
"deletedCount": 20
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 5,
|
||||
"success": 3,
|
||||
"failed": 1,
|
||||
"skipped": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 다음 단계
|
||||
|
||||
1. ✅ 데이터 처리 방식 확정 (완료)
|
||||
2. ⏳ 실행 엔진 구현 시작
|
||||
3. ⏳ 테스트 케이스 작성
|
||||
4. ⏳ UI에서 실행 결과 표시
|
||||
|
||||
---
|
||||
|
||||
**참고 문서**:
|
||||
|
||||
- [노드*기반*제어*시스템*개선\_계획.md](./노드_기반_제어_시스템_개선_계획.md)
|
||||
- [노드*연결*규칙\_설계.md](./노드_연결_규칙_설계.md)
|
||||
- [노드*구조*개선안.md](./노드_구조_개선안.md)
|
||||
431
docs/노드_연결_규칙_설계.md
Normal file
431
docs/노드_연결_규칙_설계.md
Normal file
@@ -0,0 +1,431 @@
|
||||
# 노드 연결 규칙 설계
|
||||
|
||||
**작성일**: 2025-01-02
|
||||
**버전**: 1.0
|
||||
**상태**: 🔄 설계 중
|
||||
|
||||
---
|
||||
|
||||
## 📋 목차
|
||||
|
||||
1. [개요](#개요)
|
||||
2. [노드 분류](#노드-분류)
|
||||
3. [연결 규칙 매트릭스](#연결-규칙-매트릭스)
|
||||
4. [상세 연결 규칙](#상세-연결-규칙)
|
||||
5. [구현 계획](#구현-계획)
|
||||
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
### 목적
|
||||
|
||||
노드 간 연결 가능 여부를 명확히 정의하여:
|
||||
|
||||
- 사용자의 실수 방지
|
||||
- 논리적으로 올바른 플로우만 생성 가능
|
||||
- 명확한 오류 메시지 제공
|
||||
|
||||
### 기본 원칙
|
||||
|
||||
1. **데이터 흐름 방향**: 소스 → 변환 → 액션
|
||||
2. **타입 안전성**: 출력과 입력 타입이 호환되어야 함
|
||||
3. **논리적 정합성**: 의미 없는 연결 방지
|
||||
|
||||
---
|
||||
|
||||
## 노드 분류
|
||||
|
||||
### 1. 데이터 소스 노드 (Source)
|
||||
|
||||
**역할**: 데이터를 생성하는 시작점
|
||||
|
||||
- `tableSource` - 내부 테이블
|
||||
- `externalDBSource` - 외부 DB
|
||||
- `restAPISource` - REST API
|
||||
|
||||
**특징**:
|
||||
|
||||
- ✅ 출력만 가능 (소스 핸들)
|
||||
- ❌ 입력 불가능
|
||||
- 플로우의 시작점
|
||||
|
||||
---
|
||||
|
||||
### 2. 변환/조건 노드 (Transform)
|
||||
|
||||
**역할**: 데이터를 가공하거나 흐름을 제어
|
||||
|
||||
#### 2.1 데이터 변환
|
||||
|
||||
- `fieldMapping` - 필드 매핑
|
||||
- `dataTransform` - 데이터 변환
|
||||
|
||||
**특징**:
|
||||
|
||||
- ✅ 입력 가능 (타겟 핸들)
|
||||
- ✅ 출력 가능 (소스 핸들)
|
||||
- 중간 파이프라인 역할
|
||||
|
||||
#### 2.2 조건 분기
|
||||
|
||||
- `condition` - 조건 분기
|
||||
|
||||
**특징**:
|
||||
|
||||
- ✅ 입력 가능 (타겟 핸들)
|
||||
- ✅ 출력 가능 (TRUE/FALSE 2개의 소스 핸들)
|
||||
- 흐름을 분기
|
||||
|
||||
---
|
||||
|
||||
### 3. 액션 노드 (Action)
|
||||
|
||||
**역할**: 실제 데이터베이스 작업 수행
|
||||
|
||||
- `insertAction` - INSERT
|
||||
- `updateAction` - UPDATE
|
||||
- `deleteAction` - DELETE
|
||||
- `upsertAction` - UPSERT
|
||||
|
||||
**특징**:
|
||||
|
||||
- ✅ 입력 가능 (타겟 핸들)
|
||||
- ⚠️ 출력 제한적 (성공/실패 결과만)
|
||||
- 플로우의 종착점 또는 중간 액션
|
||||
|
||||
---
|
||||
|
||||
### 4. 유틸리티 노드 (Utility)
|
||||
|
||||
**역할**: 보조적인 기능 제공
|
||||
|
||||
- `log` - 로그 출력
|
||||
- `comment` - 주석
|
||||
|
||||
**특징**:
|
||||
|
||||
- `log`: 입력/출력 모두 가능 (패스스루)
|
||||
- `comment`: 연결 불가능 (독립 노드)
|
||||
|
||||
---
|
||||
|
||||
## 연결 규칙 매트릭스
|
||||
|
||||
### 출력(From) → 입력(To) 연결 가능 여부
|
||||
|
||||
| From ↓ / To → | tableSource | externalDB | restAPI | condition | fieldMapping | dataTransform | insert | update | delete | upsert | log | comment |
|
||||
| ----------------- | ----------- | ---------- | ------- | --------- | ------------ | ------------- | ------ | ------ | ------ | ------ | --- | ------- |
|
||||
| **tableSource** | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **externalDB** | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **restAPI** | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **condition** | ❌ | ❌ | ❌ | ⚠️ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **fieldMapping** | ❌ | ❌ | ❌ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **dataTransform** | ❌ | ❌ | ❌ | ✅ | ✅ | ⚠️ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **insert** | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ✅ | ❌ |
|
||||
| **update** | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ✅ | ❌ |
|
||||
| **delete** | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ✅ | ❌ |
|
||||
| **upsert** | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ✅ | ❌ |
|
||||
| **log** | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **comment** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
**범례**:
|
||||
|
||||
- ✅ 허용
|
||||
- ❌ 금지
|
||||
- ⚠️ 조건부 허용 (경고 메시지와 함께)
|
||||
|
||||
---
|
||||
|
||||
## 상세 연결 규칙
|
||||
|
||||
### 규칙 1: 소스 노드는 입력을 받을 수 없음
|
||||
|
||||
**금지되는 연결**:
|
||||
|
||||
```
|
||||
❌ 어떤 노드 → tableSource
|
||||
❌ 어떤 노드 → externalDBSource
|
||||
❌ 어떤 노드 → restAPISource
|
||||
```
|
||||
|
||||
**이유**: 소스 노드는 데이터의 시작점이므로 외부 입력이 의미 없음
|
||||
|
||||
**오류 메시지**:
|
||||
|
||||
```
|
||||
"소스 노드는 입력을 받을 수 없습니다. 소스 노드는 플로우의 시작점입니다."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 규칙 2: 소스 노드끼리 연결 불가
|
||||
|
||||
**금지되는 연결**:
|
||||
|
||||
```
|
||||
❌ tableSource → externalDBSource
|
||||
❌ restAPISource → tableSource
|
||||
```
|
||||
|
||||
**이유**: 소스 노드는 독립적으로 데이터를 생성하므로 서로 연결 불필요
|
||||
|
||||
**오류 메시지**:
|
||||
|
||||
```
|
||||
"소스 노드끼리는 연결할 수 없습니다. 각 소스는 독립적으로 동작합니다."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 규칙 3: Comment 노드는 연결 불가
|
||||
|
||||
**금지되는 연결**:
|
||||
|
||||
```
|
||||
❌ 어떤 노드 → comment
|
||||
❌ comment → 어떤 노드
|
||||
```
|
||||
|
||||
**이유**: Comment는 설명 전용 노드로 데이터 흐름에 영향을 주지 않음
|
||||
|
||||
**오류 메시지**:
|
||||
|
||||
```
|
||||
"주석 노드는 연결할 수 없습니다. 주석은 플로우 설명 용도로만 사용됩니다."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 규칙 4: 동일한 타입의 변환 노드 연속 연결 경고
|
||||
|
||||
**경고가 필요한 연결**:
|
||||
|
||||
```
|
||||
⚠️ fieldMapping → fieldMapping
|
||||
⚠️ dataTransform → dataTransform
|
||||
⚠️ condition → condition
|
||||
```
|
||||
|
||||
**이유**: 논리적으로 가능하지만 비효율적일 수 있음
|
||||
|
||||
**경고 메시지**:
|
||||
|
||||
```
|
||||
"동일한 타입의 변환 노드가 연속으로 연결되었습니다. 하나의 노드로 통합하는 것이 효율적입니다."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 규칙 5: 액션 노드 연속 연결 경고
|
||||
|
||||
**경고가 필요한 연결**:
|
||||
|
||||
```
|
||||
⚠️ insertAction → updateAction
|
||||
⚠️ updateAction → deleteAction
|
||||
⚠️ deleteAction → insertAction
|
||||
```
|
||||
|
||||
**이유**: 트랜잭션 관리나 성능에 영향을 줄 수 있음
|
||||
|
||||
**경고 메시지**:
|
||||
|
||||
```
|
||||
"액션 노드가 연속으로 연결되었습니다. 트랜잭션과 성능을 고려해주세요."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 규칙 6: 자기 자신에게 연결 금지
|
||||
|
||||
**금지되는 연결**:
|
||||
|
||||
```
|
||||
❌ 모든 노드 → 자기 자신
|
||||
```
|
||||
|
||||
**이유**: 무한 루프 방지
|
||||
|
||||
**오류 메시지**:
|
||||
|
||||
```
|
||||
"노드는 자기 자신에게 연결할 수 없습니다."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 규칙 7: Log 노드는 패스스루
|
||||
|
||||
**허용되는 연결**:
|
||||
|
||||
```
|
||||
✅ 모든 노드 → log → 모든 노드 (소스 제외)
|
||||
```
|
||||
|
||||
**특징**:
|
||||
|
||||
- Log 노드는 데이터를 그대로 전달
|
||||
- 디버깅 및 모니터링 용도
|
||||
- 데이터 흐름에 영향 없음
|
||||
|
||||
---
|
||||
|
||||
## 구현 계획
|
||||
|
||||
### Phase 1: 기본 금지 규칙 (우선순위: 높음)
|
||||
|
||||
**구현 위치**: `frontend/lib/stores/flowEditorStore.ts` - `validateConnection` 함수
|
||||
|
||||
```typescript
|
||||
function validateConnection(
|
||||
connection: Connection,
|
||||
nodes: FlowNode[]
|
||||
): { valid: boolean; error?: string } {
|
||||
const sourceNode = nodes.find((n) => n.id === connection.source);
|
||||
const targetNode = nodes.find((n) => n.id === connection.target);
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return { valid: false, error: "노드를 찾을 수 없습니다" };
|
||||
}
|
||||
|
||||
// 규칙 1: 소스 노드는 입력을 받을 수 없음
|
||||
if (isSourceNode(targetNode.type)) {
|
||||
return {
|
||||
valid: false,
|
||||
error:
|
||||
"소스 노드는 입력을 받을 수 없습니다. 소스 노드는 플로우의 시작점입니다.",
|
||||
};
|
||||
}
|
||||
|
||||
// 규칙 2: 소스 노드끼리 연결 불가
|
||||
if (isSourceNode(sourceNode.type) && isSourceNode(targetNode.type)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "소스 노드끼리는 연결할 수 없습니다.",
|
||||
};
|
||||
}
|
||||
|
||||
// 규칙 3: Comment 노드는 연결 불가
|
||||
if (sourceNode.type === "comment" || targetNode.type === "comment") {
|
||||
return {
|
||||
valid: false,
|
||||
error: "주석 노드는 연결할 수 없습니다.",
|
||||
};
|
||||
}
|
||||
|
||||
// 규칙 6: 자기 자신에게 연결 금지
|
||||
if (connection.source === connection.target) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "노드는 자기 자신에게 연결할 수 없습니다.",
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
```
|
||||
|
||||
**예상 작업 시간**: 30분
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: 경고 규칙 (우선순위: 중간)
|
||||
|
||||
**구현 방법**: 연결은 허용하되 경고 표시
|
||||
|
||||
```typescript
|
||||
function getConnectionWarning(
|
||||
connection: Connection,
|
||||
nodes: FlowNode[]
|
||||
): string | null {
|
||||
const sourceNode = nodes.find((n) => n.id === connection.source);
|
||||
const targetNode = nodes.find((n) => n.id === connection.target);
|
||||
|
||||
if (!sourceNode || !targetNode) return null;
|
||||
|
||||
// 규칙 4: 동일한 타입의 변환 노드 연속 연결
|
||||
if (sourceNode.type === targetNode.type && isTransformNode(sourceNode.type)) {
|
||||
return "동일한 타입의 변환 노드가 연속으로 연결되었습니다. 하나로 통합하는 것이 효율적입니다.";
|
||||
}
|
||||
|
||||
// 규칙 5: 액션 노드 연속 연결
|
||||
if (isActionNode(sourceNode.type) && isActionNode(targetNode.type)) {
|
||||
return "액션 노드가 연속으로 연결되었습니다. 트랜잭션과 성능을 고려해주세요.";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
**UI 구현**:
|
||||
|
||||
- 경고 아이콘을 연결선 위에 표시
|
||||
- 호버 시 경고 메시지 툴팁 표시
|
||||
|
||||
**예상 작업 시간**: 1시간
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: 시각적 피드백 (우선순위: 낮음)
|
||||
|
||||
**기능**:
|
||||
|
||||
1. 드래그 중 호환 가능한 노드 하이라이트
|
||||
2. 불가능한 연결 시도 시 빨간색 표시
|
||||
3. 경고가 있는 연결은 노란색 표시
|
||||
|
||||
**예상 작업 시간**: 2시간
|
||||
|
||||
---
|
||||
|
||||
## 테스트 케이스
|
||||
|
||||
### 금지 테스트
|
||||
|
||||
- [ ] tableSource → tableSource (금지)
|
||||
- [ ] fieldMapping → comment (금지)
|
||||
- [ ] 자기 자신 → 자기 자신 (금지)
|
||||
|
||||
### 경고 테스트
|
||||
|
||||
- [ ] fieldMapping → fieldMapping (경고)
|
||||
- [ ] insertAction → updateAction (경고)
|
||||
|
||||
### 정상 테스트
|
||||
|
||||
- [ ] tableSource → fieldMapping → insertAction
|
||||
- [ ] externalDBSource → condition → (TRUE) → updateAction
|
||||
- [ ] restAPISource → log → dataTransform → upsertAction
|
||||
|
||||
---
|
||||
|
||||
## 향후 확장
|
||||
|
||||
### 추가 고려사항
|
||||
|
||||
1. **핸들별 제약**:
|
||||
|
||||
- Condition 노드의 TRUE/FALSE 출력 구분
|
||||
- 특정 핸들만 특정 노드 타입과 연결 가능
|
||||
|
||||
2. **데이터 타입 검증**:
|
||||
|
||||
- 숫자 필드만 계산 노드로 연결 가능
|
||||
- 문자열 필드만 텍스트 변환 노드로 연결 가능
|
||||
|
||||
3. **순서 제약**:
|
||||
- UPDATE/DELETE 전에 반드시 SELECT 필요
|
||||
- 특정 변환 순서 강제
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
|
||||
| 버전 | 날짜 | 변경 내용 | 작성자 |
|
||||
| ---- | ---------- | --------- | ------ |
|
||||
| 1.0 | 2025-01-02 | 초안 작성 | AI |
|
||||
|
||||
---
|
||||
|
||||
**다음 단계**: Phase 1 구현 시작
|
||||
Reference in New Issue
Block a user