ERP-node/docs/노드_시스템_버튼_통합_분석.md

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)