Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/report

This commit is contained in:
dohyeons
2025-10-13 09:44:09 +09:00
74 changed files with 20398 additions and 482 deletions

View 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 고려

View 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. **배포**: 단계적 릴리스
---
**피드백 환영**: 이 설계에 대한 의견을 주시면 개선하겠습니다! 💬

File diff suppressed because it is too large Load Diff

View 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)

View 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)

View 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 구현 시작