20 KiB
20 KiB
노드 시스템 - 버튼 통합 호환성 분석
작성일: 2025-01-02
버전: 1.0
상태: 🔍 분석 완료
📋 목차
개요
목적
화면관리의 버튼 컴포넌트에 할당된 기존 제어 시스템을 새로운 노드 기반 제어 시스템으로 전환하기 위한 호환성 분석
비교 대상
- 현재:
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 방식 모두 지원
작업
- ButtonDataflowConfig 확장
interface ButtonDataflowConfig {
controlMode: "relationship" | "flow" | "none";
// 기존 (하위 호환)
relationshipConfig?: {
relationshipId: string;
executionTiming: "before" | "after" | "replace";
};
// 🆕 신규
flowConfig?: {
flowId: number;
executionTiming: "before" | "after" | "replace";
};
}
- 실행 로직 분기
if (buttonConfig.controlMode === "flow") {
await executeButtonWithFlow(buttonConfig, formData, context);
} else if (buttonConfig.controlMode === "relationship") {
await executeButtonWithRelationship(buttonConfig, formData, context);
}
- 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: 완전 전환
목표
모든 버튼이 노드 플로우 방식 사용
작업
- 마이그레이션 스크립트 실행
-- 모든 관계를 플로우로 변환
SELECT migrate_all_relationships_to_flows();
- UI에서 관계 모드 제거
// controlMode에서 "relationship" 제거
type ControlMode = "flow" | "none";
- 레거시 코드 정리
executeButtonWithRelationship()제거RelationshipService제거 (또는 읽기 전용)
결론
✅ 호환 가능
노드 시스템과 버튼 제어 시스템은 충분히 호환 가능합니다!
🎯 권장 방안
**하이브리드 방식 (전략 1)**으로 점진적 마이그레이션
이유
- ✅ 기존 시스템 유지 - 서비스 중단 없음
- ✅ 점진적 전환 - 리스크 최소화
- ✅ 유연성 - 두 방식 모두 활용 가능
- ✅ 학습 곡선 - 사용자가 천천히 적응
📋 다음 단계
-
Phase 1 구현 (예상: 2일)
ButtonDataflowConfig확장executeButtonWithFlow()구현- UI 선택 옵션 추가
-
Phase 2 도구 개발 (예상: 1일)
- 마이그레이션 스크립트
- 자동 변환 로직
-
Phase 3 전환 (예상: 1일)
- 데이터 마이그레이션
- 레거시 제거
총 소요 시간
약 4일
참고 문서: