940 lines
20 KiB
Markdown
940 lines
20 KiB
Markdown
|
|
# 노드 시스템 - 버튼 통합 호환성 분석
|
||
|
|
|
||
|
|
**작성일**: 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)
|