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

20 KiB

노드 시스템 - 버튼 통합 호환성 분석

작성일: 2025-01-02
버전: 1.0
상태: 🔍 분석 완료


📋 목차

  1. 개요
  2. 현재 시스템 분석
  3. 호환성 분석
  4. 통합 전략
  5. 마이그레이션 계획

개요

목적

화면관리의 버튼 컴포넌트에 할당된 기존 제어 시스템을 새로운 노드 기반 제어 시스템으로 전환하기 위한 호환성 분석

비교 대상

  • 현재: relationshipId 기반 제어 시스템
  • 신규: flowId 기반 노드 제어 시스템

현재 시스템 분석

1. 데이터 구조

ButtonDataflowConfig

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;
}

관계 데이터 구조

{
  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. 데이터 전달 방식

입력 데이터

{
  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"
  }
}

액션 실행 시

// 각 액션에 전체 데이터 전달
executeDataAction(action, {
  formData,
  selectedRowsData,
  context,
});

새로운 노드 시스템 분석

1. 데이터 구조

FlowData

interface FlowData {
  flowId: number;
  flowName: string;
  flowDescription: string;
  nodes: FlowNode[]; // 🔑 핵심: 노드 배열
  edges: FlowEdge[]; // 🔑 핵심: 연결 정보
}

노드 예시

// 소스 노드
{
  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: [...]
  }
}

연결 예시

// 엣지 (노드 간 연결)
{
  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

{
  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"]
}

노드 실행 시

// 부모 노드 결과 전달
const inputData = prepareInputData(node, parents, context);

// 부모가 하나면 부모의 결과 데이터
// 부모가 여러 개면 모든 부모의 데이터 병합

호환성 분석

호환 가능한 부분

1. 조건 검증

현재:

{
  field: "status",
  operator: "equals",
  value: "active"
}

신규:

{
  type: "condition",
  data: {
    conditions: [
      {
        field: "status",
        operator: "equals",
        value: "active"
      }
    ]
  }
}

결론: 조건 구조가 거의 동일 → 마이그레이션 쉬움


2. 액션 실행

현재:

{
  type: "database",
  operation: "INSERT",
  tableName: "users",
  fields: [
    { name: "name", value: "김철수" }
  ]
}

신규:

{
  type: "insertAction",
  data: {
    targetTable: "users",
    fieldMappings: [
      { sourceField: "name", targetField: "name" }
    ]
  }
}

결론: 액션 개념이 동일 → 필드명만 변환하면 됨


3. 데이터 소스

현재:

controlDataSource: "form" | "table-selection" | "both";

신규:

{
  type: "tableSource", // 테이블 선택 데이터
  // 또는
  type: "manualInput", // 폼 데이터
}

결론: 소스 타입 매핑 가능


⚠️ 차이점 및 주의사항

1. 실행 타이밍

현재:

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 병렬 실행

현재:

actionGroups: [
  {
    name: "그룹1",
    actions: [action1, action2], // 순차 실행
  },
];

신규:

소스
  ↓
  ├─→ INSERT (병렬)
  ├─→ UPDATE (병렬)
  └─→ DELETE (병렬)

문제점:

  • 현재는 "그룹 내 순차, 그룹 간 조건부"
  • 신규는 "레벨별 병렬, 연쇄 중단"

해결 방안:

노드 연결로 순차/병렬 표현:

순차:  INSERT → UPDATE → DELETE
병렬:  Source → INSERT
              → UPDATE
              → DELETE

3. 데이터 전달 방식

현재:

// 모든 액션에 동일한 데이터 전달
executeDataAction(action, {
  formData,
  selectedRowsData,
  context,
});

신규:

// 부모 노드 결과를 자식에게 전달
const inputData = parentResult.data || sourceData;

문제점:

  • 현재는 "원본 데이터 공유"
  • 신규는 "결과 데이터 체이닝"

해결 방안:

// 버튼 실행 시 초기 데이터 설정
context.sourceData = {
  formData,
  selectedRowsData,
};

// 각 노드는 필요에 따라 선택
- formData 사용
- 부모 결과 사용
-   사용

4. 컨텍스트 정보

현재:

{
  buttonId: "btn-1",
  screenId: 123,
  companyCode: "COMPANY_A",
  userId: "user-1"
}

신규:

// ExecutionContext에 추가 필요
{
  sourceData: [...],
  nodeResults: Map(),
  // 🆕 추가 필요
  buttonContext?: {
    buttonId: string,
    screenId: number,
    companyCode: string,
    userId: string
  }
}

결론: 컨텍스트 확장 가능


통합 전략

전략 1: 하이브리드 방식 (권장 )

개념

버튼 설정에서 relationshipId 대신 flowId를 저장하고, 기존 타이밍 개념 유지

버튼 설정

interface ButtonDataflowConfig {
  controlMode: "flow"; // 🆕 신규 모드

  flowConfig?: {
    flowId: number; // 🔑 노드 플로우 ID
    flowName: string;
    executionTiming: "before" | "after" | "replace"; // 기존 유지
    contextData?: Record<string, any>;
  };

  controlDataSource?: "form" | "table-selection" | "both";
}

실행 로직

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;
  }
}

소스 데이터 준비

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 확장
interface ButtonDataflowConfig {
  controlMode: "relationship" | "flow" | "none";

  // 기존 (하위 호환)
  relationshipConfig?: {
    relationshipId: string;
    executionTiming: "before" | "after" | "replace";
  };

  // 🆕 신규
  flowConfig?: {
    flowId: number;
    executionTiming: "before" | "after" | "replace";
  };
}
  1. 실행 로직 분기
if (buttonConfig.controlMode === "flow") {
  await executeButtonWithFlow(buttonConfig, formData, context);
} else if (buttonConfig.controlMode === "relationship") {
  await executeButtonWithRelationship(buttonConfig, formData, context);
}
  1. UI 업데이트
  • 버튼 설정에 "제어 방식 선택" 추가
  • "기존 관계" vs "노드 플로우" 선택 가능

Phase 2: 마이그레이션 도구

관계 → 플로우 변환기

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;
}

액션 변환 로직

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. 마이그레이션 스크립트 실행
-- 모든 관계를 플로우로 변환
SELECT migrate_all_relationships_to_flows();
  1. UI에서 관계 모드 제거
// controlMode에서 "relationship" 제거
type ControlMode = "flow" | "none";
  1. 레거시 코드 정리
  • executeButtonWithRelationship() 제거
  • RelationshipService 제거 (또는 읽기 전용)

결론

호환 가능

노드 시스템과 버튼 제어 시스템은 충분히 호환 가능합니다!

🎯 권장 방안

**하이브리드 방식 (전략 1)**으로 점진적 마이그레이션

이유

  1. 기존 시스템 유지 - 서비스 중단 없음
  2. 점진적 전환 - 리스크 최소화
  3. 유연성 - 두 방식 모두 활용 가능
  4. 학습 곡선 - 사용자가 천천히 적응

📋 다음 단계

  1. Phase 1 구현 (예상: 2일)

    • ButtonDataflowConfig 확장
    • executeButtonWithFlow() 구현
    • UI 선택 옵션 추가
  2. Phase 2 도구 개발 (예상: 1일)

    • 마이그레이션 스크립트
    • 자동 변환 로직
  3. Phase 3 전환 (예상: 1일)

    • 데이터 마이그레이션
    • 레거시 제거

총 소요 시간

약 4일


참고 문서: