ERP-node/버튼_제어관리_기능_통합_계획서.md

49 KiB

🔧 버튼 제어관리 기능 통합 계획서

📋 프로젝트 개요

현재 구축되어 있는 **데이터 흐름 제어관리 시스템(DataFlow Management)**을 화면관리 시스템의 버튼 컴포넌트에 통합하여, 버튼 클릭 시 데이터 흐름을 제어할 수 있는 고급 기능을 제공합니다.

🎯 목표

  • 버튼 액션 실행 시 조건부 데이터 제어 기능 제공
  • 기존 제어관리 시스템의 조건부 연결 로직을 버튼 액션에 적용
  • 복잡한 비즈니스 로직을 GUI로 설정 가능한 시스템 구축

🔍 현재 상황 분석

제어관리 시스템 (DataFlow Diagrams) 분석

데이터베이스 구조

CREATE TABLE dataflow_diagrams (
  diagram_id SERIAL PRIMARY KEY,
  diagram_name VARCHAR(255),
  relationships JSONB,           -- 테이블 관계 정보
  company_code VARCHAR(50),
  created_at TIMESTAMP,
  updated_at TIMESTAMP,
  created_by VARCHAR(100),
  updated_by VARCHAR(100),
  node_positions JSONB,          -- 시각적 위치 정보
  control JSONB,                 -- 🔥 조건 설정 정보
  plan JSONB,                    -- 🔥 실행 계획 정보
  category JSON                  -- 🔥 연결 타입 정보
);

핵심 데이터 구조

1. control (조건 설정)

{
  "id": "rel-1758010445208",
  "triggerType": "insert",
  "conditions": [
    {
      "id": "cond_1758010388399_65jnzabvv",
      "type": "group-start",
      "groupId": "group_1758010388399_x4uhh1ztz",
      "groupLevel": 0
    },
    {
      "id": "cond_1758010388969_rs2y93llp",
      "type": "condition",
      "field": "target_type",
      "value": "1",
      "dataType": "string",
      "operator": "=",
      "logicalOperator": "AND"
    }
    // ... 추가 조건들
  ]
}

2. plan (실행 계획)

{
  "id": "rel-1758010445208",
  "sourceTable": "approval_kind",
  "actions": [
    {
      "id": "action_1",
      "name": "액션 1",
      "actionType": "insert",
      "conditions": [...],
      "fieldMappings": [
        {
          "sourceField": "",
          "sourceTable": "",
          "targetField": "target_type",
          "targetTable": "approval_kind",
          "defaultValue": "123123"
        }
      ],
      "splitConfig": {
        "delimiter": "",
        "sourceField": "",
        "targetField": ""
      }
    }
  ]
}

3. category (연결 타입)

[
  {
    "id": "rel-1758010379858",
    "category": "simple-key"
  },
  {
    "id": "rel-1758010445208",
    "category": "data-save"
  }
]

현재 버튼 시스템 분석

ButtonTypeConfig 인터페이스

export interface ButtonTypeConfig {
  actionType: ButtonActionType; // 기본 액션 타입
  variant?:
    | "default"
    | "destructive"
    | "outline"
    | "secondary"
    | "ghost"
    | "link";
  icon?: string;
  confirmMessage?: string;

  // 모달 관련 설정
  popupTitle?: string;
  popupContent?: string;
  popupScreenId?: number;

  // 네비게이션 관련 설정
  navigateType?: "url" | "screen";
  navigateUrl?: string;
  navigateScreenId?: number;
  navigateTarget?: "_self" | "_blank";

  // 커스텀 액션 설정
  customAction?: string;

  // 스타일 설정
  backgroundColor?: string;
  textColor?: string;
  borderColor?: string;
}

ButtonActionType

export type ButtonActionType =
  | "save"
  | "delete"
  | "edit"
  | "add"
  | "search"
  | "reset"
  | "submit"
  | "close"
  | "popup"
  | "modal"
  | "newWindow"
  | "navigate";

🚀 구현 계획

Phase 1: 기본 구조 확장

1.1 ButtonTypeConfig 인터페이스 확장 (기존 액션 타입 유지)

export interface ButtonTypeConfig {
  actionType: ButtonActionType; // 기존 액션 타입 그대로 유지
  variant?:
    | "default"
    | "destructive"
    | "outline"
    | "secondary"
    | "ghost"
    | "link";
  icon?: string;
  confirmMessage?: string;

  // 모달 관련 설정
  popupTitle?: string;
  popupContent?: string;
  popupScreenId?: number;

  // 네비게이션 관련 설정
  navigateType?: "url" | "screen";
  navigateUrl?: string;
  navigateScreenId?: number;
  navigateTarget?: "_self" | "_blank";

  // 커스텀 액션 설정
  customAction?: string;

  // 🔥 NEW: 모든 액션에 제어관리 옵션 추가
  enableDataflowControl?: boolean; // 제어관리 활성화 여부
  dataflowConfig?: ButtonDataflowConfig; // 제어관리 설정
  dataflowTiming?: "before" | "after" | "replace"; // 언제 실행할지

  // 스타일 설정
  backgroundColor?: string;
  textColor?: string;
  borderColor?: string;
}

export interface ButtonDataflowConfig {
  // 제어 방식 선택
  controlMode: "simple" | "advanced";

  // Simple 모드: 기존 관계도 선택
  selectedDiagramId?: number;
  selectedRelationshipId?: string;

  // Advanced 모드: 직접 조건 설정
  directControl?: {
    sourceTable: string;
    triggerType: "insert" | "update" | "delete";
    conditions: DataflowCondition[];
    actions: DataflowAction[];
  };

  // 실행 옵션
  executionOptions?: {
    rollbackOnError?: boolean;
    enableLogging?: boolean;
    maxRetryCount?: number;
    asyncExecution?: boolean;
  };
}

// 실행 타이밍 옵션 설명
// - "before": 기존 액션 실행 전에 제어관리 실행
// - "after": 기존 액션 실행 후에 제어관리 실행
// - "replace": 기존 액션 대신 제어관리만 실행

1.3 데이터 구조 정의

export interface DataflowCondition {
  id: string;
  type: "condition" | "group-start" | "group-end";
  field?: string;
  operator?: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
  value?: any;
  dataType?: "string" | "number" | "boolean" | "date";
  logicalOperator?: "AND" | "OR";
  groupId?: string;
  groupLevel?: number;
}

export interface DataflowAction {
  id: string;
  name: string;
  actionType: "insert" | "update" | "delete" | "upsert";
  targetTable: string;
  conditions?: DataflowCondition[];
  fieldMappings: DataflowFieldMapping[];
  splitConfig?: {
    sourceField: string;
    delimiter: string;
    targetField: string;
  };
}

export interface DataflowFieldMapping {
  sourceTable?: string;
  sourceField: string;
  targetTable?: string;
  targetField: string;
  defaultValue?: string;
  transformFunction?: string;
}

Phase 2: UI 컴포넌트 개발

2.1 ButtonDataflowConfigPanel 컴포넌트 (기존 액션별 제어관리 옵션)

interface ButtonDataflowConfigPanelProps {
  component: ComponentData;
  onUpdateProperty: (path: string, value: any) => void;
}

export const ButtonDataflowConfigPanel: React.FC<
  ButtonDataflowConfigPanelProps
> = ({ component, onUpdateProperty }) => {
  const config = component.webTypeConfig || {};
  const dataflowConfig = config.dataflowConfig || {};

  return (
    <div className="space-y-6">
      {/* 제어관리 활성화 스위치 */}
      <div className="flex items-center justify-between">
        <Label>제어관리 기능 활성화</Label>
        <Switch
          checked={config.enableDataflowControl || false}
          onCheckedChange={(checked) =>
            onUpdateProperty("webTypeConfig.enableDataflowControl", checked)
          }
        />
      </div>

      {/* 제어관리가 활성화된 경우에만 설정 표시 */}
      {config.enableDataflowControl && (
        <>
          {/* 실행 타이밍 선택 */}
          <div>
            <Label>실행 타이밍</Label>
            <Select
              value={config.dataflowTiming || "after"}
              onValueChange={(value) =>
                onUpdateProperty("webTypeConfig.dataflowTiming", value)
              }
            >
              <SelectContent>
                <SelectItem value="before">
                  기존 액션 실행  (사전 검증/준비)
                </SelectItem>
                <SelectItem value="after">
                  기존 액션 실행  (후속 처리)
                </SelectItem>
                <SelectItem value="replace">
                  기존 액션 대신 (완전 대체)
                </SelectItem>
              </SelectContent>
            </Select>
            <p className="mt-1 text-xs text-gray-500">
              {config.dataflowTiming === "before" &&
                "예: 저장 전 데이터 검증, 삭제 전 권한 확인"}
              {config.dataflowTiming === "after" &&
                "예: 저장 후 알림 발송, 삭제 후 관련 데이터 정리"}
              {config.dataflowTiming === "replace" &&
                "예: 복잡한 비즈니스 로직으로 기본 동작 완전 대체"}
            </p>
          </div>

          {/* 제어 모드 선택 */}
          <div>
            <Label>제어 모드</Label>
            <Select
              value={dataflowConfig.controlMode || "simple"}
              onValueChange={(value) =>
                onUpdateProperty(
                  "webTypeConfig.dataflowConfig.controlMode",
                  value
                )
              }
            >
              <SelectContent>
                <SelectItem value="simple">
                  간편 모드 (기존 관계도 선택)
                </SelectItem>
                <SelectItem value="advanced">고급 모드 (직접 설정)</SelectItem>
              </SelectContent>
            </Select>
          </div>

          {/* 간편 모드 UI */}
          {dataflowConfig.controlMode === "simple" && (
            <SimpleModePanel
              config={dataflowConfig}
              onUpdateProperty={onUpdateProperty}
            />
          )}

          {/* 고급 모드 UI */}
          {dataflowConfig.controlMode === "advanced" && (
            <AdvancedModePanel
              config={dataflowConfig}
              onUpdateProperty={onUpdateProperty}
            />
          )}

          {/* 실행 옵션 */}
          <ExecutionOptionsPanel
            config={dataflowConfig}
            onUpdateProperty={onUpdateProperty}
          />
        </>
      )}
    </div>
  );
};

2.2 SimpleModePanel - 기존 관계도 선택

const SimpleModePanel: React.FC<{
  config: ButtonDataflowConfig;
  onUpdateProperty: (path: string, value: any) => void;
}> = ({ config, onUpdateProperty }) => {
  const [diagrams, setDiagrams] = useState<DataFlowDiagram[]>([]);
  const [relationships, setRelationships] = useState<JsonRelationship[]>([]);

  return (
    <div className="space-y-4">
      {/* 관계도 선택 */}
      <div>
        <Label>관계도 선택</Label>
        <DiagramSelector
          value={config.selectedDiagramId}
          onSelect={(diagramId) => {
            onUpdateProperty(
              "webTypeConfig.dataflowConfig.selectedDiagramId",
              diagramId
            );
            // 관계도 선택 시 관련 관계들 로드
            loadRelationships(diagramId);
          }}
        />
      </div>

      {/* 관계 선택 */}
      {config.selectedDiagramId && (
        <div>
          <Label>제어할 관계 선택</Label>
          <RelationshipSelector
            diagramId={config.selectedDiagramId}
            value={config.selectedRelationshipId}
            onSelect={(relationshipId) =>
              onUpdateProperty(
                "webTypeConfig.dataflowConfig.selectedRelationshipId",
                relationshipId
              )
            }
          />
        </div>
      )}

      {/* 선택된 관계 미리보기 */}
      {config.selectedRelationshipId && (
        <RelationshipPreview
          diagramId={config.selectedDiagramId}
          relationshipId={config.selectedRelationshipId}
        />
      )}
    </div>
  );
};

2.3 AdvancedModePanel - 직접 조건 설정

const AdvancedModePanel: React.FC<{
  config: ButtonDataflowConfig;
  onUpdateProperty: (path: string, value: any) => void;
}> = ({ config, onUpdateProperty }) => {
  return (
    <div className="space-y-6">
      {/* 소스 테이블 선택 */}
      <div>
        <Label>소스 테이블</Label>
        <TableSelector
          value={config.directControl?.sourceTable}
          onSelect={(table) =>
            onUpdateProperty(
              "webTypeConfig.dataflowConfig.directControl.sourceTable",
              table
            )
          }
        />
      </div>

      {/* 트리거 타입 선택 */}
      <div>
        <Label>트리거 타입</Label>
        <Select
          value={config.directControl?.triggerType || "insert"}
          onValueChange={(value) =>
            onUpdateProperty(
              "webTypeConfig.dataflowConfig.directControl.triggerType",
              value
            )
          }
        >
          <SelectContent>
            <SelectItem value="insert">데이터 삽입 </SelectItem>
            <SelectItem value="update">데이터 수정 </SelectItem>
            <SelectItem value="delete">데이터 삭제 </SelectItem>
          </SelectContent>
        </Select>
      </div>

      {/* 조건 설정 */}
      <div>
        <Label>실행 조건</Label>
        <ConditionBuilder
          conditions={config.directControl?.conditions || []}
          onUpdate={(conditions) =>
            onUpdateProperty(
              "webTypeConfig.dataflowConfig.directControl.conditions",
              conditions
            )
          }
          sourceTable={config.directControl?.sourceTable}
        />
      </div>

      {/* 액션 설정 */}
      <div>
        <Label>실행 액션</Label>
        <ActionBuilder
          actions={config.directControl?.actions || []}
          onUpdate={(actions) =>
            onUpdateProperty(
              "webTypeConfig.dataflowConfig.directControl.actions",
              actions
            )
          }
          sourceTable={config.directControl?.sourceTable}
        />
      </div>
    </div>
  );
};

Phase 3: 서비스 계층 개발 (성능 최적화 적용)

3.1 OptimizedButtonDataflowService (즉시 응답 + 백그라운드 실행)

// 🔥 성능 최적화: 캐싱 시스템
class DataflowConfigCache {
  private memoryCache = new Map<string, ButtonDataflowConfig>();
  private readonly TTL = 5 * 60 * 1000; // 5분 TTL

  async getConfig(buttonId: string): Promise<ButtonDataflowConfig | null> {
    const cacheKey = `button_dataflow_${buttonId}`;

    // L1: 메모리 캐시 확인 (1ms)
    if (this.memoryCache.has(cacheKey)) {
      console.log("⚡ Cache hit:", buttonId);
      return this.memoryCache.get(cacheKey)!;
    }

    // L2: 서버에서 로드 (100-300ms)
    console.log("🌐 Loading from server:", buttonId);
    const serverConfig = await this.loadFromServer(buttonId);

    // 캐시에 저장
    this.memoryCache.set(cacheKey, serverConfig);

    // TTL 후 캐시 제거
    setTimeout(() => {
      this.memoryCache.delete(cacheKey);
    }, this.TTL);

    return serverConfig;
  }

  private async loadFromServer(buttonId: string): Promise<ButtonDataflowConfig> {
    // 실제 서버 호출 로직
    return {} as ButtonDataflowConfig;
  }
}

// 🔥 성능 최적화: 작업 큐 시스템
class DataflowJobQueue {
  private queue: Array<{
    id: string;
    buttonId: string;
    actionType: ButtonActionType;
    config: ButtonTypeConfig;
    contextData: Record<string, any>;
    companyCode: string;
    priority: "high" | "normal" | "low";
  }> = [];

  private processing = false;

  // 🔥 즉시 반환하는 작업 큐잉
  enqueue(
    buttonId: string,
    actionType: ButtonActionType,
    config: ButtonTypeConfig,
    contextData: Record<string, any>,
    companyCode: string,
    priority: "high" | "normal" | "low" = "normal"
  ): string {
    const jobId = `job_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;

    this.queue.push({
      id: jobId,
      buttonId,
      actionType,
      config,
      contextData,
      companyCode,
      priority,
    });

    // 우선순위 정렬
    this.queue.sort((a, b) => {
      const weights = { high: 3, normal: 2, low: 1 };
      return weights[b.priority] - weights[a.priority];
    });

    // 비동기 처리 시작
    this.processQueue();

    return jobId; // 🔥 즉시 반환
  }

  private async processQueue(): Promise<void> {
    if (this.processing || this.queue.length === 0) return;

    this.processing = true;

    try {
      // 배치 처리 (최대 3개 동시)
      const batch = this.queue.splice(0, 3);
      const promises = batch.map(job => this.executeJob(job));
      await Promise.allSettled(promises);
    } finally {
      this.processing = false;
      if (this.queue.length > 0) {
        setTimeout(() => this.processQueue(), 10);
      }
    }
  }

  private async executeJob(job: any): Promise<void> {
    const startTime = performance.now();

    try {
      await OptimizedButtonDataflowService.executeJobInternal(job);

      const executionTime = performance.now() - startTime;
      console.log(`⚡ Job ${job.id} completed in ${executionTime.toFixed(2)}ms`);
    } catch (error) {
      console.error(`❌ Job ${job.id} failed:`, error);
    }
  }
}

// 전역 인스턴스
const configCache = new DataflowConfigCache();
const jobQueue = new DataflowJobQueue();

export class OptimizedButtonDataflowService {
  /**
   * 🔥 메인 엔트리포인트: 즉시 응답 + 백그라운드 실행
   */
  static async executeButtonWithDataflow(
    actionType: ButtonActionType,
    buttonConfig: ButtonTypeConfig,
    contextData: Record<string, any>,
    companyCode: string,
    buttonId: string
  ): Promise<{ jobId: string; immediateResult?: any }> {

    const { enableDataflowControl, dataflowTiming } = buttonConfig;

    // 🔥 제어관리가 비활성화된 경우: 즉시 실행
    if (!enableDataflowControl) {
      const result = await this.executeOriginalAction(actionType, buttonConfig, contextData);
      return { jobId: "immediate", immediateResult: result };
    }

    // 🔥 타이밍별 즉시 응답 전략
    switch (dataflowTiming) {
      case "before":
        // before는 동기 처리 필요 (검증 목적)
        return await this.executeBeforeTiming(actionType, buttonConfig, contextData, companyCode);

      case "after":
        // after는 백그라운드 처리 가능
        return await this.executeAfterTiming(actionType, buttonConfig, contextData, companyCode, buttonId);

      case "replace":
        // replace는 상황에 따라 동기/비동기 선택
        return await this.executeReplaceTiming(actionType, buttonConfig, contextData, companyCode, buttonId);

      default:
        return await this.executeAfterTiming(actionType, buttonConfig, contextData, companyCode, buttonId);
    }
  }

  /**
   * 🔥 After 타이밍: 즉시 기존 액션 + 백그라운드 제어관리
   */
  private static async executeAfterTiming(
    actionType: ButtonActionType,
    buttonConfig: ButtonTypeConfig,
    contextData: Record<string, any>,
    companyCode: string,
    buttonId: string
  ): Promise<{ jobId: string; immediateResult: any }> {

    // 🔥 Step 1: 기존 액션 즉시 실행 (50-200ms)
    const immediateResult = await this.executeOriginalAction(
      actionType,
      buttonConfig,
      contextData
    );

    // 🔥 Step 2: 제어관리는 백그라운드에서 실행 (즉시 반환)
    const jobId = jobQueue.enqueue(
      buttonId,
      actionType,
      buttonConfig,
      { ...contextData, originalActionResult: immediateResult },
      companyCode,
      "normal"
    );

    return {
      jobId,
      immediateResult
    };
  }

  /**
   * 🔥 Before 타이밍: 빠른 제어관리 + 기존 액션
   */
  private static async executeBeforeTiming(
    actionType: ButtonActionType,
    buttonConfig: ButtonTypeConfig,
    contextData: Record<string, any>,
    companyCode: string
  ): Promise<{ jobId: string; immediateResult: any }> {

    // 간단한 조건만 즉시 검증 (복잡한 것은 에러)
    const isSimpleValidation = await this.isSimpleValidationOnly(buttonConfig.dataflowConfig);

    if (isSimpleValidation) {
      // 🔥 간단한 검증: 메모리에서 즉시 처리 (1-10ms)
      const validationResult = await this.executeQuickValidation(
        buttonConfig.dataflowConfig!,
        contextData
      );

      if (!validationResult.success) {
        return {
          jobId: "validation_failed",
          immediateResult: { success: false, message: validationResult.message }
        };
      }

      // 검증 통과 시 기존 액션 실행
      const actionResult = await this.executeOriginalAction(actionType, buttonConfig, contextData);
      return { jobId: "immediate", immediateResult: actionResult };

    } else {
      // 🔥 복잡한 검증: 사용자에게 알림 후 백그라운드 처리
      const jobId = jobQueue.enqueue(
        buttonConfig.buttonId || "unknown",
        actionType,
        buttonConfig,
        contextData,
        companyCode,
        "high" // 높은 우선순위
      );

      return {
        jobId,
        immediateResult: {
          success: true,
          message: "검증 중입니다. 잠시만 기다려주세요.",
          processing: true
        }
      };
    }
  }

  /**
   * 🔥 간단한 조건인지 판단 (메모리에서 즉시 처리 가능한지)
   */
  private static async isSimpleValidationOnly(config?: ButtonDataflowConfig): Promise<boolean> {
    if (!config || config.controlMode !== "advanced") return true;

    const conditions = config.directControl?.conditions || [];

    // 조건이 5개 이하이고 모두 단순 비교 연산자면 간단한 검증
    return conditions.length <= 5 &&
           conditions.every(c =>
             c.type === "condition" &&
             ["=", "!=", ">", "<", ">=", "<="].includes(c.operator || "")
           );
  }

  /**
   * 🔥 빠른 검증 (메모리에서 즉시 처리)
   */
  private static async executeQuickValidation(
    config: ButtonDataflowConfig,
    data: Record<string, any>
  ): Promise<{ success: boolean; message?: string }> {

    if (config.controlMode === "simple") {
      // 간편 모드는 일단 통과 (실제 검증은 백그라운드에서)
      return { success: true };
    }

    const conditions = config.directControl?.conditions || [];

    for (const condition of conditions) {
      if (condition.type === "condition") {
        const fieldValue = data[condition.field!];
        const isValid = this.evaluateSimpleCondition(
          fieldValue,
          condition.operator!,
          condition.value
        );

        if (!isValid) {
          return {
            success: false,
            message: `조건 불만족: ${condition.field} ${condition.operator} ${condition.value}`
          };
        }
      }
    }

    return { success: true };
  }

  /**
   * 🔥 단순 조건 평가 (메모리에서 즉시)
   */
  private static evaluateSimpleCondition(
    fieldValue: any,
    operator: string,
    conditionValue: any
  ): boolean {
    switch (operator) {
      case "=": return fieldValue === conditionValue;
      case "!=": return fieldValue !== conditionValue;
      case ">": return fieldValue > conditionValue;
      case "<": return fieldValue < conditionValue;
      case ">=": return fieldValue >= conditionValue;
      case "<=": return fieldValue <= conditionValue;
      default: return true;
    }
  }

  /**
   * 🔥 기존 액션 실행 (최적화)
   */
  private static async executeOriginalAction(
    actionType: ButtonActionType,
    buttonConfig: ButtonTypeConfig,
    contextData: Record<string, any>
  ): Promise<any> {
    const startTime = performance.now();

    try {
      switch (actionType) {
        case "save":
          return await this.executeSaveAction(buttonConfig, contextData);
        case "delete":
          return await this.executeDeleteAction(buttonConfig, contextData);
        case "search":
          return await this.executeSearchAction(buttonConfig, contextData);
        default:
          return { success: true, message: `${actionType} 액션 실행됨` };
      }
    } finally {
      const executionTime = performance.now() - startTime;
      if (executionTime > 200) {
        console.warn(`🐌 Slow action: ${actionType} took ${executionTime.toFixed(2)}ms`);
      }
    }
  }

  /**
   * 🔥 내부 작업 실행 (큐에서 호출)
   */
  static async executeJobInternal(job: any): Promise<void> {
    // 실제 제어관리 로직 실행
    const dataflowResult = await this.executeDataflowLogic(
      job.config.dataflowConfig,
      job.contextData,
      job.companyCode
    );

    // 결과를 클라이언트에 전송 (WebSocket, Server-Sent Events 등)
    this.notifyClient(job.id, dataflowResult);
  }

  private static async executeDataflowLogic(
    config: ButtonDataflowConfig,
    contextData: Record<string, any>,
    companyCode: string
  ): Promise<ExecutionResult> {
    // 기존 제어관리 로직 활용
    if (config.controlMode === "simple") {
      return await this.executeSimpleMode(config, contextData, companyCode);
    } else {
      return await this.executeAdvancedMode(config, contextData, companyCode);
    }
  }

  private static notifyClient(jobId: string, result: ExecutionResult): void {
    // WebSocket이나 Server-Sent Events로 결과 전송
    console.log(`📤 Notifying client: Job ${jobId} completed`, result);
  }
}

  /**
   * 간편 모드 실행 - 기존 관계도 활용
   */
  private static async executeSimpleMode(
    config: ButtonDataflowConfig,
    contextData: Record<string, any>,
    companyCode: string
  ): Promise<ExecutionResult> {
    // 1. 선택된 관계도와 관계 정보 조회
    const diagram = await this.getDiagramById(
      config.selectedDiagramId,
      companyCode
    );
    const relationship = this.findRelationshipById(
      diagram,
      config.selectedRelationshipId
    );

    // 2. 기존 EventTriggerService 활용
    return await EventTriggerService.executeSpecificRelationship(
      relationship,
      contextData,
      companyCode
    );
  }

  /**
   * 고급 모드 실행 - 직접 설정 조건 활용
   */
  private static async executeAdvancedMode(
    config: ButtonDataflowConfig,
    contextData: Record<string, any>,
    companyCode: string
  ): Promise<ExecutionResult> {
    const { directControl } = config;
    if (!directControl) {
      throw new Error("고급 모드 설정이 없습니다.");
    }

    // 1. 조건 검증
    const conditionsMet = await this.evaluateConditions(
      directControl.conditions,
      contextData
    );

    if (!conditionsMet) {
      return {
        success: true,
        executedActions: 0,
        message: "조건을 만족하지 않아 실행되지 않았습니다.",
      };
    }

    // 2. 액션 실행
    return await this.executeActions(
      directControl.actions,
      contextData,
      companyCode
    );
  }

  /**
   * 조건 평가
   */
  private static async evaluateConditions(
    conditions: DataflowCondition[],
    data: Record<string, any>
  ): Promise<boolean> {
    // 기존 EventTriggerService의 조건 평가 로직 재활용
    return await ConditionEvaluator.evaluate(conditions, data);
  }

  /**
   * 액션 실행
   */
  private static async executeActions(
    actions: DataflowAction[],
    contextData: Record<string, any>,
    companyCode: string
  ): Promise<ExecutionResult> {
    // 기존 EventTriggerService의 액션 실행 로직 재활용
    return await ActionExecutor.execute(actions, contextData, companyCode);
  }
}

3.2 기존 EventTriggerService 확장

export class EventTriggerService {
  // ... 기존 메서드들

  /**
   * 🔥 NEW: 특정 관계 실행 (버튼에서 호출)
   */
  static async executeSpecificRelationship(
    relationship: JsonRelationship,
    contextData: Record<string, any>,
    companyCode: string
  ): Promise<ExecutionResult> {
    // 관계에 해당하는 제어 조건 및 실행 계획 추출
    const control = this.extractControlFromRelationship(relationship);
    const plan = this.extractPlanFromRelationship(relationship);

    // 조건 검증
    const conditionsMet = await this.evaluateConditions(
      control.conditions,
      contextData
    );

    if (!conditionsMet) {
      return {
        success: true,
        executedActions: 0,
        message: "조건을 만족하지 않아 실행되지 않았습니다.",
      };
    }

    // 액션 실행
    return await this.executePlan(plan, contextData, companyCode);
  }

  /**
   * 🔥 NEW: 버튼 컨텍스트에서 데이터플로우 실행
   */
  static async executeFromButtonContext(
    buttonId: string,
    screenId: number,
    formData: Record<string, any>,
    companyCode: string
  ): Promise<ExecutionResult> {
    // 1. 버튼 설정 조회
    const buttonConfig = await this.getButtonDataflowConfig(buttonId, screenId);

    // 2. 컨텍스트 데이터 준비
    const contextData = {
      ...formData,
      buttonId,
      screenId,
      timestamp: new Date().toISOString(),
      userContext: await this.getUserContext(),
    };

    // 3. 데이터플로우 실행
    return await ButtonDataflowService.executeButtonDataflow(
      buttonConfig,
      contextData,
      companyCode
    );
  }
}

Phase 4: API 엔드포인트 개발

4.1 ButtonDataflowController

// backend-node/src/controllers/buttonDataflowController.ts

export async function executeButtonDataflow(
  req: AuthenticatedRequest,
  res: Response
): Promise<void> {
  try {
    const { buttonId, screenId, formData } = req.body;
    const companyCode = req.user?.company_code;

    const result = await EventTriggerService.executeFromButtonContext(
      buttonId,
      screenId,
      formData,
      companyCode
    );

    res.json({
      success: true,
      data: result,
    });
  } catch (error) {
    logger.error("Button dataflow execution failed:", error);
    res.status(500).json({
      success: false,
      message: "데이터플로우 실행 중 오류가 발생했습니다.",
    });
  }
}

export async function getAvailableDiagrams(
  req: AuthenticatedRequest,
  res: Response
): Promise<void> {
  try {
    const companyCode = req.user?.company_code;

    const diagrams = await DataFlowAPI.getJsonDataFlowDiagrams(companyCode);

    res.json({
      success: true,
      data: diagrams,
    });
  } catch (error) {
    logger.error("Failed to get available diagrams:", error);
    res.status(500).json({
      success: false,
      message: "관계도 목록 조회 중 오류가 발생했습니다.",
    });
  }
}

export async function getDiagramRelationships(
  req: AuthenticatedRequest,
  res: Response
): Promise<void> {
  try {
    const { diagramId } = req.params;
    const companyCode = req.user?.company_code;

    const diagram = await DataFlowAPI.getJsonDataFlowDiagramById(
      parseInt(diagramId),
      companyCode
    );

    const relationships = diagram.relationships?.relationships || [];

    res.json({
      success: true,
      data: relationships,
    });
  } catch (error) {
    logger.error("Failed to get diagram relationships:", error);
    res.status(500).json({
      success: false,
      message: "관계 목록 조회 중 오류가 발생했습니다.",
    });
  }
}

4.2 라우팅 설정

// backend-node/src/routes/buttonDataflowRoutes.ts

import express from "express";
import {
  executeButtonDataflow,
  getAvailableDiagrams,
  getDiagramRelationships,
} from "../controllers/buttonDataflowController";
import { authenticateToken } from "../middleware/authMiddleware";

const router = express.Router();

// 모든 라우트에 인증 미들웨어 적용
router.use(authenticateToken);

// 버튼 데이터플로우 실행
router.post("/execute", executeButtonDataflow);

// 사용 가능한 관계도 목록 조회
router.get("/diagrams", getAvailableDiagrams);

// 특정 관계도의 관계 목록 조회
router.get("/diagrams/:diagramId/relationships", getDiagramRelationships);

export default router;

Phase 5: 프론트엔드 통합

5.1 ButtonConfigPanel 수정 (모든 액션에 제어관리 옵션 추가)

// frontend/components/screen/config-panels/ButtonConfigPanel.tsx 수정

export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
  component,
  onUpdateProperty,
}) => {
  const config = component.webTypeConfig || {};

  return (
    <div className="space-y-4">
      {/* 기존 액션 타입 선택 (변경 없음) */}
      <div>
        <Label htmlFor="action-type">액션 타입</Label>
        <Select
          value={config.actionType || "save"}
          onValueChange={(value) =>
            onUpdateProperty("webTypeConfig.actionType", value)
          }
        >
          <SelectContent>
            <SelectItem value="save">저장</SelectItem>
            <SelectItem value="delete">삭제</SelectItem>
            <SelectItem value="edit">수정</SelectItem>
            <SelectItem value="add">추가</SelectItem>
            <SelectItem value="search">검색</SelectItem>
            <SelectItem value="reset">초기화</SelectItem>
            <SelectItem value="submit">제출</SelectItem>
            <SelectItem value="close">닫기</SelectItem>
            <SelectItem value="popup">팝업 열기</SelectItem>
            <SelectItem value="navigate">페이지 이동</SelectItem>
          </SelectContent>
        </Select>
      </div>

      {/* 기존 액션별 설정들 (variant, icon, confirmMessage 등) */}
      {/* ... 기존 UI 컴포넌트들 ... */}

      {/* 🔥 NEW: 모든 액션에 제어관리 옵션 추가 */}
      <div className="mt-6 border-t pt-6">
        <div className="mb-4 flex items-center space-x-2">
          <Switch
            id="enable-dataflow"
            checked={config.enableDataflowControl || false}
            onCheckedChange={(checked) =>
              onUpdateProperty("webTypeConfig.enableDataflowControl", checked)
            }
          />
          <Label htmlFor="enable-dataflow" className="text-sm font-medium">
            📊 제어관리 기능 추가
          </Label>
        </div>

        {config.enableDataflowControl && (
          <div className="ml-6 space-y-4 rounded-lg border bg-gray-50 p-4">
            <div className="text-sm text-gray-600">
              <strong>{getActionDisplayName(config.actionType)}</strong> 액션과
              함께 데이터 흐름 제어 기능이 실행됩니다.
            </div>

            <ButtonDataflowConfigPanel
              component={component}
              onUpdateProperty={onUpdateProperty}
            />
          </div>
        )}
      </div>
    </div>
  );
};

// 액션 타입별 표시명 헬퍼 함수
function getActionDisplayName(actionType: string): string {
  const displayNames = {
    save: "저장",
    delete: "삭제",
    edit: "수정",
    add: "추가",
    search: "검색",
    reset: "초기화",
    submit: "제출",
    close: "닫기",
    popup: "팝업",
    navigate: "페이지 이동",
  };
  return displayNames[actionType] || actionType;
}

5.2 프론트엔드 최적화 (즉시 응답 UI)

// frontend/components/screen/OptimizedButtonComponent.tsx

import React, { useState, useCallback } from "react";
import { useDebouncedCallback } from "use-debounce";
import { toast } from "react-hot-toast";

interface OptimizedButtonProps {
  component: ComponentData;
  onDataflowComplete?: (result: any) => void;
}

export const OptimizedButtonComponent: React.FC<OptimizedButtonProps> = ({
  component,
  onDataflowComplete,
}) => {
  const [isExecuting, setIsExecuting] = useState(false);
  const [executionTime, setExecutionTime] = useState<number | null>(null);
  const [backgroundJobs, setBackgroundJobs] = useState<Set<string>>(new Set());

  const config = component.webTypeConfig;

  // 🔥 디바운싱으로 중복 클릭 방지
  const handleClick = useDebouncedCallback(async () => {
    if (isExecuting) return;

    setIsExecuting(true);
    const startTime = performance.now();

    try {
      // 🔥 현재 폼 데이터 수집
      const formData = collectFormData();

      if (config?.enableDataflowControl && config?.dataflowConfig) {
        // 🔥 최적화된 버튼 실행 (즉시 응답)
        await executeOptimizedButtonAction(component, formData);
      } else {
        // 🔥 기존 액션만 실행
        await executeOriginalAction(config?.actionType || "save", formData);
      }
    } catch (error) {
      console.error("Button execution failed:", error);
      toast.error("버튼 실행 중 오류가 발생했습니다.");
    } finally {
      const endTime = performance.now();
      setExecutionTime(endTime - startTime);
      setIsExecuting(false);
    }
  }, 300); // 300ms 디바운싱

  /**
   * 🔥 최적화된 버튼 액션 실행
   */
  const executeOptimizedButtonAction = async (
    component: ComponentData,
    formData: Record<string, any>
  ) => {
    const config = component.webTypeConfig!;

    // 🔥 API 호출 (즉시 응답)
    const response = await apiClient.post(
      "/api/button-dataflow/execute-optimized",
      {
        actionType: config.actionType,
        buttonConfig: config,
        buttonId: component.id,
        formData: formData,
      }
    );

    const { jobId, immediateResult } = response.data;

    // 🔥 즉시 결과 처리
    if (immediateResult) {
      handleImmediateResult(config.actionType, immediateResult);

      // 사용자에게 즉시 피드백
      toast.success(
        getSuccessMessage(config.actionType, config.dataflowTiming)
      );
    }

    // 🔥 백그라운드 작업 추적
    if (jobId && jobId !== "immediate") {
      setBackgroundJobs((prev) => new Set([...prev, jobId]));

      // 백그라운드 작업 완료 대기 (선택적)
      if (config.dataflowTiming === "before") {
        // before 타이밍은 결과를 기다려야 함
        await waitForBackgroundJob(jobId);
      } else {
        // after/replace 타이밍은 백그라운드에서 조용히 처리
        trackBackgroundJob(jobId);
      }
    }
  };

  /**
   * 🔥 즉시 결과 처리
   */
  const handleImmediateResult = (actionType: string, result: any) => {
    switch (actionType) {
      case "save":
        if (result.success) {
          // 폼 초기화 또는 목록 새로고침
          refreshDataList?.();
        }
        break;
      case "delete":
        if (result.success) {
          // 목록에서 제거
          removeFromList?.(result.deletedId);
        }
        break;
      case "search":
        if (result.success) {
          // 검색 결과 표시
          displaySearchResults?.(result.data);
        }
        break;
      default:
        console.log(`${actionType} 액션 완료:`, result);
    }
  };

  /**
   * 🔥 성공 메시지 생성
   */
  const getSuccessMessage = (actionType: string, timing?: string): string => {
    const actionName = getActionDisplayName(actionType);

    switch (timing) {
      case "before":
        return `${actionName} 작업을 처리 중입니다...`;
      case "after":
        return `${actionName}이 완료되었습니다. 추가 처리를 진행 중입니다.`;
      case "replace":
        return `사용자 정의 작업을 처리 중입니다...`;
      default:
        return `${actionName}이 완료되었습니다.`;
    }
  };

  /**
   * 🔥 백그라운드 작업 추적
   */
  const trackBackgroundJob = (jobId: string) => {
    // WebSocket이나 polling으로 작업 상태 확인
    const pollJobStatus = async () => {
      try {
        const statusResponse = await apiClient.get(
          `/api/button-dataflow/job-status/${jobId}`
        );
        const { status, result } = statusResponse.data;

        if (status === "completed") {
          setBackgroundJobs((prev) => {
            const newSet = new Set(prev);
            newSet.delete(jobId);
            return newSet;
          });

          // 백그라운드 작업 완료 알림 (조용하게)
          if (result.executedActions > 0) {
            toast.success(
              `추가 처리가 완료되었습니다. (${result.executedActions}개 액션)`,
              { duration: 2000 }
            );
          }

          onDataflowComplete?.(result);
        } else if (status === "failed") {
          setBackgroundJobs((prev) => {
            const newSet = new Set(prev);
            newSet.delete(jobId);
            return newSet;
          });

          console.error("Background job failed:", result);
        } else {
          // 아직 진행 중 - 1초 후 다시 확인
          setTimeout(pollJobStatus, 1000);
        }
      } catch (error) {
        console.error("Failed to check job status:", error);
      }
    };

    // 즉시 상태 확인 시작
    setTimeout(pollJobStatus, 500);
  };

  /**
   * 🔥 백그라운드 작업 완료 대기 (before 타이밍용)
   */
  const waitForBackgroundJob = async (jobId: string): Promise<void> => {
    return new Promise((resolve, reject) => {
      const checkStatus = async () => {
        try {
          const response = await apiClient.get(
            `/api/button-dataflow/job-status/${jobId}`
          );
          const { status, result } = response.data;

          if (status === "completed") {
            setBackgroundJobs((prev) => {
              const newSet = new Set(prev);
              newSet.delete(jobId);
              return newSet;
            });

            toast.success("모든 처리가 완료되었습니다.");
            onDataflowComplete?.(result);
            resolve();
          } else if (status === "failed") {
            setBackgroundJobs((prev) => {
              const newSet = new Set(prev);
              newSet.delete(jobId);
              return newSet;
            });

            toast.error("처리 중 오류가 발생했습니다.");
            reject(new Error(result.error));
          } else {
            // 진행 중 - 500ms 후 다시 확인
            setTimeout(checkStatus, 500);
          }
        } catch (error) {
          reject(error);
        }
      };

      checkStatus();
    });
  };

  return (
    <Button
      onClick={handleClick}
      disabled={isExecuting}
      className={`
        relative transition-all duration-200
        ${isExecuting ? "opacity-75 cursor-wait" : ""}
        ${backgroundJobs.size > 0 ? "bg-blue-50 border-blue-200" : ""}
      `}
    >
      {/* 메인 버튼 내용 */}
      {isExecuting ? (
        <div className="flex items-center space-x-2">
          <Spinner size="sm" />
          <span>처리중...</span>
        </div>
      ) : (
        component.label || "버튼"
      )}

      {/* 백그라운드 작업 표시 */}
      {backgroundJobs.size > 0 && !isExecuting && (
        <div className="absolute -top-1 -right-1 h-3 w-3">
          <div className="animate-pulse h-full w-full bg-blue-500 rounded-full"></div>
        </div>
      )}

      {/* 개발 모드에서 성능 정보 표시 */}
      {process.env.NODE_ENV === "development" && executionTime && (
        <span className="ml-2 text-xs opacity-60">
          {executionTime.toFixed(0)}ms
        </span>
      )}
    </Button>
  );
};

/**
 * 🔥 액션 타입별 표시명
 */
function getActionDisplayName(actionType: string): string {
  const displayNames = {
    save: "저장",
    delete: "삭제",
    edit: "수정",
    add: "추가",
    search: "검색",
    reset: "초기화",
    submit: "제출",
    close: "닫기",
    popup: "팝업",
    navigate: "페이지 이동",
  };
  return displayNames[actionType] || actionType;
}

/**
 * 🔥 기존 액션 실행 (제어관리 없음)
 */
const executeOriginalAction = async (
  actionType: string,
  formData: Record<string, any>
): Promise<any> => {
  const startTime = performance.now();

  try {
    const response = await apiClient.post(`/api/actions/${actionType}`, {
      formData,
    });

    const executionTime = performance.now() - startTime;
    console.log(`⚡ ${actionType} completed in ${executionTime.toFixed(2)}ms`);

    return response.data;
  } catch (error) {
    console.error(`❌ ${actionType} failed:`, error);
    throw error;
  }
};

🔄 사용 시나리오

시나리오 1: 저장 + 승인 프로세스 (after 타이밍)

  1. 설정 단계

    • 버튼 액션 타입: "save" (저장)
    • 제어관리 활성화:
    • 실행 타이밍: "after" (저장 후)
    • 제어 모드: "간편 모드"
    • 관계도 선택: "승인 프로세스 관계도"
    • 관계 선택: "문서 저장 → 결재 데이터 자동 생성"
  2. 실행 단계

    • 사용자가 저장 버튼 클릭
    • 1단계: 문서 저장 실행 (기존 save 액션)
    • 2단계: 저장 성공 후 제어관리 실행
    • 조건 검증: 문서 상태, 작성자 권한 등
    • 조건 만족 시 결재 테이블에 데이터 자동 삽입
    • 관련 승인자에게 알림 발송

시나리오 2: 삭제 + 관련 데이터 정리 (before 타이밍)

  1. 설정 단계

    • 버튼 액션 타입: "delete" (삭제)
    • 제어관리 활성화:
    • 실행 타이밍: "before" (삭제 전)
    • 제어 모드: "고급 모드"
    • 소스 테이블: "order_master"
    • 조건 설정: status != 'completed' AND created_date > 30일전
    • 액션 설정: 관련 order_items, payment_info 테이블 사전 정리
  2. 실행 단계

    • 주문 삭제 버튼 클릭
    • 1단계: 삭제 전 제어관리 실행
    • 조건 검증: 삭제 가능 상태인지 확인
    • 관련 테이블 데이터 사전 정리
    • 2단계: 메인 주문 데이터 삭제 실행

시나리오 3: 복잡한 비즈니스 로직 (replace 타이밍)

  1. 설정 단계

    • 버튼 액션 타입: "submit" (제출)
    • 제어관리 활성화:
    • 실행 타이밍: "replace" (기존 액션 대신)
    • 제어 모드: "고급 모드"
    • 복잡한 다단계 프로세스 설정:
      • 재고 확인 → 가격 계산 → 할인 적용 → 주문 생성 → 결제 처리
  2. 실행 단계

    • 주문 제출 버튼 클릭
    • 기존 submit 액션은 실행되지 않음
    • 제어관리에서 정의한 복잡한 비즈니스 로직만 실행
    • 다단계 프로세스를 통한 주문 처리

🎯 기대 효과

개발자 관점

  • 복잡한 비즈니스 로직을 코드 없이 GUI로 설정 가능
  • 기존 제어관리 시스템의 재사용으로 개발 시간 단축
  • 버튼 액션과 데이터 제어의 통합으로 일관된 UX 제공

사용자 관점

  • 직관적인 버튼 클릭으로 복합적인 데이터 처리 가능
  • 실시간 조건 검증으로 오류 방지
  • 자동화된 데이터 흐름으로 업무 효율성 향상

시스템 관점

  • 기존 인프라 활용으로 안정성 확보
  • 모듈화된 설계로 유지보수성 향상
  • 확장 가능한 아키텍처로 미래 요구사항 대응

📝 성능 최적화 중심 구현 우선순위

🚀 Phase 1: 즉시 효과 (1-2주) - 성능 기반

  1. 즉시 응답 패턴 구현

    • OptimizedButtonComponent 개발
    • 기존 액션 + 백그라운드 제어관리 분리
    • 디바운싱 및 중복 클릭 방지
  2. 기본 캐싱 시스템

    • DataflowConfigCache 구현 (메모리 캐시)
    • 버튼별 설정 5분 TTL 캐싱
    • 캐시 히트율 모니터링
  3. 데이터베이스 최적화

    • 버튼별 제어관리 조회 인덱스 추가
    • 전체 스캔 제거, 직접 조회로 변경
    • 간단한 조건 메모리 평가
  4. 간편 모드만 구현

    • 기존 관계도 선택 방식
    • "after" 타이밍만 지원 (리스크 최소화)
    • 복잡한 고급 모드는 2차에서

🔧 Phase 2: 고급 기능 (3-4주) - 안정성 확보

  1. 🔄 작업 큐 시스템

    • DataflowJobQueue 구현
    • 배치 처리 (최대 3개 동시)
    • 우선순위 기반 작업 처리
  2. 🔄 고급 모드 추가

    • ConditionBuilder, ActionBuilder 컴포넌트
    • "before", "replace" 타이밍 지원
    • 복잡한 조건 설정 UI
  3. 🔄 실시간 상태 추적

    • WebSocket 또는 polling 기반 작업 상태 확인
    • 백그라운드 작업 진행률 표시
    • 실패 시 자동 재시도 로직
  4. 🔄 성능 모니터링

    • 실시간 성능 지표 수집
    • 느린 쿼리 감지 및 알림
    • 캐시 효율성 분석

Phase 3: 고도화 (5-6주) - 사용자 경험 최적화

  1. 프리로딩 시스템

    • 사용자 패턴 분석 기반 설정 미리 로드
    • 예측적 캐싱 (자주 사용되는 관계도 우선)
    • 브라우저 유휴 시간 활용 백그라운드 로딩
  2. 고급 캐싱 전략

    • 다층 캐싱 (L1: 메모리, L2: 브라우저 저장소, L3: 서버)
    • 캐시 무효화 전략 고도화
    • 분산 캐싱 (여러 탭 간 공유)
  3. 성능 대시보드

    • 관리자용 성능 모니터링 대시보드
    • 버튼별 사용 빈도 및 성능 지표
    • 자동 최적화 추천 시스템
  4. AI 기반 최적화

    • 사용자 패턴 학습
    • 자동 설정 추천
    • 성능 병목점 자동 감지

🎯 성능 목표 달성 지표

Phase 목표 응답 시간 사용자 체감 구현 내용
Phase 1 50-200ms 즉각 반응 즉시 응답 + 캐싱
Phase 2 100-300ms 빠른 응답 큐 시스템 + 최적화
Phase 3 10-100ms 초고속 프리로딩 + AI 최적화

💡 주요 성능 최적화 포인트

🔥 Critical Path 최적화

사용자 클릭 → 즉시 UI 응답 (0ms) → 기존 액션 (50-200ms) → 백그라운드 제어관리

🔥 Smart Caching Strategy

L1: 메모리 (1ms) → L2: 브라우저 저장소 (5-10ms) → L3: 서버 (100-300ms)

🔥 Database Optimization

기존: 전체 관계도 스캔 (500ms+)
새로운: 버튼별 직접 조회 (10-50ms)

이렇게 성능을 중심으로 단계적으로 구현하면, 사용자는 기존과 동일한 속도감을 유지하면서 강력한 제어관리 기능을 점진적으로 활용할 수 있게 됩니다!