"use client";

import React from "react";
import { toast } from "sonner";
import { showErrorToast } from "@/lib/utils/toastUtils";
import { screenApi } from "@/lib/api/screen";
import { DynamicFormApi } from "@/lib/api/dynamicForm";
import { ImprovedButtonActionExecutor } from "@/lib/utils/improvedButtonActionExecutor";
import { apiClient } from "@/lib/api/client";
import type { ExtendedControlContext } from "@/types/control-management";
import { isColumnRequiredByMeta } from "@/lib/registry/DynamicComponentRenderer";

/**
 * 🔧 formData 내 배열 값을 쉼표 구분 문자열로 변환
 * PostgreSQL 배열 형식 저장 방지
 */
function normalizeFormDataArrays(formData: Record<string, any>): Record<string, any> {
  if (!formData || typeof formData !== "object") return formData;

  const normalized: Record<string, any> = {};
  for (const [key, value] of Object.entries(formData)) {
    if (Array.isArray(value)) {
      // 배열 내 숫자를 문자열로 변환 후 쉼표 구분
      const stringValue = value
        .map((v) => (typeof v === "number" ? String(v) : v))
        .filter((v) => v !== null && v !== undefined && v !== "")
        .join(",");
      console.log(`🔧 [normalizeFormDataArrays] 배열→문자열: ${key}`, { original: value, converted: stringValue });
      normalized[key] = stringValue;
    } else {
      normalized[key] = value;
    }
  }
  return normalized;
}

/**
 * 버튼 액션 타입 정의
 */
export type ButtonActionType =
  | "save" // 저장
  | "delete" // 삭제
  | "edit" // 편집
  | "copy" // 복사 (품목코드 초기화)
  | "navigate" // 페이지 이동
  | "openRelatedModal" // 연관 데이터 버튼의 선택 데이터로 모달 열기
  | "openModalWithData" // 데이터를 전달하면서 모달 열기 (deprecated: modal로 통합)
  | "modal" // 모달 열기 (선택된 데이터 전달 옵션 포함)
  | "control" // 제어 흐름
  | "view_table_history" // 테이블 이력 보기
  | "excel_download" // 엑셀 다운로드
  | "excel_upload" // 엑셀 업로드
  | "multi_table_excel_upload" // 다중 테이블 엑셀 업로드
  | "barcode_scan" // 바코드 스캔
  | "code_merge" // 코드 병합
  // | "empty_vehicle" // 공차등록 (위치 수집 + 상태 변경) - 운행알림으로 통합
  | "operation_control" // 운행알림 및 종료 (위치 수집 + 상태 변경 + 연속 추적)
  | "swap_fields" // 필드 값 교환 (출발지 ↔ 목적지)
  | "transferData" // 데이터 전달 (컴포넌트 간 or 화면 간)
  | "quickInsert" // 즉시 저장 (선택한 데이터를 특정 테이블에 즉시 INSERT)
  | "event" // 이벤트 버스로 이벤트 발송 (스케줄 생성 등)
  | "approval" // 결재 요청
  | "apiCall"; // 범용 API 호출 (생산계획 자동 스케줄 등)

/**
 * 버튼 액션 설정
 */
export interface ButtonActionConfig {
  type: ButtonActionType;

  // 저장/제출 관련
  saveEndpoint?: string;
  validateForm?: boolean;

  // 네비게이션 관련
  targetUrl?: string;
  targetScreenId?: number;

  // 모달/팝업 관련
  modalTitle?: string;
  modalTitleBlocks?: Array<{
    // 🆕 블록 기반 제목 (우선순위 높음)
    id: string;
    type: "text" | "field";
    value: string; // type=text: 텍스트 내용, type=field: 컬럼명
    tableName?: string; // type=field일 때 테이블명
    label?: string; // type=field일 때 표시용 라벨
  }>;
  modalDescription?: string;
  modalSize?: "sm" | "md" | "lg" | "xl";
  popupWidth?: number;
  popupHeight?: number;

  // 🆕 모달 데이터 전달 옵션 (modal 액션 통합)
  passSelectedData?: boolean; // 선택된 데이터 전달 여부 (기본: true)
  autoDetectDataSource?: boolean; // 데이터 소스 자동 감지 (TableList/SplitPanel 등)
  dataSourceId?: string; // modalDataStore에서 데이터를 가져올 ID
  fieldMappings?: Array<{ sourceField: string; targetField: string }>; // 필드 매핑

  // 확인 메시지
  confirmMessage?: string;
  successMessage?: string;
  errorMessage?: string;

  // 제어관리 관련
  enableDataflowControl?: boolean;
  dataflowConfig?: any; // ButtonDataflowConfig 타입 (순환 참조 방지를 위해 any 사용)
  dataflowTiming?: "before" | "after" | "replace"; // 제어 실행 타이밍

  // 테이블 이력 보기 관련
  historyTableName?: string; // 이력을 조회할 테이블명 (자동 감지 또는 수동 지정)
  historyRecordIdField?: string; // PK 필드명 (기본: "id")
  historyRecordIdSource?: "selected_row" | "form_field" | "context"; // 레코드 ID 가져올 소스
  historyRecordLabelField?: string; // 레코드 라벨로 표시할 필드 (선택사항)
  historyDisplayColumn?: string; // 전체 이력에서 레코드 구분용 컬럼 (예: device_code, name)

  // 엑셀 다운로드 관련
  excelFileName?: string; // 다운로드할 파일명 (기본: 테이블명_날짜.xlsx)
  excelSheetName?: string; // 시트명 (기본: "Sheet1")
  excelIncludeHeaders?: boolean; // 헤더 포함 여부 (기본: true)

  // 엑셀 업로드 관련
  excelUploadMode?: "insert" | "update" | "upsert"; // 업로드 모드
  excelKeyColumn?: string; // 업데이트/Upsert 시 키 컬럼
  excelNumberingRuleId?: string; // 채번 규칙 ID (단일 테이블용)
  excelNumberingTargetColumn?: string; // 채번 적용 컬럼 (단일 테이블용)
  excelAfterUploadFlows?: Array<{ flowId: string; order: number }>; // 업로드 후 제어 실행

  // 바코드 스캔 관련
  barcodeTargetField?: string; // 스캔 결과를 입력할 필드명
  barcodeFormat?: "all" | "1d" | "2d"; // 바코드 포맷 (기본: "all")
  barcodeAutoSubmit?: boolean; // 스캔 후 자동 제출 여부

  // 코드 병합 관련
  mergeColumnName?: string; // 병합할 컬럼명 (예: "item_code")
  mergeShowPreview?: boolean; // 병합 전 미리보기 표시 여부 (기본: true)

  // 위치정보 관련
  geolocationTableName?: string; // 위치정보 저장 테이블명 (기본: 현재 화면 테이블)
  geolocationLatField?: string; // 위도를 저장할 필드명 (예: "latitude")
  geolocationLngField?: string; // 경도를 저장할 필드명 (예: "longitude")
  geolocationAccuracyField?: string; // 정확도를 저장할 필드명 (선택, 예: "accuracy")
  geolocationTimestampField?: string; // 타임스탬프를 저장할 필드명 (선택, 예: "location_time")
  geolocationHighAccuracy?: boolean; // 고정밀 모드 사용 여부 (기본: true)
  geolocationTimeout?: number; // 타임아웃 (ms, 기본: 10000)
  geolocationMaxAge?: number; // 캐시된 위치 최대 수명 (ms, 기본: 0)
  geolocationAutoSave?: boolean; // 위치 가져온 후 자동 저장 여부 (기본: false)
  geolocationKeyField?: string; // DB UPDATE 시 WHERE 조건에 사용할 키 필드 (예: "user_id")
  geolocationKeySourceField?: string; // 키 값 소스 (예: "__userId__" 또는 폼 필드명)
  geolocationUpdateField?: boolean; // 위치정보와 함께 추가 필드 변경 여부
  geolocationExtraTableName?: string; // 추가 필드 변경 대상 테이블 (다른 테이블 가능)
  geolocationExtraField?: string; // 추가로 변경할 필드명 (예: "status")
  geolocationExtraValue?: string | number | boolean; // 추가로 변경할 값 (예: "active")
  geolocationExtraKeyField?: string; // 다른 테이블의 키 필드 (예: "vehicle_id")
  geolocationExtraKeySourceField?: string; // 현재 폼에서 키 값을 가져올 필드 (예: "vehicle_id")

  // 🆕 두 번째 테이블 설정 (위치정보 + 상태변경을 각각 다른 테이블에)
  geolocationSecondTableEnabled?: boolean; // 두 번째 테이블 사용 여부
  geolocationSecondTableName?: string; // 두 번째 테이블명 (예: "vehicles")
  geolocationSecondMode?: "update" | "insert"; // 작업 모드 (기본: update)
  geolocationSecondField?: string; // 두 번째 테이블에서 변경할 필드명 (예: "status")
  geolocationSecondValue?: string | number | boolean; // 두 번째 테이블에서 변경할 값 (예: "inactive")
  geolocationSecondKeyField?: string; // 두 번째 테이블의 키 필드 (예: "id") - UPDATE 모드에서만 사용
  geolocationSecondKeySourceField?: string; // 현재 폼에서 키 값을 가져올 필드 (예: "vehicle_id") - UPDATE 모드에서만 사용
  geolocationSecondInsertFields?: Record<string, any>; // INSERT 모드에서 추가로 넣을 필드들

  // 🆕 연속 위치 추적 설정 (update_field 액션의 updateWithTracking 옵션용)
  trackingInterval?: number; // 위치 저장 주기 (ms, 기본: 10000 = 10초)
  trackingTripIdField?: string; // 운행 ID를 저장할 필드명 (예: "trip_id")
  trackingAutoGenerateTripId?: boolean; // 운행 ID 자동 생성 여부 (기본: true)
  trackingDepartureField?: string; // 출발지 필드명 (formData에서 가져옴)
  trackingArrivalField?: string; // 도착지 필드명 (formData에서 가져옴)
  trackingVehicleIdField?: string; // 차량 ID 필드명 (formData에서 가져옴)
  trackingStatusOnStart?: string; // 추적 시작 시 상태값 (예: "active")
  trackingStatusOnStop?: string; // 추적 종료 시 상태값 (예: "completed")
  trackingStatusField?: string; // 상태 필드명 (vehicles 테이블 등)
  trackingStatusTableName?: string; // 상태 변경 대상 테이블명
  trackingStatusKeyField?: string; // 상태 변경 키 필드 (예: "user_id")
  trackingStatusKeySourceField?: string; // 키 값 소스 (예: "__userId__")

  // 필드 값 교환 관련 (출발지 ↔ 목적지)
  swapFieldA?: string; // 교환할 첫 번째 필드명 (예: "departure")
  swapFieldB?: string; // 교환할 두 번째 필드명 (예: "destination")
  swapRelatedFields?: Array<{ fieldA: string; fieldB: string }>; // 함께 교환할 관련 필드들 (예: 위도/경도)

  // 필드 값 변경 관련 (특정 필드를 특정 값으로 변경)
  updateTargetField?: string; // 변경할 필드명 (예: "status")
  updateTargetValue?: string | number | boolean; // 변경할 값 (예: "active")
  updateAutoSave?: boolean; // 변경 후 자동 저장 여부 (기본: true)
  updateMultipleFields?: Array<{ field: string; value: string | number | boolean }>; // 여러 필드 동시 변경
  updateTableName?: string; // 대상 테이블명 (다른 테이블 UPDATE 시)
  updateKeyField?: string; // 키 필드명 (WHERE 조건에 사용)
  updateKeySourceField?: string; // 키 값 소스 (폼 필드명 또는 __userId__ 등 특수 키워드)

  // 🆕 필드 값 변경 + 위치정보 수집 (update_field 액션에서 사용)
  updateWithGeolocation?: boolean; // 위치정보도 함께 수집할지 여부
  updateGeolocationLatField?: string; // 위도 저장 필드
  updateGeolocationLngField?: string; // 경도 저장 필드

  // 🆕 필드 값 변경 + 연속 위치 추적 (update_field 액션에서 사용)
  updateWithTracking?: boolean; // 연속 위치 추적 사용 여부
  updateTrackingMode?: "start" | "stop"; // 추적 모드 (시작/종료)
  updateTrackingInterval?: number; // 위치 저장 주기 (ms, 기본: 10000)
  updateGeolocationAccuracyField?: string; // 정확도 저장 필드 (선택)
  updateGeolocationTimestampField?: string; // 타임스탬프 저장 필드 (선택)

  // 🆕 공차등록 연속 위치 추적 설정 (empty_vehicle 액션에서 사용)
  emptyVehicleTracking?: boolean; // 공차 상태에서 연속 위치 추적 여부 (기본: true)
  emptyVehicleTrackingInterval?: number; // 위치 저장 주기 (ms, 기본: 10000 = 10초)

  // 편집 관련 (수주관리 등 그룹별 다중 레코드 편집)
  editMode?: "modal" | "navigate" | "inline"; // 편집 모드
  editModalTitle?: string; // 편집 모달 제목
  editModalDescription?: string; // 편집 모달 설명
  groupByColumns?: string[]; // 같은 그룹의 여러 행을 함께 편집 (예: ["order_no"])

  // 데이터 전달 관련 (transferData 액션용)
  dataTransfer?: {
    // 소스 설정
    sourceComponentId: string; // 데이터를 가져올 컴포넌트 ID (테이블 등)
    sourceComponentType?: string; // 소스 컴포넌트 타입

    // 타겟 설정
    targetType: "component" | "screen"; // 타겟 타입 (같은 화면의 컴포넌트 or 다른 화면)

    // 타겟이 컴포넌트인 경우
    targetComponentId?: string; // 타겟 컴포넌트 ID

    // 타겟이 화면인 경우
    targetScreenId?: number; // 타겟 화면 ID

    // 데이터 매핑 규칙
    mappingRules: Array<{
      sourceField: string; // 소스 필드명
      targetField: string; // 타겟 필드명
      transform?: "sum" | "average" | "concat" | "first" | "last" | "count"; // 변환 함수
      defaultValue?: any; // 기본값
    }>;

    // 전달 옵션
    mode?: "append" | "replace" | "merge"; // 수신 모드 (기본: append)
    clearAfterTransfer?: boolean; // 전달 후 소스 데이터 초기화
    confirmBeforeTransfer?: boolean; // 전달 전 확인 메시지
    confirmMessage?: string; // 확인 메시지 내용

    // 검증
    validation?: {
      requireSelection?: boolean; // 선택 필수 (기본: true)
      minSelection?: number; // 최소 선택 개수
      maxSelection?: number; // 최대 선택 개수
    };
  };

  // 연관 데이터 버튼 모달 열기 관련
  relatedModalConfig?: {
    targetScreenId: number; // 열릴 모달 화면 ID
    componentId?: string; // 특정 RelatedDataButtons 컴포넌트 지정 (선택사항)
  };

  // 즉시 저장 (Quick Insert) 관련
  quickInsertConfig?: {
    targetTable: string; // 저장할 테이블명
    columnMappings: Array<{
      targetColumn: string; // 대상 테이블의 컬럼명
      sourceType: "component" | "leftPanel" | "fixed" | "currentUser"; // 값 소스 타입
      sourceComponentId?: string; // 컴포넌트에서 값을 가져올 경우 컴포넌트 ID
      sourceColumnName?: string; // 컴포넌트의 columnName (formData 접근용)
      sourceColumn?: string; // 좌측 패널 또는 컴포넌트의 특정 컬럼
      fixedValue?: any; // 고정값
      userField?: "userId" | "userName" | "companyCode"; // currentUser 타입일 때 사용할 필드
    }>;
    duplicateCheck?: {
      enabled: boolean; // 중복 체크 활성화 여부
      columns?: string[]; // 중복 체크할 컬럼들
      errorMessage?: string; // 중복 시 에러 메시지
    };
    afterInsert?: {
      refreshData?: boolean; // 저장 후 데이터 새로고침 (테이블리스트, 카드 디스플레이)
      clearComponents?: boolean; // 저장 후 컴포넌트 값 초기화
      showSuccessMessage?: boolean; // 성공 메시지 표시 여부 (기본: true)
      successMessage?: string; // 성공 메시지
    };
  };

  // 이벤트 버스 발송 관련 (event 액션용)
  eventConfig?: {
    eventName: string; // 발송할 이벤트 이름 (V2_EVENTS 키)
    eventPayload?: Record<string, any>; // 이벤트 페이로드 (requestId는 자동 생성)
  };

  // 범용 API 호출 관련 (apiCall 액션용)
  apiCallConfig?: {
    method: "GET" | "POST" | "PUT" | "DELETE";
    endpoint: string; // 예: "/api/production/generate-schedule"
    payloadMapping?: Record<string, string>; // formData 필드 → API body 필드 매핑
    staticPayload?: Record<string, any>; // 고정 페이로드 값
    useSelectedRows?: boolean; // true면 선택된 행 데이터를 body에 포함
    selectedRowsKey?: string; // 선택된 행 데이터의 key (기본: "items")
    refreshAfterSuccess?: boolean; // 성공 후 테이블 새로고침 (기본: true)
    confirmMessage?: string; // 실행 전 확인 메시지
  };
}

/**
 * 버튼 액션 실행 컨텍스트
 */
export interface ButtonActionContext {
  formData: Record<string, any>;
  originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
  screenId?: number;
  tableName?: string;
  userId?: string; // 🆕 현재 로그인한 사용자 ID
  userName?: string; // 🆕 현재 로그인한 사용자 이름
  companyCode?: string; // 🆕 현재 사용자의 회사 코드
  onFormDataChange?: (fieldName: string, value: any) => void;
  onClose?: () => void;
  onRefresh?: () => void;
  onFlowRefresh?: () => void; // 플로우 새로고침 콜백
  onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백

  // 테이블 선택된 행 정보 (다중 선택 액션용)
  selectedRows?: any[];
  selectedRowsData?: any[];

  // 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용)
  flowSelectedData?: any[];
  flowSelectedStepId?: number | null;

  // 🆕 같은 화면의 모든 컴포넌트 (TableList 자동 감지용)
  allComponents?: any[];

  // 제어 실행을 위한 추가 정보
  buttonId?: string;

  // 테이블 정렬 정보 (엑셀 다운로드용)
  sortBy?: string; // 정렬 컬럼명
  sortOrder?: "asc" | "desc"; // 정렬 방향
  columnOrder?: string[]; // 컬럼 순서 (사용자가 드래그앤드롭으로 변경한 순서)
  tableDisplayData?: any[]; // 화면에 표시된 데이터 (정렬 및 컬럼 순서 적용됨)

  // 🆕 엑셀 다운로드 개선을 위한 추가 필드
  filterConditions?: Record<string, any>; // 필터 조건 (예: { status: "active", dept: "dev" })
  searchTerm?: string; // 검색어
  searchColumn?: string; // 검색 대상 컬럼
  visibleColumns?: string[]; // 화면에 표시 중인 컬럼 목록 (순서 포함)
  columnLabels?: Record<string, string>; // 컬럼명 → 라벨명 매핑 (한글)
  currentPage?: number; // 현재 페이지
  pageSize?: number; // 페이지 크기
  totalItems?: number; // 전체 항목 수

  // 🆕 컴포넌트별 설정 (parentDataMapping 등)
  componentConfigs?: Record<string, any>; // 컴포넌트 ID → 컴포넌트 설정

  // 🆕 분할 패널 부모 데이터 (좌측 화면에서 선택된 데이터)
  splitPanelParentData?: Record<string, any>;

  // 🆕 분할 패널 컨텍스트 (quickInsert 등에서 좌측 패널 데이터 접근용)
  splitPanelContext?: {
    selectedLeftData?: Record<string, any>;
    refreshRightPanel?: () => void;
  };

  // 🆕 저장된 데이터 (저장 후 제어 실행 시 플로우에 전달)
  savedData?: any;
}

/**
 * 🆕 특수 키워드를 실제 값으로 변환하는 헬퍼 함수
 * 지원하는 키워드:
 * - __userId__ : 로그인한 사용자 ID
 * - __userName__ : 로그인한 사용자 이름
 * - __companyCode__ : 로그인한 사용자의 회사 코드
 * - __screenId__ : 현재 화면 ID
 * - __tableName__ : 현재 테이블명
 */
export function resolveSpecialKeyword(sourceField: string | undefined, context: ButtonActionContext): any {
  if (!sourceField) return undefined;

  // 특수 키워드 처리
  switch (sourceField) {
    case "__userId__":
      return context.userId;
    case "__userName__":
      return context.userName;
    case "__companyCode__":
      return context.companyCode;
    case "__screenId__":
      return context.screenId;
    case "__tableName__":
      return context.tableName;
    default:
      // 일반 폼 데이터에서 가져오기
      return context.formData?.[sourceField];
  }
}

/**
 * 버튼 액션 실행기
 */
export class ButtonActionExecutor {
  /**
   * 액션 실행
   */
  static async executeAction(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    try {
      // 확인 로직은 컴포넌트에서 처리하므로 여기서는 제거

      switch (config.type) {
        case "save":
          return await this.handleSave(config, context);

        case "delete":
          return await this.handleDelete(config, context);

        case "copy":
          return await this.handleCopy(config, context);

        case "navigate":
          return this.handleNavigate(config, context);

        case "openModalWithData":
          // deprecated: modal로 통합 (하위 호환성 유지)
          return await this.handleModal({ ...config, passSelectedData: true, autoDetectDataSource: true }, context);

        case "openRelatedModal":
          return await this.handleOpenRelatedModal(config, context);

        case "modal":
          return await this.handleModal(config, context);

        case "edit":
          return await this.handleEdit(config, context);

        case "control":
          return this.handleControl(config, context);

        case "view_table_history":
          return this.handleViewTableHistory(config, context);

        case "excel_download":
          return await this.handleExcelDownload(config, context);

        case "excel_upload":
          return await this.handleExcelUpload(config, context);

        case "multi_table_excel_upload":
          return await this.handleMultiTableExcelUpload(config, context);

        case "barcode_scan":
          return await this.handleBarcodeScan(config, context);

        case "code_merge":
          return await this.handleCodeMerge(config, context);

        case "transferData":
          return await this.handleTransferData(config, context);

        // case "empty_vehicle":
        //   return await this.handleEmptyVehicle(config, context);

        case "operation_control":
          return await this.handleOperationControl(config, context);

        case "swap_fields":
          return await this.handleSwapFields(config, context);

        case "quickInsert":
          return await this.handleQuickInsert(config, context);

        case "event":
          return await this.handleEvent(config, context);

        case "apiCall":
          return await this.handleApiCall(config, context);

        case "approval":
          return this.handleApproval(config, context);

        default:
          console.warn(`지원되지 않는 액션 타입: ${config.type}`);
          return false;
      }
    } catch (error: any) {
      console.error("버튼 액션 실행 오류:", error);
      const actualMsg =
        error?.response?.data?.message ||
        error?.response?.data?.error?.message ||
        error?.message ||
        "";
      const title = actualMsg || config.errorMessage || `'${config.label || config.type}' 실행에 실패했습니다`;
      toast.error(title);
      return false;
    }
  }

  /**
   * 필수 항목 검증
   */
  private static validateRequiredFields(context: ButtonActionContext): { isValid: boolean; missingFields: string[] } {
    const missingFields: string[] = [];
    const { formData, allComponents, tableName } = context;

    if (!allComponents || allComponents.length === 0) {
      console.log("⚠️ [validateRequiredFields] allComponents 없음 - 검증 스킵");
      return { isValid: true, missingFields: [] };
    }

    allComponents.forEach((component: any) => {
      const columnName = component.columnName || component.style?.columnName;

      // 수동 required 속성 + NOT NULL 메타데이터 기반 필수 여부 통합 체크
      const manualRequired =
        component.required === true ||
        component.style?.required === true ||
        component.componentConfig?.required === true;
      const notNullRequired = isColumnRequiredByMeta(tableName, columnName);
      const isRequired = manualRequired || notNullRequired;

      const label = component.label || component.style?.label || columnName;

      if (isRequired && columnName) {
        const value = formData[columnName];
        if (value === null || value === undefined || (typeof value === "string" && value.trim() === "")) {
          missingFields.push(label || columnName);
        }
      }
    });

    return {
      isValid: missingFields.length === 0,
      missingFields,
    };
  }

  /**
   * 저장 액션 처리 (INSERT/UPDATE 자동 판단 - DB 기반)
   */
  private static saveCallCount = 0; // 🆕 호출 횟수 추적
  private static saveLock: Map<string, number> = new Map(); // 🆕 중복 호출 방지 락

  private static async handleSave(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    this.saveCallCount++;
    const callId = this.saveCallCount;

    const { formData, originalData, tableName, screenId, onSave } = context;

    // 🆕 중복 호출 방지: 같은 screenId + tableName + formData 조합으로 2초 내 재호출 시 무시
    const formDataHash = JSON.stringify(Object.keys(formData).sort());
    const lockKey = `${screenId}-${tableName}-${formDataHash}`;
    const lastCallTime = this.saveLock.get(lockKey) || 0;
    const now = Date.now();
    const timeDiff = now - lastCallTime;

    if (timeDiff < 2000) {
      return true; // 중복 호출은 성공으로 처리
    }

    this.saveLock.set(lockKey, now);

    // ✅ 필수 항목 검증

    const requiredValidation = this.validateRequiredFields(context);
    if (!requiredValidation.isValid) {
      console.log("❌ [handleSave] 필수 항목 누락:", requiredValidation.missingFields);
      toast.error(`필수 항목을 입력해주세요: ${requiredValidation.missingFields.join(", ")}`);
      return false;
    }

    // ✅ 입력 형식 검증 (이메일, 전화번호, URL 등)
    const formatValidationDetail = { errors: [] as Array<{ columnName: string; message: string }> };
    window.dispatchEvent(
      new CustomEvent("validateFormInputs", {
        detail: formatValidationDetail,
      }),
    );
    // 약간의 대기 (이벤트 핸들러가 동기적으로 실행되지만 안전을 위해)
    await new Promise((resolve) => setTimeout(resolve, 50));

    if (formatValidationDetail.errors.length > 0) {
      const errorMessages = formatValidationDetail.errors.map((e) => e.message);
      console.log("❌ [handleSave] 입력 형식 검증 실패:", formatValidationDetail.errors);
      toast.error(`입력 형식을 확인해주세요: ${errorMessages.join(", ")}`);
      return false;
    }

    // beforeFormSave 이벤트 발송 (BomItemEditor 등 서브 컴포넌트의 저장 처리)
    const beforeSaveEventDetail = {
      formData: context.formData,
      skipDefaultSave: false,
      validationFailed: false,
      validationErrors: [] as string[],
      pendingPromises: [] as Promise<void>[],
    };
    window.dispatchEvent(
      new CustomEvent("beforeFormSave", {
        detail: beforeSaveEventDetail,
      }),
    );

    if (beforeSaveEventDetail.pendingPromises.length > 0) {
      await Promise.all(beforeSaveEventDetail.pendingPromises);
    } else {
      await new Promise((resolve) => setTimeout(resolve, 100));
    }

    if (beforeSaveEventDetail.validationFailed) {
      console.log("❌ [handleSave] 검증 실패로 저장 중단:", beforeSaveEventDetail.validationErrors);
      return false;
    }

    // EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용
    if (onSave) {
      try {
        await onSave();

        // 모달 저장 후에도 제어관리 실행 (onSave 경로에서도 dataflow 지원)
        if (config.enableDataflowControl && config.dataflowConfig) {
          console.log("📦 [handleSave] onSave 콜백 후 제어관리 실행 시작");
          const contextWithSavedData = {
            ...context,
            savedData: context.formData,
            selectedRowsData: context.selectedRowsData || [],
          };
          await this.executeAfterSaveControl(config, contextWithSavedData);
        }

        return true;
      } catch (error: any) {
        console.error("❌ [handleSave] onSave 콜백 실행 오류:", error);
        const msg =
          error?.response?.data?.message ||
          error?.response?.data?.error?.message ||
          error?.message ||
          "저장에 실패했습니다.";
        toast.error(msg);
        return false;
      }
    }

    console.log("⚠️ [handleSave] onSave 콜백 없음 - 기본 저장 로직 실행");

    // skipDefaultSave 플래그 확인
    if (beforeSaveEventDetail.skipDefaultSave) {
      return true;
    }

    // _tableSection_ 데이터 확인 (TableSectionRenderer 사용 시)
    const hasTableSectionData = Object.keys(context.formData || {}).some(
      (k) => k.startsWith("_tableSection_") || k.startsWith("__tableSection_"),
    );

    console.log("⚠️ [handleSave] 기본 저장 로직 실행 (onSave 콜백 없음 또는 _tableSection_ 데이터 있음)");

    // 🆕 렉 구조 컴포넌트 일괄 저장 감지
    let rackStructureLocations: any[] | undefined;
    let rackStructureFieldKey = "_rackStructureLocations";
    let hasEmptyRackStructureField = false;

    // formData에서 렉 구조 데이터 또는 빈 배열 찾기
    for (const [key, value] of Object.entries(context.formData || {})) {
      // 배열인 경우만 체크
      if (Array.isArray(value)) {
        if (value.length > 0 && value[0]) {
          const firstItem = value[0];
          const isNewFormat =
            firstItem.location_code &&
            firstItem.location_name &&
            firstItem.row_num !== undefined &&
            firstItem.level_num !== undefined;
          const isOldFormat =
            firstItem.locationCode &&
            firstItem.locationName &&
            firstItem.rowNum !== undefined &&
            firstItem.levelNum !== undefined;

          if (isNewFormat || isOldFormat) {
            rackStructureLocations = value;
            rackStructureFieldKey = key;
            break;
          }
        } else if (value.length === 0 && key.startsWith("comp_")) {
          // comp_로 시작하는 빈 배열은 렉 구조 컴포넌트일 가능성 있음
          // allComponents에서 확인 (v1, v2 모두 지원)
          const rackStructureComponentInLayout = context.allComponents?.find(
            (comp: any) =>
              comp.type === "component" &&
              (comp.componentId === "rack-structure" || comp.componentId === "v2-rack-structure") &&
              comp.columnName === key,
          );
          if (rackStructureComponentInLayout) {
            hasEmptyRackStructureField = true;
            rackStructureFieldKey = key;
          }
        }
      }
    }

    // 렉 구조 컴포넌트가 있지만 미리보기 데이터가 없는 경우
    if (hasEmptyRackStructureField && (!rackStructureLocations || rackStructureLocations.length === 0)) {
      alert("미리보기를 먼저 생성해주세요.\n\n렉 구조 조건을 설정한 후 '미리보기 생성' 버튼을 클릭하세요.");
      return false;
    }

    // 렉 구조 등록 화면 감지 (warehouse_location 테이블 + zone 필드 있음 + 렉 구조 데이터 없음)
    // floor는 선택 입력이므로 감지 조건에서 제외
    const isRackStructureScreen =
      context.tableName === "warehouse_location" &&
      context.formData?.zone &&
      !rackStructureLocations;

    if (isRackStructureScreen) {
      alert(
        "렉 구조 등록 화면입니다.\n\n" +
          "미리보기를 먼저 생성해주세요.\n" +
          "- 중복된 위치가 있으면 미리보기가 생성되지 않습니다.\n" +
          "- 기존 데이터를 삭제하거나 다른 열/단을 선택해주세요.",
      );
      return false;
    }

    // 렉 구조 데이터가 있으면 일괄 저장
    if (rackStructureLocations && rackStructureLocations.length > 0) {
      return await this.handleRackStructureBatchSave(config, context, rackStructureLocations, rackStructureFieldKey);
    }

    // 🆕 SelectedItemsDetailInput 배치 저장 처리 (fieldGroups 구조)

    // 🔧 formData 자체가 배열인 경우 (ScreenModal의 그룹 레코드 수정)
    if (Array.isArray(context.formData)) {
      console.log(
        "⚠️ [handleSave] formData가 배열입니다 - SelectedItemsDetailInput이 이미 처리했으므로 일반 저장 건너뜀",
      );
      console.log("⚠️ [handleSave] formData 배열:", context.formData);
      // ✅ SelectedItemsDetailInput이 이미 UPSERT를 실행했으므로 일반 저장을 건너뜀
      return true; // 성공으로 반환
    }

    const selectedItemsKeys = Object.keys(context.formData).filter((key) => {
      const value = context.formData[key];

      return Array.isArray(value) && value.length > 0 && value[0]?.originalData && value[0]?.fieldGroups;
    });

    if (selectedItemsKeys.length > 0) {
      return await this.handleBatchSave(config, context, selectedItemsKeys);
    } else {
      console.log("⚠️ [handleSave] SelectedItemsDetailInput 데이터 감지 실패 - 일반 저장 진행");
    }

    // 🆕 RepeaterFieldGroup JSON 문자열 파싱 및 저장 처리
    // formData에 JSON 배열 문자열이 저장된 경우 처리 (반복_필드_그룹 등)
    const repeaterJsonKeys = Object.keys(context.formData).filter((key) => {
      const value = context.formData[key];
      if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) {
        try {
          const parsed = JSON.parse(value);
          return Array.isArray(parsed) && parsed.length > 0 && parsed[0]._targetTable;
        } catch {
          return false;
        }
      }
      return false;
    });

    if (repeaterJsonKeys.length > 0) {
      // 🎯 채번 규칙 할당 처리 (RepeaterFieldGroup 저장 전에 실행)
      // 🔧 수정 모드 체크: formData.id가 존재하면 UPDATE 모드이므로 채번 코드 재할당 금지
      const isEditModeRepeater =
        context.formData.id !== undefined && context.formData.id !== null && context.formData.id !== "";

      const fieldsWithNumberingRepeater: Record<string, string> = {};

      // formData에서 채번 규칙이 설정된 필드 찾기
      for (const [key, value] of Object.entries(context.formData)) {
        if (key.endsWith("_numberingRuleId") && value) {
          const fieldName = key.replace("_numberingRuleId", "");
          fieldsWithNumberingRepeater[fieldName] = value as string;
        }
      }

      // 🔧 수정 모드에서는 채번 코드 할당 건너뛰기 (기존 번호 유지)
      // 신규 등록 모드에서만 allocateCode 호출하여 새 번호 할당
      if (Object.keys(fieldsWithNumberingRepeater).length > 0 && !isEditModeRepeater) {
        const { allocateNumberingCode } = await import("@/lib/api/numberingRule");

        for (const [fieldName, ruleId] of Object.entries(fieldsWithNumberingRepeater)) {
          try {
            // 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
            const userInputCode = context.formData[fieldName] as string;
            const allocateResult = await allocateNumberingCode(ruleId, userInputCode, context.formData);

            if (allocateResult.success && allocateResult.data?.generatedCode) {
              const newCode = allocateResult.data.generatedCode;

              context.formData[fieldName] = newCode;
            } else {
              console.warn(`⚠️ [handleSave-RepeaterFieldGroup] ${fieldName} 코드 할당 실패:`, allocateResult.error);
            }
          } catch (allocateError) {
            console.error(`❌ [handleSave-RepeaterFieldGroup] ${fieldName} 코드 할당 오류:`, allocateError);
          }
        }
      } else if (isEditModeRepeater) {
      }

      // 🆕 상단 폼 데이터(마스터 정보) 추출
      // RepeaterFieldGroup JSON과 컴포넌트 키를 제외한 나머지가 마스터 정보
      const masterFields: Record<string, any> = {};
      Object.keys(context.formData).forEach((fieldKey) => {
        // 제외 조건
        if (fieldKey.startsWith("comp_")) return;
        if (fieldKey.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) return;
        if (fieldKey.endsWith("_label") || fieldKey.endsWith("_value_label")) return;

        const value = context.formData[fieldKey];

        // JSON 배열 문자열 제외 (RepeaterFieldGroup 데이터)
        if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) return;

        // 객체 타입인 경우 (범용_폼_모달 등) 내부 필드를 펼쳐서 추가
        if (typeof value === "object" && value !== null && !Array.isArray(value)) {
          Object.entries(value).forEach(([innerKey, innerValue]) => {
            if (innerKey.endsWith("_label") || innerKey.endsWith("_value_label")) return;
            if (innerValue !== undefined && innerValue !== null && innerValue !== "") {
              masterFields[innerKey] = innerValue;
            }
          });
          return;
        }

        // 유효한 값만 포함
        if (value !== undefined && value !== null && value !== "") {
          masterFields[fieldKey] = value;
        }
      });

      for (const key of repeaterJsonKeys) {
        try {
          const parsedData = JSON.parse(context.formData[key]);
          const repeaterTargetTable = parsedData[0]?._targetTable;

          if (!repeaterTargetTable) {
            console.warn(`⚠️ [handleSave] RepeaterFieldGroup targetTable 없음 (key: ${key})`);
            continue;
          }

          // 🆕 품목 고유 필드 목록 (RepeaterFieldGroup 설정에서 가져옴)
          // 첫 번째 아이템의 _repeaterFields에서 추출
          const repeaterFields: string[] = parsedData[0]?._repeaterFields || [];
          const itemOnlyFields = new Set([...repeaterFields, "id"]); // id는 항상 포함

          for (const item of parsedData) {
            // 메타 필드 제거
            const {
              _targetTable,
              _isNewItem,
              _existingRecord,
              _originalItemIds,
              _deletedItemIds,
              _repeaterFields,
              _subDataSelection,
              _subDataMaxValue,
              ...itemData
            } = item;

            // 🔧 품목 고유 필드만 추출 (RepeaterFieldGroup 설정 기반)
            const itemOnlyData: Record<string, any> = {};
            Object.keys(itemData).forEach((field) => {
              if (itemOnlyFields.has(field)) {
                itemOnlyData[field] = itemData[field];
              }
            });

            // 🆕 하위 데이터 선택에서 값 추출 (subDataSource 설정 기반)
            // 필드 정의에서 subDataSource.enabled가 true이고 sourceColumn이 설정된 필드만 처리
            if (_subDataSelection && typeof _subDataSelection === "object") {
              // _repeaterFieldsConfig에서 subDataSource 설정 확인
              const fieldsConfig = item._repeaterFieldsConfig as
                | Array<{
                    name: string;
                    subDataSource?: { enabled: boolean; sourceColumn: string };
                  }>
                | undefined;

              if (fieldsConfig && Array.isArray(fieldsConfig)) {
                fieldsConfig.forEach((fieldConfig) => {
                  if (fieldConfig.subDataSource?.enabled && fieldConfig.subDataSource?.sourceColumn) {
                    const targetField = fieldConfig.name; // 필드명 = 저장할 컬럼명
                    const sourceColumn = fieldConfig.subDataSource.sourceColumn;
                    const sourceValue = _subDataSelection[sourceColumn];

                    if (sourceValue !== undefined && sourceValue !== null) {
                      itemOnlyData[targetField] = sourceValue;
                    }
                  }
                });
              } else {
                // 하위 호환성: fieldsConfig가 없으면 기존 방식 사용
                Object.keys(_subDataSelection).forEach((subDataKey) => {
                  if (
                    itemOnlyData[subDataKey] === undefined ||
                    itemOnlyData[subDataKey] === null ||
                    itemOnlyData[subDataKey] === ""
                  ) {
                    const subDataValue = _subDataSelection[subDataKey];
                    if (subDataValue !== undefined && subDataValue !== null) {
                      itemOnlyData[subDataKey] = subDataValue;
                    }
                  }
                });
              }
            }

            // 🔧 마스터 정보 + 품목 고유 정보 병합
            // masterFields: 상단 폼에서 수정한 최신 마스터 정보
            // itemOnlyData: 품목 고유 필드만 (품번, 품명, 수량 등)
            const dataWithMeta: Record<string, unknown> = {
              ...masterFields, // 상단 마스터 정보 (최신)
              ...itemOnlyData, // 품목 고유 필드만
              created_by: context.userId,
              updated_by: context.userId,
              company_code: context.companyCode,
            };

            // 불필요한 필드 제거
            Object.keys(dataWithMeta).forEach((field) => {
              if (field.endsWith("_label") || field.endsWith("_value_label") || field.endsWith("_numberingRuleId")) {
                delete dataWithMeta[field];
              }
            });

            // 새 레코드 vs 기존 레코드 판단
            const isNewRecord = _isNewItem || !item.id || item.id === "" || item.id === undefined;

            if (isNewRecord) {
              // INSERT - DynamicFormApi 사용하여 제어관리 실행
              delete dataWithMeta.id;

              const insertResult = await DynamicFormApi.saveFormData({
                screenId: context.screenId || 0,
                tableName: repeaterTargetTable,
                data: dataWithMeta as Record<string, any>,
              });
            } else if (item.id && _existingRecord === true) {
              // UPDATE - 기존 레코드
              const originalData = { id: item.id };
              const updatedData = { ...dataWithMeta, id: item.id };

              const updateResult = await apiClient.put(`/table-management/tables/${repeaterTargetTable}/edit`, {
                originalData,
                updatedData,
              });
            }
          }
        } catch (err) {
          console.error(`❌ [handleSave] RepeaterFieldGroup 저장 실패 (key: ${key}):`, err);
        }
      }

      // RepeaterFieldGroup 저장 완료 후 새로고침

      context.onRefresh?.();
      context.onFlowRefresh?.();
      window.dispatchEvent(new CustomEvent("closeEditModal"));
      window.dispatchEvent(new CustomEvent("saveSuccessInModal"));

      return true;
    }

    // 🆕 Universal Form Modal 테이블 섹션 병합 저장 처리
    // 범용_폼_모달 내부에 _tableSection_ 데이터가 있는 경우 공통 필드 + 개별 품목 병합 저장
    const universalFormModalResult = await this.handleUniversalFormModalTableSectionSave(config, context, formData);
    if (universalFormModalResult.handled) {
      return universalFormModalResult.success;
    }

    // 폼 유효성 검사
    if (config.validateForm) {
      const validation = this.validateFormData(formData);
      if (!validation.isValid) {
        toast.error(`입력값을 확인해주세요: ${validation.errors.join(", ")}`);
        return false;
      }
    }

    try {
      // API 엔드포인트가 지정된 경우
      if (config.saveEndpoint) {
        const response = await fetch(config.saveEndpoint, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify(formData),
        });

        if (!response.ok) {
          throw new Error(`저장 실패: ${response.statusText}`);
        }
      }

      // saveResult를 상위 스코프에서 정의 (repeaterSave 이벤트에서 사용)
      let saveResult: { success: boolean; data?: any; message?: string } | undefined;

      // 제어 실행 데이터를 상위 스코프에서 정의 (리피터 저장 완료 후 실행 위해)
      let pendingDataflowControl: { config: ButtonActionConfig; context: ButtonActionContext } | null = null;

      if (tableName && screenId) {
        // DB에서 실제 기본키 조회하여 INSERT/UPDATE 자동 판단
        const primaryKeyResult = await DynamicFormApi.getTablePrimaryKeys(tableName);

        if (!primaryKeyResult.success) {
          throw new Error(primaryKeyResult.message || "기본키 조회에 실패했습니다.");
        }

        const primaryKeys = primaryKeyResult.data || [];
        let primaryKeyValue = this.extractPrimaryKeyValueFromDB(formData, primaryKeys);

        // 🔧 수정: originalData가 있고 실제 데이터가 있으면 UPDATE 모드로 처리
        // originalData는 수정 버튼 클릭 시 editData로 전달되어 context.originalData로 설정됨
        // 빈 객체 {}도 truthy이므로 Object.keys로 실제 데이터 유무 확인
        const hasRealOriginalData = originalData && Object.keys(originalData).length > 0;

        // 🆕 폴백: formData에 PK가 없으면 originalData에서 PK 추출
        // 수정 모달에서 id 입력 필드가 없는 경우 formData에 id가 포함되지 않음
        if (!primaryKeyValue && hasRealOriginalData) {
          primaryKeyValue = this.extractPrimaryKeyValueFromDB(originalData, primaryKeys);
          if (primaryKeyValue) {
            // formData에도 PK 값을 주입하여 UPDATE 쿼리에서 사용 가능하게 함
            const pkColumn = primaryKeys[0];
            formData[pkColumn] = primaryKeyValue;
            console.log(`🔑 [handleSave] originalData에서 PK 복원: ${pkColumn} = ${primaryKeyValue}`);
          }
        }

        // 🆕 폴백 로직: originalData가 없어도 formData에 id가 있으면 UPDATE로 판단
        // 조건부 컨테이너 등에서 originalData 전달이 누락되는 경우를 처리
        const hasIdInFormData = formData.id !== undefined && formData.id !== null && formData.id !== "";
        const isUpdate = (hasRealOriginalData || hasIdInFormData) && !!primaryKeyValue;

        if (isUpdate) {
          // UPDATE 처리 - 부분 업데이트 사용 (원본 데이터가 있는 경우)

          // 🔧 UPDATE 전 formData 배열 → 쉼표 구분 문자열 변환 (리피터 데이터 제외)
          for (const key of Object.keys(formData)) {
            const value = formData[key];
            if (Array.isArray(value)) {
              // 리피터 데이터인지 확인 (객체 배열이고 _targetTable 또는 _isNewItem이 있으면 리피터)
              const isRepeaterData =
                value.length > 0 &&
                typeof value[0] === "object" &&
                value[0] !== null &&
                ("_targetTable" in value[0] || "_isNewItem" in value[0] || "_existingRecord" in value[0]);

              if (!isRepeaterData) {
                // 🔧 다중 선택 배열 → 쉼표 구분 문자열로 변환
                const stringValue = value
                  .map((v) => (typeof v === "number" ? String(v) : v))
                  .filter((v) => v !== null && v !== undefined && v !== "")
                  .join(",");
                console.log(`🔧 [handleSave UPDATE] 배열→문자열 변환: ${key}`, {
                  original: value,
                  converted: stringValue,
                });
                formData[key] = stringValue;
              }
            }
          }

          if (hasRealOriginalData) {
            // 부분 업데이트: 변경된 필드만 업데이트

            saveResult = await DynamicFormApi.updateFormDataPartial(primaryKeyValue, originalData, formData, tableName);
          } else {
            // 전체 업데이트 (originalData 없이 id로 UPDATE 판단된 경우)

            saveResult = await DynamicFormApi.updateFormData(primaryKeyValue, {
              tableName,
              data: formData,
            });
          }
        } else {
          // INSERT 처리

          // 🆕 자동으로 작성자 정보 추가
          if (!context.userId) {
            throw new Error("사용자 정보를 불러올 수 없습니다. 다시 로그인해주세요.");
          }

          const writerValue = context.userId;
          const companyCodeValue = context.companyCode || "";

          // console.log("👤 [buttonActions] 사용자 정보:", {
          //   userId: context.userId,
          //   userName: context.userName,
          //   companyCode: context.companyCode,
          //   formDataWriter: formData.writer,
          //   formDataCompanyCode: formData.company_code,
          //   defaultWriterValue: writerValue,
          //   companyCodeValue,
          // });

          // 🎯 채번 규칙 할당 처리 (저장 시점에 실제 순번 증가)

          const fieldsWithNumbering: Record<string, string> = {};

          // formData에서 채번 규칙이 설정된 필드 찾기
          for (const [key, value] of Object.entries(formData)) {
            if (key.endsWith("_numberingRuleId") && value) {
              const fieldName = key.replace("_numberingRuleId", "");
              fieldsWithNumbering[fieldName] = value as string;
            }
          }

          // 🔥 저장 시점에 allocateCode 호출하여 실제 순번 증가
          if (Object.keys(fieldsWithNumbering).length > 0) {
            const { allocateNumberingCode } = await import("@/lib/api/numberingRule");

            let hasAllocationFailure = false;
            const failedFields: string[] = [];

            for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) {
              try {
                // 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
                const userInputCode = formData[fieldName] as string;
                const allocateResult = await allocateNumberingCode(ruleId, userInputCode, formData);

                if (allocateResult.success && allocateResult.data?.generatedCode) {
                  const newCode = allocateResult.data.generatedCode;

                  formData[fieldName] = newCode;
                } else {
                  console.warn(`⚠️ ${fieldName} 코드 할당 실패:`, allocateResult.error);
                  // 🆕 기존 값이 빈 문자열이면 실패로 표시
                  if (!formData[fieldName] || formData[fieldName] === "") {
                    hasAllocationFailure = true;
                    failedFields.push(fieldName);
                  }
                }
              } catch (allocateError) {
                console.error(`❌ ${fieldName} 코드 할당 오류:`, allocateError);
                // 🆕 기존 값이 빈 문자열이면 실패로 표시
                if (!formData[fieldName] || formData[fieldName] === "") {
                  hasAllocationFailure = true;
                  failedFields.push(fieldName);
                }
              }
            }

            // 🆕 채번 규칙 할당 실패 시 저장 중단
            if (hasAllocationFailure) {
              const fieldNames = failedFields.join(", ");
              toast.error(`채번 규칙 할당에 실패했습니다 (${fieldNames}). 화면 설정에서 채번 규칙을 확인해주세요.`);
              console.error(`❌ 채번 규칙 할당 실패로 저장 중단. 실패 필드: ${fieldNames}`);
              console.error("💡 해결 방법: 화면관리에서 해당 필드의 채번 규칙 설정을 확인하세요.");
              return false;
            }
          }

          // 🆕 분할 패널 부모 데이터 병합 (좌측 화면에서 선택된 데이터)
          // 🔧 중요: 신규 등록 시에는 연결 필드(equipment_code 등)만 병합해야 함
          // 모든 필드를 병합하면 동일한 컬럼명이 있을 때 부모 값이 들어가는 문제 발생
          // 예: 설비의 manufacturer가 소모품의 manufacturer로 들어감
          const rawSplitPanelData = context.splitPanelParentData || {};

          // INSERT 모드에서는 연결에 필요한 필드만 추출
          const cleanedSplitPanelData: Record<string, any> = {};

          // 필수 연결 필드: company_code (멀티테넌시)
          if (rawSplitPanelData.company_code) {
            cleanedSplitPanelData.company_code = rawSplitPanelData.company_code;
          }

          // 연결 필드 패턴으로 자동 감지 (equipment_code, xxx_code, xxx_id 패턴)
          const linkFieldPatterns = ["_code", "_id"];
          const excludeFields = [
            "id",
            "company_code",
            "created_date",
            "updated_date",
            "created_at",
            "updated_at",
            "writer",
            "created_by",
            "updated_by",
          ];

          for (const [key, value] of Object.entries(rawSplitPanelData)) {
            if (excludeFields.includes(key)) continue;
            if (value === undefined || value === null) continue;

            // 연결 필드 패턴 확인
            const isLinkField = linkFieldPatterns.some((pattern) => key.endsWith(pattern));
            if (isLinkField) {
              cleanedSplitPanelData[key] = value;
            }
          }

          if (Object.keys(rawSplitPanelData).length > 0) {
          }

          const dataWithUserInfo = {
            ...cleanedSplitPanelData, // 정리된 분할 패널 부모 데이터 먼저 적용
            ...formData, // 폼 데이터가 우선 (덮어쓰기 가능)
            writer: formData.writer || writerValue, // ✅ 입력값 우선, 없으면 userId
            created_by: writerValue, // created_by는 항상 로그인한 사람
            updated_by: writerValue, // updated_by는 항상 로그인한 사람
            company_code: formData.company_code || companyCodeValue, // ✅ 입력값 우선, 없으면 user.companyCode
          };

          // 🔧 formData에서도 id 제거 (신규 INSERT이므로)
          if ("id" in dataWithUserInfo && !formData.id) {
            delete dataWithUserInfo.id;
          }

          // _numberingRuleId 필드 제거 (실제 저장하지 않음)
          for (const key of Object.keys(dataWithUserInfo)) {
            if (key.endsWith("_numberingRuleId")) {
              delete dataWithUserInfo[key];
            }
          }

          // 🆕 배열 데이터 처리 - 마스터 테이블에는 배열 데이터를 저장하지 않음
          // 리피터 데이터는 별도의 RepeaterFieldGroup/V2Repeater 저장 로직에서 처리됨
          // 🔧 단순 배열(다중 선택)은 쉼표 구분 문자열로 변환, 리피터 데이터 배열은 제거
          for (const key of Object.keys(dataWithUserInfo)) {
            const value = dataWithUserInfo[key];
            if (Array.isArray(value)) {
              // 리피터 데이터인지 확인 (객체 배열이고 _targetTable 또는 _isNewItem이 있으면 리피터)
              const isRepeaterData =
                value.length > 0 &&
                typeof value[0] === "object" &&
                value[0] !== null &&
                ("_targetTable" in value[0] || "_isNewItem" in value[0] || "_existingRecord" in value[0]);

              if (isRepeaterData) {
                // 리피터 데이터는 제거 (별도 저장 로직에서 처리)
                delete dataWithUserInfo[key];
              } else {
                // 🔧 다중 선택 배열 → 쉼표 구분 문자열로 변환
                const stringValue = value
                  .map((v) => (typeof v === "number" ? String(v) : v))
                  .filter((v) => v !== null && v !== undefined && v !== "")
                  .join(",");
                console.log(`🔧 [handleSave] 배열→문자열 변환: ${key}`, { original: value, converted: stringValue });
                dataWithUserInfo[key] = stringValue;
              }
            }
          }

          // 🆕 반복 필드 그룹에서 삭제된 항목 처리
          // formData의 각 필드에서 _deletedItemIds가 있는지 확인

          for (const [key, value] of Object.entries(dataWithUserInfo)) {
            let parsedValue = value;

            // JSON 문자열인 경우 파싱 시도
            if (typeof value === "string" && value.startsWith("[")) {
              try {
                parsedValue = JSON.parse(value);
              } catch (e) {
                // 파싱 실패하면 원본 값 유지
              }
            }

            if (Array.isArray(parsedValue) && parsedValue.length > 0) {
              const firstItem = parsedValue[0];
              const deletedItemIds = firstItem?._deletedItemIds;
              const targetTable = firstItem?._targetTable;

              if (deletedItemIds && deletedItemIds.length > 0 && targetTable) {
                // 삭제 API 호출 - screenId 전달하여 제어관리 실행 가능하도록 함
                for (const itemId of deletedItemIds) {
                  try {
                    const deleteResult = await DynamicFormApi.deleteFormDataFromTable(
                      itemId,
                      targetTable,
                      context.screenId,
                    );
                    if (deleteResult.success) {
                    } else {
                      console.warn(`⚠️ [handleSave] 항목 삭제 실패: ${itemId}`, deleteResult.message);
                    }
                  } catch (deleteError) {
                    console.error(`❌ [handleSave] 항목 삭제 오류: ${itemId}`, deleteError);
                  }
                }
              }
            }
          }

          // 🆕 RepeaterFieldGroup 데이터 저장 처리 (_targetTable이 있는 배열 데이터)
          // formData에서 _targetTable 메타데이터가 포함된 배열 필드 찾기

          // 🆕 마스터-디테일 저장: 테이블 간 조인 관계 캐시
          const joinRelationshipCache: Record<string, { mainColumn: string; detailColumn: string } | null> = {};

          for (const [fieldKey, fieldValue] of Object.entries(context.formData)) {
            // JSON 문자열인 경우 파싱
            let parsedData = fieldValue;
            if (typeof fieldValue === "string" && fieldValue.startsWith("[")) {
              try {
                parsedData = JSON.parse(fieldValue);
              } catch {
                continue;
              }
            }

            // 배열이고 첫 번째 항목에 _targetTable이 있는 경우만 처리
            if (!Array.isArray(parsedData) || parsedData.length === 0) continue;

            const firstItem = parsedData[0];
            const repeaterTargetTable = firstItem?._targetTable;

            // _targetTable이 없거나, _repeatScreenModal_ 키면 스킵 (다른 로직에서 처리)
            if (!repeaterTargetTable || fieldKey.startsWith("_repeatScreenModal_")) continue;

            // _deferSave 플래그가 있으면 메인 저장 후 처리 (마스터-디테일 순차 저장)
            if (firstItem?._deferSave) continue;

            // 🆕 V2Repeater가 등록된 테이블이면 RepeaterFieldGroup 저장 스킵
            // V2Repeater가 repeaterSave 이벤트로 저장 처리함
            // @ts-ignore - window에 동적 속성 사용
            const registeredV2RepeaterTables = Array.from(window.__v2RepeaterInstances || []);
            if (registeredV2RepeaterTables.includes(repeaterTargetTable)) {
              continue;
            }

            // 🆕 마스터-디테일 조인 관계 조회 (메인 테이블 → 리피터 테이블)
            let joinRelationship: { mainColumn: string; detailColumn: string } | null = null;
            const cacheKey = `${tableName}:${repeaterTargetTable}`;

            if (tableName && repeaterTargetTable && tableName !== repeaterTargetTable) {
              // 캐시에서 먼저 확인
              if (cacheKey in joinRelationshipCache) {
                joinRelationship = joinRelationshipCache[cacheKey];
              } else {
                try {
                  const joinResponse = await apiClient.get(
                    `/button-dataflow/join-relationship/${tableName}/${repeaterTargetTable}`,
                  );
                  if (joinResponse.data?.success && joinResponse.data?.data?.found) {
                    joinRelationship = {
                      mainColumn: joinResponse.data.data.mainColumn,
                      detailColumn: joinResponse.data.data.detailColumn,
                    };
                  }
                } catch (joinError) {
                  console.warn("⚠️ [handleSave] 조인 관계 조회 실패:", joinError);
                }
                // 결과를 캐시에 저장 (없어도 null로 저장하여 재조회 방지)
                joinRelationshipCache[cacheKey] = joinRelationship;
              }
            }

            // 🆕 범용 폼 모달의 공통 필드 추출 (order_no, manager_id 등)
            // "범용_폼_모달" 키에서 공통 필드를 가져옴
            const universalFormData = context.formData["범용_폼_모달"] as Record<string, unknown> | undefined;
            const commonFields: Record<string, unknown> = {};
            if (universalFormData && typeof universalFormData === "object") {
              // 공통 필드 복사 (내부 메타 필드 제외)
              for (const [key, value] of Object.entries(universalFormData)) {
                if (!key.startsWith("_") && !key.endsWith("_numberingRuleId") && value !== undefined && value !== "") {
                  commonFields[key] = value;
                }
              }
            }

            // 🆕 루트 레벨 formData에서 RepeaterFieldGroup에 전달할 공통 필드 추출
            // 규칙 기반 필터링: 하드코딩 대신 패턴으로 제외할 필드를 정의
            for (const [fieldName, value] of Object.entries(context.formData)) {
              // 제외 규칙 1: comp_로 시작하는 필드 (하위 항목 배열)
              if (fieldName.startsWith("comp_")) continue;
              // 제외 규칙 2: _numberingRuleId로 끝나는 필드 (채번 규칙 메타 정보)
              if (fieldName.endsWith("_numberingRuleId")) continue;
              // 제외 규칙 3: _로 시작하는 필드 (내부 메타 필드)
              if (fieldName.startsWith("_")) continue;
              // 제외 규칙 4: 배열 타입 (하위 항목 데이터)
              if (Array.isArray(value)) continue;
              // 제외 규칙 5: 객체 타입 (복잡한 구조 데이터) - null 제외
              if (value !== null && typeof value === "object") continue;
              // 제외 규칙 6: 빈 값
              if (value === undefined || value === "" || value === null) continue;
              // 제외 규칙 7: 이미 commonFields에 있는 필드 (범용 폼 모달에서 가져온 필드)
              if (fieldName in commonFields) continue;

              // 위 규칙에 해당하지 않는 단순 값(문자열, 숫자, 날짜 등)은 공통 필드로 전달
              commonFields[fieldName] = value;
            }

            // 🆕 마스터-디테일 조인: 메인 테이블의 조인 컬럼 값을 commonFields에 추가
            if (joinRelationship) {
              const mainColumnValue = context.formData[joinRelationship.mainColumn];
              if (mainColumnValue !== undefined && mainColumnValue !== null && mainColumnValue !== "") {
                // 리피터 테이블의 조인 컬럼에 메인 테이블의 값 주입
                commonFields[joinRelationship.detailColumn] = mainColumnValue;
              } else {
                console.warn(`⚠️ [handleSave] 조인 컬럼 값이 없음: ${joinRelationship.mainColumn}`);
              }
            }

            for (const item of parsedData) {
              // 메타 필드 제거 (eslint 경고 무시 - 의도적으로 분리)

              const {
                _targetTable: _,
                _isNewItem,
                _existingRecord: __,
                _originalItemIds: ___,
                _deletedItemIds: ____,
                _fkColumn: itemFkColumn,
                ...dataToSave
              } = item;

              // 🆕 빈 id 필드 제거 (새 항목인 경우)
              if (!dataToSave.id || dataToSave.id === "" || dataToSave.id === null) {
                delete dataToSave.id;
              }

              // BOM 에디터 등 마스터-디테일: FK 값이 없으면 메인 저장 결과의 ID 주입
              if (itemFkColumn && (!dataToSave[itemFkColumn] || dataToSave[itemFkColumn] === null)) {
                const mainSavedId = saveResult?.data?.id || saveResult?.data?.data?.id || context.formData?.id;
                if (mainSavedId) {
                  dataToSave[itemFkColumn] = mainSavedId;
                }
              }

              // 🆕 공통 필드 병합 + 사용자 정보 추가
              const dataWithMeta: Record<string, unknown> = {
                ...dataToSave,
                ...commonFields,
                created_by: context.userId,
                updated_by: context.userId,
                company_code: context.companyCode,
              };

              try {
                // 🆕 새 항목 판단: _isNewItem 플래그 또는 id가 없거나 빈 문자열인 경우
                const isNewRecord = _isNewItem || !item.id || item.id === "" || item.id === undefined;

                if (isNewRecord) {
                  // INSERT (새 항목)
                  // id 필드 완전히 제거 (자동 생성되도록)
                  delete dataWithMeta.id;
                  // 빈 문자열 id도 제거
                  if ("id" in dataWithMeta && (dataWithMeta.id === "" || dataWithMeta.id === null)) {
                    delete dataWithMeta.id;
                  }

                  const insertResult = await apiClient.post(
                    `/table-management/tables/${repeaterTargetTable}/add`,
                    dataWithMeta,
                  );

                  // 무시된 컬럼이 있으면 경고 출력
                  if (insertResult.data?.data?.skippedColumns?.length > 0) {
                    console.warn(
                      `⚠️ [${repeaterTargetTable}] 테이블에 존재하지 않는 컬럼이 무시됨:`,
                      insertResult.data.data.skippedColumns,
                    );
                  }
                } else if (item.id) {
                  // UPDATE (기존 항목)
                  const originalData = { id: item.id };
                  const updatedData = { ...dataWithMeta, id: item.id };

                  const updateResult = await apiClient.put(`/table-management/tables/${repeaterTargetTable}/edit`, {
                    originalData,
                    updatedData,
                  });
                }
              } catch (err) {
                const error = err as { response?: { data?: unknown; status?: number }; message?: string };
                console.error(`❌ [handleSave] RepeaterFieldGroup 저장 실패 (${repeaterTargetTable}):`, {
                  status: error.response?.status,
                  data: error.response?.data,
                  message: error.message,
                  fullError: JSON.stringify(error.response?.data, null, 2),
                });
              }
            }
          }

          // 🆕 v3.9: RepeatScreenModal의 외부 테이블 데이터 저장 처리
          const repeatScreenModalKeys = Object.keys(context.formData).filter(
            (key) => key.startsWith("_repeatScreenModal_") && key !== "_repeatScreenModal_aggregations",
          );

          // RepeatScreenModal 데이터가 있으면 해당 테이블에 대한 메인 저장은 건너뜀
          const repeatScreenModalTables = repeatScreenModalKeys.map((key) => key.replace("_repeatScreenModal_", ""));

          // 🆕 RepeaterFieldGroup 테이블 목록 수집 (메인 저장 건너뛰기 판단용)
          const repeaterFieldGroupTables: string[] = [];
          for (const [, fieldValue] of Object.entries(context.formData)) {
            let parsedData = fieldValue;
            if (typeof fieldValue === "string" && fieldValue.startsWith("[")) {
              try {
                parsedData = JSON.parse(fieldValue);
              } catch {
                continue;
              }
            }
            if (Array.isArray(parsedData) && parsedData.length > 0 && parsedData[0]?._targetTable) {
              repeaterFieldGroupTables.push(parsedData[0]._targetTable);
            }
          }

          // 🆕 V2Repeater 전역 등록 확인
          // @ts-ignore - window에 동적 속성 사용
          const v2RepeaterTables = Array.from(window.__v2RepeaterInstances || []);

          // V2Repeater가 동일 테이블에 존재하는지 allComponents로 감지
          // (useCustomTable 미설정 = 화면 테이블에 직접 저장하는 리피터)
          const hasRepeaterOnSameTable =
            context.allComponents?.some((c: any) => {
              const compType = c.componentType || c.overrides?.type;
              if (compType !== "v2-repeater") return false;
              const compConfig = c.componentConfig || c.overrides || {};
              return !compConfig.useCustomTable;
            }) || false;

          // 메인 저장 건너뛰기 조건:
          // 1. RepeatScreenModal 또는 RepeaterFieldGroup에서 같은 테이블 처리
          // 2. V2Repeater가 같은 테이블에 존재 (리피터 데이터에 메인 폼 데이터 병합되어 저장됨)
          // 3. allComponents에서 useCustomTable 미설정 V2Repeater 감지 (글로벌 등록 없는 경우)
          const shouldSkipMainSave =
            repeatScreenModalTables.includes(tableName) ||
            repeaterFieldGroupTables.includes(tableName) ||
            v2RepeaterTables.includes(tableName) ||
            hasRepeaterOnSameTable;

          if (shouldSkipMainSave) {
            saveResult = { success: true, message: "RepeaterFieldGroup/RepeatScreenModal/V2Repeater에서 처리" };
          } else {
            saveResult = await DynamicFormApi.saveFormData({
              screenId,
              tableName,
              data: dataWithUserInfo,
            });
          }

          if (repeatScreenModalKeys.length > 0) {
            // 🆕 formData에서 채번 규칙으로 생성된 값들 추출 (예: shipment_plan_no)
            const numberingFields: Record<string, any> = {};
            for (const [fieldKey, value] of Object.entries(context.formData)) {
              // _numberingRuleId로 끝나는 키가 있으면 해당 필드는 채번 규칙 값
              if (context.formData[`${fieldKey}_numberingRuleId`]) {
                numberingFields[fieldKey] = value;
              }
            }

            for (const key of repeatScreenModalKeys) {
              const targetTable = key.replace("_repeatScreenModal_", "");
              const rows = context.formData[key] as any[];

              if (!Array.isArray(rows) || rows.length === 0) continue;

              // 🆕 마스터-디테일 조인 관계 조회 (메인 테이블 → RepeatScreenModal 테이블)
              let joinRelationship: { mainColumn: string; detailColumn: string } | null = null;
              if (tableName && targetTable && tableName !== targetTable) {
                const cacheKey = `${tableName}:${targetTable}`;
                if (cacheKey in joinRelationshipCache) {
                  joinRelationship = joinRelationshipCache[cacheKey];
                } else {
                  try {
                    const joinResponse = await apiClient.get(
                      `/button-dataflow/join-relationship/${tableName}/${targetTable}`,
                    );
                    if (joinResponse.data?.success && joinResponse.data?.data?.found) {
                      joinRelationship = {
                        mainColumn: joinResponse.data.data.mainColumn,
                        detailColumn: joinResponse.data.data.detailColumn,
                      };
                    }
                  } catch (joinError) {
                    console.warn("⚠️ [handleSave] RepeatScreenModal 조인 관계 조회 실패:", joinError);
                  }
                  joinRelationshipCache[cacheKey] = joinRelationship;
                }
              }

              // 🆕 조인 컬럼 값 준비 (메인 테이블에서 가져옴)
              const joinColumnData: Record<string, any> = {};
              if (joinRelationship) {
                const mainColumnValue = context.formData[joinRelationship.mainColumn];
                if (mainColumnValue !== undefined && mainColumnValue !== null && mainColumnValue !== "") {
                  joinColumnData[joinRelationship.detailColumn] = mainColumnValue;
                }
              }

              for (const row of rows) {
                const { _isNew, _targetTable, id, ...dataToSave } = row;

                // 사용자 정보 추가 + 채번 규칙 값 병합 + 조인 컬럼 값 추가
                const dataWithMeta = {
                  ...dataToSave,
                  ...numberingFields, // 채번 규칙 값 (shipment_plan_no 등)
                  ...joinColumnData, // 🆕 조인 컬럼 값 (마스터-디테일 관계)
                  created_by: context.userId,
                  updated_by: context.userId,
                  company_code: context.companyCode,
                };

                try {
                  if (_isNew) {
                    // INSERT

                    const insertResult = await apiClient.post(
                      `/table-management/tables/${targetTable}/add`,
                      dataWithMeta,
                    );
                  } else if (id) {
                    // UPDATE
                    const originalData = { id };
                    const updatedData = { ...dataWithMeta, id };

                    const updateResult = await apiClient.put(`/table-management/tables/${targetTable}/edit`, {
                      originalData,
                      updatedData,
                    });
                  }
                } catch (error: any) {
                  console.error(`❌ [handleSave] ${targetTable} 저장 실패:`, error.response?.data || error.message);
                  // 개별 실패는 전체 저장을 중단하지 않음
                }
              }
            }
          }

          // 🆕 v2-repeat-container 데이터 저장 처리 (_repeatContainerTables에 그룹화된 데이터)
          const repeatContainerTables = context.formData._repeatContainerTables as Record<string, any[]> | undefined;
          if (repeatContainerTables && Object.keys(repeatContainerTables).length > 0) {
            for (const [targetTable, rows] of Object.entries(repeatContainerTables)) {
              if (!Array.isArray(rows) || rows.length === 0) continue;

              // 🆕 마스터-디테일 조인 관계 조회
              let joinRelationship: { mainColumn: string; detailColumn: string } | null = null;
              if (tableName && targetTable && tableName !== targetTable) {
                const cacheKey = `${tableName}:${targetTable}`;
                if (cacheKey in joinRelationshipCache) {
                  joinRelationship = joinRelationshipCache[cacheKey];
                } else {
                  try {
                    const joinResponse = await apiClient.get(
                      `/button-dataflow/join-relationship/${tableName}/${targetTable}`,
                    );
                    if (joinResponse.data?.success && joinResponse.data?.data?.found) {
                      joinRelationship = {
                        mainColumn: joinResponse.data.data.mainColumn,
                        detailColumn: joinResponse.data.data.detailColumn,
                      };
                    }
                  } catch (joinError) {
                    console.warn("⚠️ [handleSave] RepeatContainer 조인 관계 조회 실패:", joinError);
                  }
                  joinRelationshipCache[cacheKey] = joinRelationship;
                }
              }

              // 조인 컬럼 값 준비
              const joinColumnData: Record<string, any> = {};
              if (joinRelationship) {
                const mainColumnValue = context.formData[joinRelationship.mainColumn];
                if (mainColumnValue !== undefined && mainColumnValue !== null && mainColumnValue !== "") {
                  joinColumnData[joinRelationship.detailColumn] = mainColumnValue;
                }
              }

              for (const row of rows) {
                const { _isDirty, _sectionIndex, _targetTable, id, ...dataToSave } = row;

                // 변경되지 않은 행은 건너뛰기
                if (_isDirty === false) {
                  continue;
                }

                // 사용자 정보 추가 + 조인 컬럼 값 추가
                const dataWithMeta = {
                  ...dataToSave,
                  ...joinColumnData, // 조인 컬럼 값
                  created_by: context.userId,
                  updated_by: context.userId,
                  company_code: context.companyCode,
                };

                try {
                  if (!id) {
                    // INSERT (id가 없으면 새 레코드)

                    const insertResult = await apiClient.post(
                      `/table-management/tables/${targetTable}/add`,
                      dataWithMeta,
                    );
                  } else {
                    // UPDATE (id가 있으면 기존 레코드)
                    const originalData = { id };
                    const updatedData = { ...dataWithMeta, id };

                    const updateResult = await apiClient.put(`/table-management/tables/${targetTable}/edit`, {
                      originalData,
                      updatedData,
                    });
                  }
                } catch (error: any) {
                  console.error(
                    `❌ [handleSave] ${targetTable} 저장 실패 (RepeatContainer):`,
                    error.response?.data || error.message,
                  );
                }
              }
            }
          }

          // 🆕 v3.9: RepeatScreenModal 집계 저장 처리
          const aggregationConfigs = context.formData._repeatScreenModal_aggregations as Array<{
            resultField: string;
            aggregatedValue: number;
            targetTable: string;
            targetColumn: string;
            joinKey: { sourceField: string; targetField: string };
            sourceValue: any;
          }>;

          if (aggregationConfigs && aggregationConfigs.length > 0) {
            for (const config of aggregationConfigs) {
              const { targetTable, targetColumn, joinKey, aggregatedValue, sourceValue } = config;

              try {
                const originalData = { [joinKey.targetField]: sourceValue };
                const updatedData = {
                  [targetColumn]: aggregatedValue,
                  [joinKey.targetField]: sourceValue,
                };

                const updateResult = await apiClient.put(`/table-management/tables/${targetTable}/edit`, {
                  originalData,
                  updatedData,
                });
              } catch (error: any) {
                console.error(`❌ [handleSave] ${targetTable} 집계 저장 실패:`, error.response?.data || error.message);
              }
            }
          }
        }

        if (!saveResult.success) {
          const errorMsg =
            saveResult.message ||
            saveResult.error?.message ||
            "저장에 실패했습니다.";
          toast.error(errorMsg);
          return false;
        }

        // 제어 실행 준비 (실제 실행은 리피터 저장 완료 후로 지연)
        console.log("🔍 [handleSave] 제어관리 설정 체크:", {
          enableDataflowControl: config.enableDataflowControl,
          hasDataflowConfig: !!config.dataflowConfig,
          flowControls: config.dataflowConfig?.flowControls?.length || 0,
        });
        if (config.enableDataflowControl && config.dataflowConfig) {
          const savedRecord = saveResult?.data?.data || saveResult?.data || {};
          const actualFormData = savedRecord?.data || savedRecord;
          const formData: Record<string, any> = (
            Object.keys(actualFormData).length > 0 ? actualFormData : context.formData || {}
          ) as Record<string, any>;
          let parsedSectionData: any[] = [];

          const compFieldKey = Object.keys(formData).find(
            (key) => key.startsWith("comp_") && typeof formData[key] === "string",
          );

          if (compFieldKey) {
            try {
              const sectionData = JSON.parse(formData[compFieldKey]);
              if (Array.isArray(sectionData) && sectionData.length > 0) {
                parsedSectionData = sectionData.map((item: any) => {
                  const { _isNewItem, _targetTable, _existingRecord, ...cleanItem } = item;
                  const commonFields: Record<string, any> = {};
                  Object.keys(formData).forEach((key) => {
                    if (!key.startsWith("comp_") && !key.endsWith("_numberingRuleId")) {
                      commonFields[key] = formData[key];
                    }
                  });
                  return { ...commonFields, ...cleanItem };
                });
              }
            } catch (parseError) {
              console.warn("⚠️ [handleSave] 테이블 섹션 데이터 파싱 실패:", parseError);
            }
          }

          pendingDataflowControl = {
            config,
            context: {
              ...context,
              savedData: formData,
              selectedRowsData: parsedSectionData.length > 0 ? parsedSectionData : context.selectedRowsData,
            },
          };
        }
      } else {
        throw new Error("저장에 필요한 정보가 부족합니다. (테이블명 또는 화면ID 누락)");
      }

      // V2Repeater 저장 이벤트 발생 (모달 닫기 전에 실행해야 V2Repeater가 이벤트를 수신할 수 있음)
      const savedId = saveResult?.data?.id || saveResult?.data?.data?.id || formData.id || context.formData?.id;

      // _deferSave 데이터 처리 (마스터-디테일 순차 저장: 레벨별 저장 + temp→real ID 매핑)
      if (savedId) {
        for (const [fieldKey, fieldValue] of Object.entries(context.formData)) {
          let parsedData = fieldValue;
          if (typeof fieldValue === "string" && fieldValue.startsWith("[")) {
            try {
              parsedData = JSON.parse(fieldValue);
            } catch {
              continue;
            }
          }
          if (!Array.isArray(parsedData) || parsedData.length === 0) continue;
          if (!parsedData[0]?._deferSave) continue;

          const targetTable = parsedData[0]?._targetTable;
          if (!targetTable) continue;

          // 레벨별 그룹핑 (레벨 0 먼저 저장 → 레벨 1 → ...)
          const maxLevel = Math.max(...parsedData.map((item: any) => Number(item.level) || 0));
          const tempIdToRealId = new Map<string, string>();

          for (let lvl = 0; lvl <= maxLevel; lvl++) {
            const levelItems = parsedData.filter((item: any) => (Number(item.level) || 0) === lvl);

            for (const item of levelItems) {
              const { _targetTable: _, _isNew, _deferSave: __, _fkColumn: fkCol, tempId, ...data } = item;
              if (!data.id || data.id === "") delete data.id;

              // FK 주입 (bom_id 등)
              if (fkCol) data[fkCol] = savedId;

              // parent_detail_id의 temp 참조를 실제 ID로 교체
              if (data.parent_detail_id && tempIdToRealId.has(data.parent_detail_id)) {
                data.parent_detail_id = tempIdToRealId.get(data.parent_detail_id);
              }

              // 시스템 필드 추가
              data.created_by = context.userId;
              data.updated_by = context.userId;
              data.company_code = context.companyCode;

              try {
                const isNew = _isNew || !item.id || item.id === "";
                if (isNew) {
                  const res = await apiClient.post(`/table-management/tables/${targetTable}/add`, data);
                  const newId = res.data?.data?.id || res.data?.id;
                  if (newId && tempId) {
                    tempIdToRealId.set(tempId, newId);
                  }
                } else {
                  await apiClient.put(`/table-management/tables/${targetTable}/${item.id}`, data);
                  if (item.id && tempId) {
                    tempIdToRealId.set(tempId, item.id);
                  }
                }
              } catch (err: any) {
                console.error(`[handleSave] 디테일 저장 실패 (${targetTable}):`, err.response?.data || err.message);
              }
            }
          }
        }
      }

      // 메인 폼 데이터 구성 (사용자 정보 포함)
      const mainFormData = {
        ...formData,
        writer: formData.writer || context.userId,
        created_by: context.userId,
        updated_by: context.userId,
        company_code: formData.company_code || context.companyCode,
      };

      // _numberingRuleId 등 메타 필드 제거
      for (const key of Object.keys(mainFormData)) {
        if (key.endsWith("_numberingRuleId") || key.startsWith("_")) {
          delete mainFormData[key];
        }
      }

      console.log("🟢 [buttonActions] repeaterSave 이벤트 발행:", {
        parentId: savedId,
        tableName: context.tableName,
        masterRecordId: savedId,
        mainFormDataKeys: Object.keys(mainFormData),
      });

      // V2Repeater가 등록된 경우에만 저장 완료를 기다림
      // @ts-ignore
      const hasActiveRepeaters = window.__v2RepeaterInstances && window.__v2RepeaterInstances.size > 0;

      if (hasActiveRepeaters) {
        const repeaterSavePromise = new Promise<void>((resolve) => {
          const fallbackTimeout = setTimeout(resolve, 5000);
          const handler = () => {
            clearTimeout(fallbackTimeout);
            window.removeEventListener("repeaterSaveComplete", handler);
            resolve();
          };
          window.addEventListener("repeaterSaveComplete", handler);
        });

        window.dispatchEvent(
          new CustomEvent("repeaterSave", {
            detail: {
              parentId: savedId,
              tableName: context.tableName,
              mainFormData,
              masterRecordId: savedId,
            },
          }),
        );

        await repeaterSavePromise;
      }

      // 리피터 저장 완료 후 제어관리 실행 (디테일 레코드가 DB에 있는 상태에서 실행)
      console.log("🔍 [handleSave] 리피터 저장 완료, pendingDataflowControl:", !!pendingDataflowControl);
      if (pendingDataflowControl) {
        console.log("📦 [handleSave] 리피터 저장 완료 후 제어관리 실행 시작");
        await this.executeAfterSaveControl(
          pendingDataflowControl.config,
          pendingDataflowControl.context,
        );
      }

      // 테이블과 플로우 새로고침 (모달 닫기 전에 실행)
      context.onRefresh?.();
      context.onFlowRefresh?.();

      // 저장 성공 후 모달 닫기 이벤트 발생 (모달 내부에서만)
      // 비모달 화면에서 이 이벤트를 발행하면 ScreenModal이 반응하여 컴포넌트 트리 재마운트 발생
      if (context.onClose) {
        window.dispatchEvent(new CustomEvent("closeEditModal"));
        window.dispatchEvent(new CustomEvent("saveSuccessInModal"));
      }

      return true;
    } catch (error: any) {
      console.error("저장 오류:", error);
      const msg =
        error?.response?.data?.message ||
        error?.response?.data?.error?.message ||
        error?.message ||
        "저장에 실패했습니다.";
      toast.error(msg);
      return false;
    }
  }

  /**
   * V2Repeater 디테일 데이터 저장 이벤트 발행 (onSave 콜백 경로에서도 사용)
   */
  private static async dispatchRepeaterSave(context: ButtonActionContext): Promise<void> {
    const formData = context.formData || {};
    const savedId = formData.id;

    if (!savedId) {
      console.log("⚠️ [dispatchRepeaterSave] savedId(formData.id) 없음 - 스킵");
      return;
    }

    console.log("🟢 [dispatchRepeaterSave] repeaterSave 이벤트 발행:", {
      parentId: savedId,
      tableName: context.tableName,
      masterRecordId: savedId,
      formDataKeys: Object.keys(formData),
    });

    // @ts-ignore
    const hasActiveRepeaters = window.__v2RepeaterInstances && window.__v2RepeaterInstances.size > 0;

    if (hasActiveRepeaters) {
      const repeaterSavePromise = new Promise<void>((resolve) => {
        const fallbackTimeout = setTimeout(resolve, 5000);
        const handler = () => {
          clearTimeout(fallbackTimeout);
          window.removeEventListener("repeaterSaveComplete", handler);
          resolve();
        };
        window.addEventListener("repeaterSaveComplete", handler);
      });

      window.dispatchEvent(
        new CustomEvent("repeaterSave", {
          detail: {
            parentId: savedId,
            tableName: context.tableName,
            mainFormData: formData,
            masterRecordId: savedId,
          },
        }),
      );

      await repeaterSavePromise;
    }
  }

  /**
   * DB에서 조회한 실제 기본키로 formData에서 값 추출
   * @param formData 폼 데이터
   * @param primaryKeys DB에서 조회한 실제 기본키 컬럼명 배열
   * @returns 기본키 값 (복합키의 경우 첫 번째 키 값)
   */
  private static extractPrimaryKeyValueFromDB(formData: Record<string, any>, primaryKeys: string[]): any {
    if (!primaryKeys || primaryKeys.length === 0) {
      return null;
    }

    // 첫 번째 기본키 컬럼의 값을 사용 (복합키의 경우)
    const primaryKeyColumn = primaryKeys[0];

    if (formData.hasOwnProperty(primaryKeyColumn)) {
      const value = formData[primaryKeyColumn];

      // 복합키인 경우 로그 출력
      if (primaryKeys.length > 1) {
        console.log(`📍 첫 번째 키 (${primaryKeyColumn}) 값을 사용: ${value}`);
      }

      return value;
    }

    // 기본키 컬럼이 formData에 없는 경우
    return null;
  }

  /**
   * @deprecated DB 기반 조회로 대체됨. extractPrimaryKeyValueFromDB 사용 권장
   * formData에서 기본 키값 추출 (추측 기반)
   */
  private static extractPrimaryKeyValue(formData: Record<string, any>): any {
    // 일반적인 기본 키 필드명들 (우선순위 순)
    const commonPrimaryKeys = [
      "id",
      "ID", // 가장 일반적
      "objid",
      "OBJID", // 이 프로젝트에서 자주 사용
      "pk",
      "PK", // Primary Key 줄임말
      "_id", // MongoDB 스타일
      "uuid",
      "UUID", // UUID 방식
      "key",
      "KEY", // 기타
    ];

    // 우선순위에 따라 기본 키값 찾기
    for (const keyName of commonPrimaryKeys) {
      if (formData.hasOwnProperty(keyName)) {
        const value = formData[keyName];

        return value;
      }
    }

    // 기본 키를 찾지 못한 경우
    return null;
  }

  /**
   * 제출 액션 처리
   */
  private static async handleSubmit(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    // 제출은 저장과 유사하지만 추가적인 처리가 있을 수 있음
    return await this.handleSave(config, context);
  }

  /**
   * 🆕 렉 구조 컴포넌트 일괄 저장 처리
   * 미리보기에서 생성된 위치 데이터를 일괄 INSERT
   */
  private static async handleRackStructureBatchSave(
    config: ButtonActionConfig,
    context: ButtonActionContext,
    locations: any[],
    rackStructureFieldKey: string = "_rackStructureLocations",
  ): Promise<boolean> {
    const { tableName, screenId, userId, companyCode } = context;

    if (!tableName) {
      throw new Error("테이블명이 지정되지 않았습니다.");
    }

    if (locations.length === 0) {
      throw new Error("저장할 위치 데이터가 없습니다. 먼저 미리보기를 생성해주세요.");
    }

    // 저장 전 중복 체크
    const firstLocation = locations[0];
    const warehouseCode = firstLocation.warehouse_code || firstLocation.warehouse_id || firstLocation.warehouseCode;
    const floor = firstLocation.floor;
    const zone = firstLocation.zone;

    if (warehouseCode && zone) {
      try {
        const searchParams: Record<string, any> = {
          warehouse_code: { value: warehouseCode, operator: "equals" },
          zone: { value: zone, operator: "equals" },
        };
        if (floor) {
          searchParams.floor = { value: floor, operator: "equals" };
        }

        const existingResponse = await DynamicFormApi.getTableData(tableName, {
          search: searchParams,
          page: 1,
          pageSize: 1000,
        });

        // API 응답 구조에 따라 데이터 추출
        const responseData = existingResponse.data as any;
        const existingData = responseData?.data || responseData || [];

        if (Array.isArray(existingData) && existingData.length > 0) {
          // 중복되는 위치 확인
          const existingSet = new Set(existingData.map((loc: any) => `${loc.row_num}-${loc.level_num}`));

          const duplicates = locations.filter((loc) => {
            const key = `${loc.row_num || loc.rowNum}-${loc.level_num || loc.levelNum}`;
            return existingSet.has(key);
          });

          if (duplicates.length > 0) {
            const duplicateInfo = duplicates
              .slice(0, 5)
              .map((d) => `${d.row_num || d.rowNum}열 ${d.level_num || d.levelNum}단`)
              .join(", ");

            const moreCount = duplicates.length > 5 ? ` 외 ${duplicates.length - 5}개` : "";

            alert(
              `이미 등록된 위치가 있습니다!\n\n중복 위치: ${duplicateInfo}${moreCount}\n\n해당 위치를 제외하거나 기존 데이터를 삭제해주세요.`,
            );
            return false;
          }
        }
      } catch (checkError) {
        console.warn("⚠️ [handleRackStructureBatchSave] 중복 체크 실패 (저장 계속 진행):", checkError);
      }
    }

    // 각 위치 데이터를 그대로 저장 (렉 구조 컴포넌트에서 이미 테이블 컬럼명으로 생성됨)
    const recordsToInsert = locations.map((loc) => {
      // 렉 구조 컴포넌트에서 생성된 데이터를 그대로 사용
      // 새로운 형식(스네이크 케이스)과 기존 형식(카멜 케이스) 모두 지원
      const record: Record<string, any> = {
        // 렉 구조에서 생성된 필드 (이미 테이블 컬럼명과 동일)
        location_code: loc.location_code || loc.locationCode,
        location_name: loc.location_name || loc.locationName,
        row_num: loc.row_num || String(loc.rowNum),
        level_num: loc.level_num || String(loc.levelNum),
        // 창고 정보 (렉 구조 컴포넌트에서 전달) - DB 컬럼명은 warehouse_code
        warehouse_code: loc.warehouse_code || loc.warehouse_id || loc.warehouseCode,
        warehouse_name: loc.warehouse_name || loc.warehouseName,
        // 위치 정보 (렉 구조 컴포넌트에서 전달)
        floor: loc.floor,
        zone: loc.zone,
        location_type: loc.location_type || loc.locationType,
        status: loc.status || "사용",
        // 사용자 정보 추가
        writer: userId,
        company_code: companyCode,
      };

      return record;
    });

    // 일괄 INSERT 실행
    try {
      let successCount = 0;
      let errorCount = 0;
      const errors: string[] = [];

      for (let i = 0; i < recordsToInsert.length; i++) {
        const record = recordsToInsert[i];
        try {
          const result = await DynamicFormApi.saveFormData({
            screenId,
            tableName,
            data: record,
          });

          if (result.success) {
            successCount++;
          } else {
            errorCount++;
            const errorMsg = result.message || result.error || "알 수 없는 오류";
            errors.push(errorMsg);
            console.error(`❌ [handleRackStructureBatchSave] 저장 실패 (${i + 1}):`, errorMsg);
          }
        } catch (error: any) {
          errorCount++;
          const errorMsg = error.message || "저장 중 오류 발생";
          errors.push(errorMsg);
          console.error(`❌ [handleRackStructureBatchSave] 예외 발생 (${i + 1}):`, error);
        }
      }

      if (errorCount > 0) {
        if (successCount > 0) {
          alert(`${successCount}개 저장 완료, ${errorCount}개 저장 실패\n\n오류: ${errors.slice(0, 3).join("\n")}`);
        } else {
          throw new Error(`저장 실패: ${errors[0]}`);
        }
      } else {
        alert(`${successCount}개의 위치가 성공적으로 등록되었습니다.`);
      }

      // 성공 후 새로고침
      if (context.onRefresh) {
        context.onRefresh();
      }

      // 모달 닫기
      if (context.onClose) {
        context.onClose();
      }

      return successCount > 0;
    } catch (error: any) {
      console.error("🏗️ [handleRackStructureBatchSave] 일괄 저장 오류:", error);
      throw error;
    }
  }

  /**
   * 🆕 Universal Form Modal 테이블 섹션 병합 저장 처리
   * 범용_폼_모달 내부의 공통 필드 + _tableSection_ 데이터를 병합하여 품목별로 저장
   * 수정 모드: INSERT/UPDATE/DELETE 지원
   * 🆕 섹션별 저장 테이블(targetTable) 지원 추가
   */
  private static async handleUniversalFormModalTableSectionSave(
    config: ButtonActionConfig,
    context: ButtonActionContext,
    formData: Record<string, any>,
  ): Promise<{ handled: boolean; success: boolean }> {
    const { tableName, screenId } = context;

    // 범용_폼_모달 키 찾기 (컬럼명에 따라 다를 수 있음)
    // initializeForm에서 __tableSection_ (더블), 수정 시 _tableSection_ (싱글) 사용
    const universalFormModalKey = Object.keys(formData).find((key) => {
      const value = formData[key];
      if (!value || typeof value !== "object" || Array.isArray(value)) return false;
      return Object.keys(value).some((k) => k.startsWith("_tableSection_") || k.startsWith("__tableSection_"));
    });

    if (!universalFormModalKey) {
      return { handled: false, success: false };
    }

    const modalData = formData[universalFormModalKey];

    // 🆕 universal-form-modal 컴포넌트 설정 가져오기
    // 1. componentConfigs에서 컴포넌트 ID로 찾기
    // 2. allComponents에서 columnName으로 찾기
    // 3. 화면 레이아웃 API에서 가져오기
    let modalComponentConfig = context.componentConfigs?.[universalFormModalKey];

    // componentConfigs에서 직접 찾지 못한 경우, allComponents에서 columnName으로 찾기
    if (!modalComponentConfig && context.allComponents) {
      const modalComponent = context.allComponents.find(
        (comp: any) =>
          comp.columnName === universalFormModalKey || comp.properties?.columnName === universalFormModalKey,
      );
      if (modalComponent) {
        modalComponentConfig = modalComponent.componentConfig || modalComponent.properties?.componentConfig;
      }
    }

    // 🆕 아직도 설정을 찾지 못했으면 화면 레이아웃 API에서 가져오기
    if (!modalComponentConfig && screenId) {
      try {
        const { screenApi } = await import("@/lib/api/screen");
        const layoutData = await screenApi.getLayout(screenId);

        if (layoutData && layoutData.components) {
          // 레이아웃에서 universal-form-modal 컴포넌트 찾기
          const modalLayout = (layoutData.components as any[]).find(
            (comp) =>
              comp.properties?.columnName === universalFormModalKey || comp.columnName === universalFormModalKey,
          );
          if (modalLayout) {
            modalComponentConfig = modalLayout.properties?.componentConfig || modalLayout.componentConfig;
          }
        }
      } catch (error) {
        console.warn("⚠️ [handleUniversalFormModalTableSectionSave] 화면 레이아웃 조회 실패:", error);
      }
    }

    const sections: any[] = modalComponentConfig?.sections || [];
    const saveConfig = modalComponentConfig?.saveConfig || {};

    // 테이블 섹션 데이터 수집: DB 전체 데이터를 베이스로, 수정 데이터를 오버라이드
    const tableSectionData: Record<string, any[]> = {};
    const commonFieldsData: Record<string, any> = {};

    // 원본 그룹 데이터 추출 (수정 모드에서 UPDATE/DELETE 추적용)
    const originalGroupedData: any[] = modalData._originalGroupedData || formData._originalGroupedData || [];

    // 1단계: DB 데이터(__tableSection_)와 수정 데이터(_tableSection_)를 별도로 수집
    const dbSectionData: Record<string, any[]> = {};
    const modifiedSectionData: Record<string, any[]> = {};

    // 1-1: modalData(부모의 중첩 객체)에서 수집
    for (const [key, value] of Object.entries(modalData)) {
      if (key.startsWith("__tableSection_") && Array.isArray(value)) {
        const sectionId = key.replace("__tableSection_", "");
        dbSectionData[sectionId] = value;
      } else if (key.startsWith("_tableSection_") && Array.isArray(value)) {
        const sectionId = key.replace("_tableSection_", "");
        modifiedSectionData[sectionId] = value;
      } else if (!key.startsWith("_")) {
        commonFieldsData[key] = value;
      }
    }

    // 1-2: top-level formData에서도 수집 (handleBeforeFormSave가 직접 설정한 최신 데이터)
    // modalData(중첩 객체)가 아직 업데이트되지 않았을 수 있으므로 보완
    for (const [key, value] of Object.entries(formData)) {
      if (key === universalFormModalKey) continue;
      if (key.startsWith("__tableSection_") && Array.isArray(value)) {
        const sectionId = key.replace("__tableSection_", "");
        if (!dbSectionData[sectionId]) {
          dbSectionData[sectionId] = value;
        }
      } else if (key.startsWith("_tableSection_") && Array.isArray(value)) {
        const sectionId = key.replace("_tableSection_", "");
        if (!modifiedSectionData[sectionId]) {
          modifiedSectionData[sectionId] = value;
        }
      }
    }

    // 2단계: DB 데이터를 베이스로, 수정 데이터를 아이템별로 병합하여 전체 데이터 구성
    // - DB 데이터(__tableSection_): initializeForm에서 로드한 전체 컬럼 데이터
    // - 수정 데이터(_tableSection_): _groupedData 또는 사용자 UI 수정을 통해 설정된 데이터 (일부 컬럼만 포함 가능)
    // - 병합: { ...dbItem, ...modItem } → DB 전체 컬럼 유지 + 수정된 필드만 오버라이드
    const allSectionIds = new Set([...Object.keys(dbSectionData), ...Object.keys(modifiedSectionData)]);

    for (const sectionId of allSectionIds) {
      const dbItems = dbSectionData[sectionId] || [];
      const modItems = modifiedSectionData[sectionId];

      if (modItems) {
        tableSectionData[sectionId] = modItems.map((modItem) => {
          if (modItem.id) {
            const dbItem = dbItems.find((db) => String(db.id) === String(modItem.id));
            if (dbItem) {
              return { ...dbItem, ...modItem };
            }
          }
          return modItem;
        });
      } else {
        tableSectionData[sectionId] = dbItems;
      }
    }

    // 테이블 섹션 데이터가 없고 원본 데이터도 없으면 처리하지 않음
    const hasTableSectionData = Object.values(tableSectionData).some((arr) => arr.length > 0);
    if (!hasTableSectionData && originalGroupedData.length === 0) {
      console.log("⚠️ [handleUniversalFormModalTableSectionSave] 테이블 섹션 데이터 없음 - 일반 저장으로 전환");
      return { handled: false, success: false };
    }

    // 🎯 채번 규칙 할당 처리 (저장 시점에 실제 순번 증가)
    // 🔧 수정 모드 체크: _mainRecordId, formData.id 또는 originalGroupedData가 있으면 UPDATE 모드
    const parentMainRecordId = modalData._mainRecordId || formData._mainRecordId;
    const isEditModeUniversal =
      (parentMainRecordId !== undefined && parentMainRecordId !== null && parentMainRecordId !== "") ||
      (formData.id !== undefined && formData.id !== null && formData.id !== "") ||
      originalGroupedData.length > 0;

    const fieldsWithNumbering: Record<string, string> = {};

    // commonFieldsData와 modalData에서 채번 규칙이 설정된 필드 찾기
    for (const [key, value] of Object.entries(modalData)) {
      if (key.endsWith("_numberingRuleId") && value) {
        const fieldName = key.replace("_numberingRuleId", "");
        fieldsWithNumbering[fieldName] = value as string;
      }
    }

    // formData에서도 확인 (모달 외부에 있을 수 있음)
    for (const [key, value] of Object.entries(formData)) {
      if (key.endsWith("_numberingRuleId") && value && !fieldsWithNumbering[key.replace("_numberingRuleId", "")]) {
        const fieldName = key.replace("_numberingRuleId", "");
        fieldsWithNumbering[fieldName] = value as string;
      }
    }

    // 🔧 수정 모드에서는 채번 코드 할당 건너뛰기 (기존 번호 유지)
    // 신규 등록 모드에서만 allocateCode 호출하여 새 번호 할당
    if (Object.keys(fieldsWithNumbering).length > 0 && !isEditModeUniversal) {
      const { allocateNumberingCode } = await import("@/lib/api/numberingRule");

      for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) {
        try {
          // 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
          const userInputCode = commonFieldsData[fieldName] as string;
          const allocateResult = await allocateNumberingCode(ruleId, userInputCode, formData);

          if (allocateResult.success && allocateResult.data?.generatedCode) {
            const newCode = allocateResult.data.generatedCode;

            commonFieldsData[fieldName] = newCode;
          } else {
            console.warn(
              `⚠️ [handleUniversalFormModalTableSectionSave] ${fieldName} 코드 할당 실패, 기존 값 유지:`,
              allocateResult.error,
            );
          }
        } catch (allocateError) {
          console.error(`❌ [handleUniversalFormModalTableSectionSave] ${fieldName} 코드 할당 오류:`, allocateError);
          // 오류 시 기존 값 유지
        }
      }
    } else if (isEditModeUniversal) {
    }

    try {
      // 사용자 정보 추가
      if (!context.userId) {
        throw new Error("사용자 정보를 불러올 수 없습니다. 다시 로그인해주세요.");
      }

      const userInfo = {
        writer: context.userId,
        created_by: context.userId,
        updated_by: context.userId,
        company_code: context.companyCode || "",
      };

      let insertedCount = 0;
      let updatedCount = 0;
      let deletedCount = 0;
      let mainRecordId: number | null = null;

      // 🆕 먼저 메인 테이블에 공통 데이터 저장 (별도 테이블이 있는 경우에만)
      const hasSeparateTargetTable = sections.some(
        (s) =>
          s.type === "table" &&
          s.tableConfig?.saveConfig?.targetTable &&
          s.tableConfig.saveConfig.targetTable !== tableName,
      );

      if (hasSeparateTargetTable && Object.keys(commonFieldsData).length > 0) {
        // _mainRecordId: 분할패널에서 전달한 실제 메인 레코드 ID (formData.id는 디테일 레코드 ID일 수 있음)
        const mainRecordIdFromParent =
          modalData._mainRecordId || formData._mainRecordId || commonFieldsData._mainRecordId;
        // 메인 테이블 ID 결정: _mainRecordId > formData.id 순서로 탐색
        const existingMainId = mainRecordIdFromParent || formData.id;
        const isMainUpdate = existingMainId !== undefined && existingMainId !== null && existingMainId !== "";

        const mainRowToSave = { ...commonFieldsData, ...userInfo };

        // 메타데이터 제거
        Object.keys(mainRowToSave).forEach((key) => {
          if (key.startsWith("_")) {
            delete mainRowToSave[key];
          }
        });

        let mainSaveResult: { success: boolean; data?: any; message?: string };

        if (isMainUpdate) {
          mainSaveResult = await DynamicFormApi.updateFormData(existingMainId, {
            tableName: tableName!,
            data: mainRowToSave,
          });
          mainRecordId = existingMainId;
        } else {
          console.log("➕ [handleUniversalFormModalTableSectionSave] 메인 테이블 INSERT 실행");
          mainSaveResult = await DynamicFormApi.saveFormData({
            screenId: screenId!,
            tableName: tableName!,
            data: mainRowToSave,
          });
          mainRecordId = mainSaveResult.data?.id || null;
        }

        if (!mainSaveResult.success) {
          throw new Error(mainSaveResult.message || "메인 데이터 저장 실패");
        }
      }

      // 각 테이블 섹션 처리
      for (const [sectionId, currentItems] of Object.entries(tableSectionData)) {
        const sectionConfig = sections.find((s) => s.id === sectionId);
        const targetTableName = sectionConfig?.tableConfig?.saveConfig?.targetTable;
        const saveTableName = targetTableName || tableName!;

        // 섹션별 DB 원본 데이터 조회 (전체 컬럼 보장)
        // _originalTableSectionData_: initializeForm에서 DB 로드 시 저장한 원본 데이터
        const sectionOriginalKey = `_originalTableSectionData_${sectionId}`;
        const sectionOriginalData: any[] = modalData[sectionOriginalKey] || formData[sectionOriginalKey] || [];

        // 1️⃣ 신규 품목 INSERT (id가 없는 항목)
        const newItems = currentItems.filter((item) => !item.id);
        const existingItemsForRef = currentItems.filter((item) => item.id);

        // 기존 DB 항목에서 공통 필드 추출 (수정 모달에서 commonFieldsData에 누락된 필드 보완)
        // 예: item_code, item_name 등이 commonFieldsData에 없으면 기존 항목에서 가져옴
        const sharedFieldsFromExisting: Record<string, any> = {};
        if (existingItemsForRef.length > 0 && newItems.length > 0) {
          const refItem = existingItemsForRef[0];
          for (const [key, val] of Object.entries(refItem)) {
            if (
              key !== "id" &&
              !key.startsWith("_") &&
              val !== undefined &&
              val !== null &&
              val !== "" &&
              commonFieldsData[key] === undefined
            ) {
              sharedFieldsFromExisting[key] = val;
            }
          }
          if (Object.keys(sharedFieldsFromExisting).length > 0) {
            console.log("📋 [INSERT] 기존 항목에서 공통 필드 보완:", Object.keys(sharedFieldsFromExisting));
          }
        }

        for (const item of newItems) {
          const rowToSave = { ...sharedFieldsFromExisting, ...commonFieldsData, ...item, ...userInfo };

          Object.keys(rowToSave).forEach((key) => {
            if (key.startsWith("_")) {
              delete rowToSave[key];
            }
          });

          if (targetTableName && mainRecordId && saveConfig.primaryKeyColumn) {
            rowToSave[saveConfig.primaryKeyColumn] = mainRecordId;
          }

          console.log("➕ [INSERT] 신규 품목:", { tableName: saveTableName, data: rowToSave });

          const saveResult = await DynamicFormApi.saveFormData({
            screenId: screenId!,
            tableName: saveTableName,
            data: rowToSave,
          });

          if (!saveResult.success) {
            throw new Error(saveResult.message || "신규 품목 저장 실패");
          }

          insertedCount++;
        }

        // 2️⃣ 기존 품목 UPDATE (id가 있는 항목)
        // 전체 데이터 기반 저장: DB 데이터(__tableSection_)를 베이스로 수정 데이터가 병합된 완전한 item 사용
        const existingItems = currentItems.filter((item) => item.id);
        for (const item of existingItems) {
          // DB 원본 데이터 우선 사용 (전체 컬럼 보장), 없으면 originalGroupedData에서 탐색
          const originalItem =
            sectionOriginalData.find((orig) => String(orig.id) === String(item.id)) ||
            originalGroupedData.find((orig) => String(orig.id) === String(item.id));

          // 마스터/디테일 분리 시: 디테일 데이터만 사용 (마스터 필드 병합 안 함)
          // 같은 테이블 시: 공통 필드도 병합 (공유 필드 업데이트 필요)
          const dataToSave = hasSeparateTargetTable ? item : { ...commonFieldsData, ...item };

          if (!originalItem) {
            // 원본 없음: 전체 데이터로 UPDATE 실행
            console.warn(`⚠️ [UPDATE] 원본 데이터 없음 - 전체 데이터로 UPDATE: id=${item.id}`);

            const rowToUpdate = { ...dataToSave, ...userInfo };
            Object.keys(rowToUpdate).forEach((key) => {
              if (key.startsWith("_")) {
                delete rowToUpdate[key];
              }
            });

            const updateResult = await DynamicFormApi.updateFormData(item.id, {
              tableName: saveTableName,
              data: rowToUpdate,
            });

            if (!updateResult.success) {
              throw new Error(updateResult.message || "품목 수정 실패");
            }

            updatedCount++;
            continue;
          }

          // 변경 사항 확인: 원본(DB) vs 현재(병합된 전체 데이터)
          const hasChanges = this.checkForChanges(originalItem, dataToSave);

          if (hasChanges) {
            const updateResult = await DynamicFormApi.updateFormDataPartial(
              item.id,
              originalItem,
              dataToSave,
              saveTableName,
            );

            if (!updateResult.success) {
              throw new Error(updateResult.message || "품목 수정 실패");
            }

            updatedCount++;
          }
        }

        // 3️⃣ 삭제된 품목 DELETE (원본에는 있지만 현재에는 없는 항목)
        // 섹션별 DB 원본 데이터 사용 (위에서 이미 조회), 없으면 전역 originalGroupedData 사용
        const originalDataForDelete = sectionOriginalData.length > 0 ? sectionOriginalData : originalGroupedData;

        // ⚠️ id 타입 통일: 문자열로 변환하여 비교 (숫자 vs 문자열 불일치 방지)
        const currentIds = new Set(currentItems.map((item) => String(item.id)).filter(Boolean));
        const deletedItems = originalDataForDelete.filter((orig) => orig.id && !currentIds.has(String(orig.id)));

        for (const deletedItem of deletedItems) {
          // screenId 전달하여 제어관리 실행 가능하도록 함
          const deleteResult = await DynamicFormApi.deleteFormDataFromTable(
            deletedItem.id,
            saveTableName,
            context.screenId,
          );

          if (!deleteResult.success) {
            throw new Error(deleteResult.message || "품목 삭제 실패");
          }

          deletedCount++;
        }
      }

      // 결과 메시지 생성
      const resultParts: string[] = [];
      if (mainRecordId) resultParts.push("메인 데이터 저장");
      if (insertedCount > 0) resultParts.push(`${insertedCount}개 추가`);
      if (updatedCount > 0) resultParts.push(`${updatedCount}개 수정`);
      if (deletedCount > 0) resultParts.push(`${deletedCount}개 삭제`);

      const resultMessage = resultParts.length > 0 ? resultParts.join(", ") : "변경 사항 없음";

      toast.success(`저장 완료: ${resultMessage}`);

      // 🆕 저장 성공 후 제어 관리 실행 (다중 테이블 저장 시 소스 테이블과 일치하는 섹션만 실행)
      if (config.enableDataflowControl && config.dataflowConfig?.flowConfig?.flowId) {
        const flowId = config.dataflowConfig.flowConfig.flowId;

        try {
          // 플로우 소스 테이블 조회
          const { getFlowSourceTable } = await import("@/lib/api/nodeFlows");
          const flowSourceInfo = await getFlowSourceTable(flowId);

          if (flowSourceInfo.sourceTable) {
            // 각 섹션 확인하여 소스 테이블과 일치하는 섹션 찾기
            let controlExecuted = false;

            for (const [sectionId, sectionItems] of Object.entries(tableSectionData)) {
              const sectionConfig = sections.find((s: any) => s.id === sectionId);
              const sectionTargetTable = sectionConfig?.tableConfig?.saveConfig?.targetTable || tableName;

              // 소스 테이블과 일치하는 섹션만 제어 실행
              if (sectionTargetTable === flowSourceInfo.sourceTable && sectionItems.length > 0) {
                // 공통 필드 + 해당 섹션 데이터 병합하여 sourceData 생성
                const sourceData = sectionItems.map((item: any) => ({
                  ...commonFieldsData,
                  ...item,
                }));

                // 제어 관리용 컨텍스트 생성
                const controlContext: ButtonActionContext = {
                  ...context,
                  selectedRowsData: sourceData,
                  formData: commonFieldsData,
                };

                // 제어 관리 실행
                await this.executeAfterSaveControl(config, controlContext);
                controlExecuted = true;
                break; // 첫 번째 매칭 섹션만 실행
              }
            }

            // 매칭되는 섹션이 없으면 메인 테이블 확인
            if (!controlExecuted && tableName === flowSourceInfo.sourceTable) {
              const controlContext: ButtonActionContext = {
                ...context,
                selectedRowsData: [commonFieldsData],
                formData: commonFieldsData,
              };

              await this.executeAfterSaveControl(config, controlContext);
            }
          } else {
            console.log("⚠️ [handleUniversalFormModalTableSectionSave] 플로우 소스 테이블 없음 - 제어 스킵");
          }
        } catch (controlError) {
          console.error("❌ [handleUniversalFormModalTableSectionSave] 제어 관리 실행 오류:", controlError);
          // 제어 관리 실패는 저장 성공에 영향주지 않음
        }
      }

      // 저장 성공 이벤트 발생
      window.dispatchEvent(new CustomEvent("saveSuccess"));
      window.dispatchEvent(new CustomEvent("refreshTable"));
      // EditModal 닫기 이벤트 발생
      window.dispatchEvent(new CustomEvent("closeEditModal"));

      return { handled: true, success: true };
    } catch (error: any) {
      console.error("❌ [handleUniversalFormModalTableSectionSave] 저장 오류:", error);
      showErrorToast("테이블 섹션 데이터 저장에 실패했습니다", error, {
        guidance: "입력값을 확인하고 다시 시도해 주세요.",
      });
      return { handled: true, success: false };
    }
  }

  /**
   * 두 객체 간 변경 사항 확인
   */
  private static checkForChanges(original: Record<string, any>, current: Record<string, any>): boolean {
    // 비교할 필드 목록 (메타데이터 제외)
    const fieldsToCompare = new Set([
      ...Object.keys(original).filter((k) => !k.startsWith("_")),
      ...Object.keys(current).filter((k) => !k.startsWith("_")),
    ]);

    for (const field of fieldsToCompare) {
      // 시스템 필드는 비교에서 제외
      if (["created_date", "updated_date", "created_by", "updated_by", "writer"].includes(field)) {
        continue;
      }

      const originalValue = original[field];
      const currentValue = current[field];

      // null/undefined 통일 처리
      const normalizedOriginal = originalValue === null || originalValue === undefined ? "" : String(originalValue);
      const normalizedCurrent = currentValue === null || currentValue === undefined ? "" : String(currentValue);

      if (normalizedOriginal !== normalizedCurrent) {
        console.log(`  📝 변경 감지: ${field} = "${normalizedOriginal}" → "${normalizedCurrent}"`);
        return true;
      }
    }

    return false;
  }

  /**
   * 🆕 배치 저장 액션 처리 (SelectedItemsDetailInput용 - 새로운 데이터 구조)
   * ItemData[] → 각 품목의 details 배열을 개별 레코드로 저장
   */
  private static async handleBatchSave(
    config: ButtonActionConfig,
    context: ButtonActionContext,
    selectedItemsKeys: string[],
  ): Promise<boolean> {
    const { formData, tableName, screenId, selectedRowsData, originalData } = context;

    if (!tableName || !screenId) {
      toast.error("저장에 필요한 정보가 부족합니다. (테이블명 또는 화면ID 누락)");
      return false;
    }

    try {
      let successCount = 0;
      let failCount = 0;
      const errors: string[] = [];

      // 🆕 부모 화면 데이터 준비 (parentDataMapping용)
      // selectedRowsData 또는 originalData를 parentData로 사용
      const parentData = selectedRowsData?.[0] || originalData || {};

      // 🆕 modalDataStore에서 누적된 모든 테이블 데이터 가져오기
      // (여러 단계 모달에서 전달된 데이터 접근용)
      let modalDataStoreRegistry: Record<string, any[]> = {};
      if (typeof window !== "undefined") {
        try {
          // Zustand store에서 데이터 가져오기
          const { useModalDataStore } = await import("@/stores/modalDataStore");
          modalDataStoreRegistry = useModalDataStore.getState().dataRegistry;
        } catch (error) {
          console.warn("⚠️ modalDataStore 로드 실패:", error);
        }
      }

      // 각 테이블의 첫 번째 항목을 modalDataStore로 변환
      const modalDataStore: Record<string, any> = {};
      Object.entries(modalDataStoreRegistry).forEach(([key, items]) => {
        if (Array.isArray(items) && items.length > 0) {
          // ModalDataItem[] → originalData 추출
          modalDataStore[key] = items.map((item) => item.originalData || item);
        }
      });

      // 각 SelectedItemsDetailInput 컴포넌트의 데이터 처리
      for (const key of selectedItemsKeys) {
        // 🆕 새로운 데이터 구조: ItemData[] with fieldGroups
        const items = formData[key] as Array<{
          id: string;
          originalData: any;
          fieldGroups: Record<string, Array<{ id: string; [key: string]: any }>>;
        }>;

        // 🆕 이 컴포넌트의 parentDataMapping 설정 가져오기
        const componentConfig = context.componentConfigs?.[key];
        const parentDataMapping = componentConfig?.parentDataMapping || [];

        // 🆕 각 품목의 그룹 간 조합(카티션 곱) 생성
        for (const item of items) {
          const groupKeys = Object.keys(item.fieldGroups);

          // 각 그룹의 항목 배열 가져오기
          const groupArrays = groupKeys.map((groupKey) => ({
            groupKey,
            entries: item.fieldGroups[groupKey] || [],
          }));

          // 카티션 곱 계산 함수
          const cartesianProduct = (arrays: any[][]): any[][] => {
            if (arrays.length === 0) return [[]];
            if (arrays.length === 1) return arrays[0].map((item) => [item]);

            const [first, ...rest] = arrays;
            const restProduct = cartesianProduct(rest);

            return first.flatMap((item) => restProduct.map((combination) => [item, ...combination]));
          };

          // 모든 그룹의 카티션 곱 생성
          const entryArrays = groupArrays.map((g) => g.entries);

          // 🆕 모든 그룹이 비어있는지 확인
          const allGroupsEmpty = entryArrays.every((arr) => arr.length === 0);

          let combinations: any[][];
          if (allGroupsEmpty) {
            // 🆕 모든 그룹이 비어있으면 빈 조합 하나 생성 (품목 기본 정보만으로 저장)

            combinations = [[]];
          } else {
            // 빈 그룹을 필터링하여 카티션 곱 계산 (빈 그룹은 무시)
            const nonEmptyArrays = entryArrays.filter((arr) => arr.length > 0);
            combinations = nonEmptyArrays.length > 0 ? cartesianProduct(nonEmptyArrays) : [[]];
          }

          // 각 조합을 개별 레코드로 저장
          for (let i = 0; i < combinations.length; i++) {
            const combination = combinations[i];
            try {
              // 🆕 부모 데이터 매핑 적용
              const mappedData: any = {};

              // 1. parentDataMapping 설정이 있으면 적용
              if (parentDataMapping.length > 0) {
                for (const mapping of parentDataMapping) {
                  let sourceData: any;
                  const sourceTableName = mapping.sourceTable;
                  const selectedItemTable = componentConfig?.sourceTable;

                  if (sourceTableName === selectedItemTable) {
                    sourceData = item.originalData;
                  } else {
                    const tableData = modalDataStore[sourceTableName];
                    if (tableData && Array.isArray(tableData) && tableData.length > 0) {
                      sourceData = tableData[0];
                    } else {
                      sourceData = parentData;
                    }
                  }

                  const sourceValue = sourceData[mapping.sourceField];

                  if (sourceValue !== undefined && sourceValue !== null) {
                    mappedData[mapping.targetField] = sourceValue;
                  } else if (mapping.defaultValue !== undefined) {
                    mappedData[mapping.targetField] = mapping.defaultValue;
                  }
                }
              } else {
                // 🔧 parentDataMapping 설정이 없는 경우 기본 매핑 (하위 호환성)
                if (item.originalData.id) {
                  mappedData.item_id = item.originalData.id;
                }

                if (parentData.id || parentData.customer_id) {
                  mappedData.customer_id = parentData.customer_id || parentData.id;
                }
              }

              // 공통 필드 복사 (company_code, currency_code 등)
              if (item.originalData.company_code && !mappedData.company_code) {
                mappedData.company_code = item.originalData.company_code;
              }
              if (item.originalData.currency_code && !mappedData.currency_code) {
                mappedData.currency_code = item.originalData.currency_code;
              }

              // 원본 데이터로 시작 (매핑된 데이터 사용)
              let mergedData = { ...mappedData };

              // 각 그룹의 항목 데이터를 순차적으로 병합
              for (let j = 0; j < combination.length; j++) {
                const entry = combination[j];
                const { id, ...entryData } = entry; // id 제외
                mergedData = { ...mergedData, ...entryData };
              }

              // 🆕 조합 저장 시 id 필드 제거 (각 조합이 독립된 새 레코드가 되도록)
              const { id: _removedId, ...dataWithoutId } = mergedData;

              // 사용자 정보 추가
              if (!context.userId) {
                throw new Error("사용자 정보를 불러올 수 없습니다.");
              }

              const writerValue = context.userId;
              const companyCodeValue = context.companyCode || "";

              const dataWithUserInfo = {
                ...dataWithoutId,
                writer: dataWithoutId.writer || writerValue,
                created_by: writerValue,
                updated_by: writerValue,
                company_code: dataWithoutId.company_code || companyCodeValue,
              };

              // INSERT 실행
              const { DynamicFormApi } = await import("@/lib/api/dynamicForm");
              const saveResult = await DynamicFormApi.saveFormData({
                screenId,
                tableName,
                data: dataWithUserInfo,
              });

              if (saveResult.success) {
                successCount++;
              } else {
                failCount++;
                errors.push(`품목 ${item.id} > 조합 ${i + 1}: ${saveResult.message}`);
              }
            } catch (error: any) {
              failCount++;
              errors.push(`품목 ${item.id} > 조합 ${i + 1}: ${error.message}`);
            }
          }
        }
      }

      // 결과 토스트
      if (failCount === 0) {
        toast.success(`${successCount}개 항목이 저장되었습니다.`);
      } else if (successCount === 0) {
        showErrorToast(`${errors.length}개 항목 저장에 모두 실패했습니다`, errors.join("\n"), {
          guidance: "입력 데이터를 확인하고 다시 시도해 주세요.",
        });
        return false;
      } else {
        toast.warning(`${successCount}개 성공, ${failCount}개 실패: ${errors.join(", ")}`);
      }

      // 테이블과 플로우 새로고침
      context.onRefresh?.();
      context.onFlowRefresh?.();

      // 저장 성공 후 이벤트 발생
      window.dispatchEvent(new CustomEvent("closeEditModal"));
      window.dispatchEvent(new CustomEvent("saveSuccessInModal"));

      return true;
    } catch (error: any) {
      console.error("배치 저장 오류:", error);
      showErrorToast("배치 저장 중 오류가 발생했습니다", error, {
        guidance: "저장 대상 데이터를 확인하고 다시 시도해 주세요.",
      });
      return false;
    }
  }

  /**
   * 삭제 액션 처리
   */
  private static async handleDelete(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    const { formData, tableName, screenId, selectedRowsData, flowSelectedData } = context;

    try {
      // 플로우 선택 데이터 우선 사용
      const dataToDelete = flowSelectedData && flowSelectedData.length > 0 ? flowSelectedData : selectedRowsData;

      // 다중 선택된 데이터가 있는 경우
      if (dataToDelete && dataToDelete.length > 0) {
        console.log(`다중 삭제 액션 실행: ${dataToDelete.length}개 항목`, dataToDelete);

        // 테이블의 기본키 조회
        let primaryKeys: string[] = [];
        if (tableName) {
          try {
            const primaryKeysResult = await DynamicFormApi.getTablePrimaryKeys(tableName);
            if (primaryKeysResult.success && primaryKeysResult.data) {
              primaryKeys = primaryKeysResult.data;
            }
          } catch (error) {
            console.warn("기본키 조회 실패, 폴백 방법 사용:", error);
          }
        }

        // 각 선택된 항목을 삭제
        for (const rowData of dataToDelete) {
          let deleteId: any = null;

          // 1순위: 데이터베이스에서 조회한 기본키 사용
          if (primaryKeys.length > 0) {
            const primaryKey = primaryKeys[0]; // 첫 번째 기본키 사용
            deleteId = rowData[primaryKey];
          }

          // 2순위: 폴백 - 일반적인 ID 필드명들 시도
          if (!deleteId) {
            deleteId =
              rowData.id ||
              rowData.objid ||
              rowData.pk ||
              rowData.ID ||
              rowData.OBJID ||
              rowData.PK ||
              // 테이블별 기본키 패턴들
              rowData.sales_no ||
              rowData.contract_no ||
              rowData.order_no ||
              rowData.seq_no ||
              rowData.code ||
              rowData.code_id ||
              rowData.user_id ||
              rowData.menu_id;

            // _no로 끝나는 필드들 찾기
            if (!deleteId) {
              const noField = Object.keys(rowData).find((key) => key.endsWith("_no") && rowData[key]);
              if (noField) deleteId = rowData[noField];
            }

            // _id로 끝나는 필드들 찾기
            if (!deleteId) {
              const idField = Object.keys(rowData).find((key) => key.endsWith("_id") && rowData[key]);
              if (idField) deleteId = rowData[idField];
            }
          }

          console.log("선택된 행 데이터:", rowData);
          console.log("최종 추출된 deleteId:", deleteId);

          if (deleteId) {
            console.log("다중 데이터 삭제:", { tableName, screenId, id: deleteId });

            // screenId 전달하여 제어관리 실행 가능하도록 함
            const deleteResult = await DynamicFormApi.deleteFormDataFromTable(deleteId, tableName, screenId);
            if (!deleteResult.success) {
              throw new Error(`ID ${deleteId} 삭제 실패: ${deleteResult.message}`);
            }
          } else {
            console.error("삭제 ID를 찾을 수 없습니다. 행 데이터:", rowData);
            throw new Error(
              `삭제 ID를 찾을 수 없습니다. 기본키: ${primaryKeys.join(", ")}, 사용 가능한 필드: ${Object.keys(rowData).join(", ")}`,
            );
          }
        }

        // 데이터 소스에 따라 적절한 새로고침 호출
        if (flowSelectedData && flowSelectedData.length > 0) {
          context.onFlowRefresh?.(); // 플로우 새로고침
        } else {
          context.onRefresh?.(); // 테이블 새로고침

          // 🆕 분할 패널 등 전역 테이블 새로고침 이벤트 발생
          window.dispatchEvent(new CustomEvent("refreshTable"));
        }

        toast.success(config.successMessage || `${dataToDelete.length}개 항목이 삭제되었습니다.`);
        return true;
      }

      // 단일 삭제 (기존 로직)
      if (tableName && screenId && formData.id) {
        console.log("단일 데이터 삭제:", { tableName, screenId, id: formData.id });

        // 실제 삭제 API 호출 - screenId 전달하여 제어관리 실행 가능하도록 함
        const deleteResult = await DynamicFormApi.deleteFormDataFromTable(formData.id, tableName, screenId);

        if (!deleteResult.success) {
          throw new Error(deleteResult.message || "삭제에 실패했습니다.");
        }
      } else {
        throw new Error("삭제에 필요한 정보가 부족합니다. (ID, 테이블명 또는 화면ID 누락)");
      }

      context.onRefresh?.();

      // 🆕 분할 패널 등 전역 테이블 새로고침 이벤트 발생
      window.dispatchEvent(new CustomEvent("refreshTable"));

      toast.success(config.successMessage || "삭제되었습니다.");
      return true;
    } catch (error) {
      console.error("삭제 오류:", error);
      throw error; // 에러를 다시 던져서 컴포넌트에서 처리하도록 함
    }
  }

  /**
   * 초기화 액션 처리
   */
  private static handleReset(config: ButtonActionConfig, context: ButtonActionContext): boolean {
    const { formData, onFormDataChange } = context;

    // 폼 데이터 초기화 - 각 필드를 개별적으로 초기화
    if (onFormDataChange && formData) {
      Object.keys(formData).forEach((key) => {
        onFormDataChange(key, "");
      });
    }

    toast.success(config.successMessage || "초기화되었습니다.");
    return true;
  }

  /**
   * 취소 액션 처리
   */
  private static handleCancel(config: ButtonActionConfig, context: ButtonActionContext): boolean {
    const { onClose } = context;

    onClose?.();
    return true;
  }

  /**
   * 네비게이션 액션 처리
   */
  private static handleNavigate(config: ButtonActionConfig, context: ButtonActionContext): boolean {
    let targetUrl = config.targetUrl;

    // 화면 ID가 지정된 경우 URL 생성
    if (config.targetScreenId) {
      targetUrl = `/screens/${config.targetScreenId}`;
    }

    if (targetUrl) {
      window.location.href = targetUrl;
      return true;
    }

    toast.error("이동할 페이지가 지정되지 않았습니다.");
    return false;
  }

  /**
   * 연관 데이터 버튼의 선택 데이터로 모달 열기
   * RelatedDataButtons 컴포넌트에서 선택된 버튼 데이터를 모달로 전달
   */
  private static async handleOpenRelatedModal(
    config: ButtonActionConfig,
    context: ButtonActionContext,
  ): Promise<boolean> {
    // 버튼 설정에서 targetScreenId 가져오기 (여러 위치에서 확인)
    const targetScreenId = config.relatedModalConfig?.targetScreenId || config.targetScreenId;

    if (!targetScreenId) {
      console.error("❌ [openRelatedModal] targetScreenId가 설정되지 않았습니다.");
      toast.error("모달 화면 ID가 설정되지 않았습니다.");
      return false;
    }

    // RelatedDataButtons에서 선택된 데이터 가져오기
    const relatedData = window.__relatedButtonsSelectedData;

    if (!relatedData?.selectedItem) {
      console.warn("⚠️ [openRelatedModal] 선택된 버튼이 없습니다.");
      toast.warning("먼저 버튼을 선택해주세요.");
      return false;
    }

    const { selectedItem, config: relatedConfig } = relatedData;

    // 데이터 매핑 적용
    const initialData: Record<string, any> = {};

    if (relatedConfig?.modalLink?.dataMapping && relatedConfig.modalLink.dataMapping.length > 0) {
      relatedConfig.modalLink.dataMapping.forEach((mapping) => {
        if (mapping.sourceField === "value") {
          initialData[mapping.targetField] = selectedItem.value;
        } else if (mapping.sourceField === "id") {
          initialData[mapping.targetField] = selectedItem.id;
        } else if (selectedItem.rawData[mapping.sourceField] !== undefined) {
          initialData[mapping.targetField] = selectedItem.rawData[mapping.sourceField];
        }
      });
    } else {
      // 기본 매핑: id를 routing_version_id로 전달

      initialData["routing_version_id"] = selectedItem.value || selectedItem.id;
    }

    // 모달 열기 이벤트 발생 (ScreenModal은 editData를 사용)
    window.dispatchEvent(
      new CustomEvent("openScreenModal", {
        detail: {
          screenId: targetScreenId,
          title: config.modalTitle,
          description: config.modalDescription,
          editData: initialData, // ScreenModal은 editData로 폼 데이터를 받음
          onSuccess: () => {
            // 성공 후 데이터 새로고침
            window.dispatchEvent(new CustomEvent("refreshTableData"));
          },
        },
      }),
    );

    return true;
  }

  /**
   * 모달 액션 처리 (통합)
   * - passSelectedData: true (기본) → 선택된 데이터를 모달에 전달
   * - autoDetectDataSource: true → TableList/SplitPanel 등에서 데이터 소스 자동 감지
   * - fieldMappings → 필드 이름 매핑 지원
   */
  private static async handleModal(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    const passSelectedData = config.passSelectedData !== false; // 기본: true
    const autoDetectDataSource = config.autoDetectDataSource === true;

    if (!config.targetScreenId) {
      toast.error("모달로 열 화면이 지정되지 않았습니다.");
      return false;
    }

    // 1. 화면 정보 가져오기 (제목/설명이 미설정 시 화면명에서 가져옴)
    let screenInfo: any = null;
    if (!config.modalTitle || !config.modalDescription) {
      try {
        screenInfo = await screenApi.getScreen(config.targetScreenId);
      } catch (error) {
        console.warn("화면 정보를 가져오지 못했습니다:", error);
      }
    }
    let description = config.modalDescription || screenInfo?.description || "";

    // 2. 데이터 소스 및 선택된 데이터 수집
    let selectedData: any[] = [];
    let parentData: Record<string, any> = {};
    let dataSourceId: string | undefined;

    if (passSelectedData) {
      // 2-1. 자동 감지 모드
      if (autoDetectDataSource) {
        dataSourceId = config.dataSourceId;

        // TableList, V2List 또는 SplitPanelLayout에서 자동 감지
        if (!dataSourceId && context.allComponents) {
          // 1. table-list 컴포넌트 찾기
          const tableListComponent = context.allComponents.find(
            (comp: any) => comp.componentType === "table-list" && comp.componentConfig?.tableName,
          );

          if (tableListComponent) {
            dataSourceId = tableListComponent.componentConfig.tableName;
          } else {
            // 2. v2-list 컴포넌트 찾기
            const v2ListComponent = context.allComponents.find(
              (comp: any) =>
                comp.componentType === "v2-list" &&
                (comp.componentConfig?.dataSource?.table || comp.componentConfig?.tableName),
            );

            if (v2ListComponent) {
              dataSourceId =
                v2ListComponent.componentConfig.dataSource?.table || v2ListComponent.componentConfig.tableName;
              console.log("✨ V2List 자동 감지:", {
                componentId: v2ListComponent.id,
                tableName: dataSourceId,
              });
            } else {
              // 3. split-panel-layout 컴포넌트 찾기
              const splitPanelComponent = context.allComponents.find(
                (comp: any) =>
                  comp.componentType === "split-panel-layout" && comp.componentConfig?.leftPanel?.tableName,
              );
              if (splitPanelComponent) {
                dataSourceId = splitPanelComponent.componentConfig.leftPanel.tableName;
              }
            }
          }
        }

        dataSourceId = dataSourceId || context.tableName || "default";

        // modalDataStore에서 데이터 가져오기
        try {
          const { useModalDataStore } = await import("@/stores/modalDataStore");
          const dataRegistry = useModalDataStore.getState().dataRegistry;
          const modalData = dataRegistry[dataSourceId] || [];

          if (modalData.length === 0) {
            console.warn("⚠️ 선택된 데이터가 없습니다:", dataSourceId);
            toast.warning("선택된 데이터가 없습니다. 먼저 항목을 선택해주세요.");
            return false;
          }

          selectedData = modalData.map((item: any) => item.originalData || item);
          const rawParentData = modalData[0]?.originalData || modalData[0] || {};

          // 필드 매핑 적용
          if (config.fieldMappings?.length) {
            // 매핑이 있으면 매핑된 필드만 전달
            parentData = {};
            config.fieldMappings.forEach((mapping) => {
              if (mapping.sourceField && mapping.targetField && rawParentData[mapping.sourceField] !== undefined) {
                parentData[mapping.targetField] = rawParentData[mapping.sourceField];
              }
            });
          } else {
            // 매핑이 없으면 모든 필드 전달 (동일 컬럼명 자동 매핑)
            parentData = { ...rawParentData };
          }
        } catch (error) {
          console.error("❌ 데이터 확인 실패:", error);
          showErrorToast("상위 데이터 조회에 실패했습니다", error, {
            guidance: "데이터 소스 연결 상태를 확인해 주세요.",
          });
          return false;
        }
      } else {
        // 2-2. 기본 모드: context에서 직접 가져오기
        selectedData = context.selectedRowsData || [];
        const rawParentData = context.splitPanelParentData || selectedData[0] || {};

        // 필드 매핑 적용
        if (config.fieldMappings?.length && selectedData.length > 0) {
          // 매핑이 있으면 매핑된 필드만 전달
          parentData = {};
          config.fieldMappings.forEach((mapping) => {
            if (mapping.sourceField && mapping.targetField && rawParentData[mapping.sourceField] !== undefined) {
              parentData[mapping.targetField] = rawParentData[mapping.sourceField];
            }
          });
        } else {
          // 매핑이 없으면 모든 필드 전달 (동일 컬럼명 자동 매핑)
          parentData = { ...rawParentData };
        }
      }
    }

    // 3. 동적 모달 제목 생성
    let finalTitle = config.modalTitle || screenInfo?.screenName || "데이터 등록";

    // 블록 기반 제목 처리
    if (config.modalTitleBlocks?.length) {
      try {
        const { useModalDataStore } = await import("@/stores/modalDataStore");
        const dataRegistry = useModalDataStore.getState().dataRegistry;
        const titleParts: string[] = [];

        config.modalTitleBlocks.forEach((block) => {
          if (block.type === "text") {
            titleParts.push(block.value);
          } else if (block.type === "field" && block.tableName && block.value) {
            const tableData = dataRegistry[block.tableName];
            if (tableData?.length > 0) {
              const firstItem = tableData[0].originalData || tableData[0];
              const value = firstItem[block.value];
              titleParts.push(value !== undefined && value !== null ? String(value) : block.label || block.value);
            } else {
              titleParts.push(block.label || block.value);
            }
          }
        });

        finalTitle = titleParts.join("");
      } catch (error) {
        console.warn("동적 제목 생성 실패:", error);
      }
    }
    // {tableName.columnName} 패턴 처리
    else if (finalTitle.includes("{")) {
      try {
        const { useModalDataStore } = await import("@/stores/modalDataStore");
        const dataRegistry = useModalDataStore.getState().dataRegistry;
        const matches = finalTitle.match(/\{([^}]+)\}/g);

        if (matches) {
          matches.forEach((match) => {
            const path = match.slice(1, -1);
            const [tableName, columnName] = path.split(".");
            if (tableName && columnName) {
              const tableData = dataRegistry[tableName];
              if (tableData?.length > 0) {
                const firstItem = tableData[0].originalData || tableData[0];
                const value = firstItem[columnName];
                if (value !== undefined && value !== null) {
                  finalTitle = finalTitle.replace(match, String(value));
                }
              }
            }
          });
        }
      } catch {
        // 동적 제목 변환 실패 시 무시
      }
    }

    // 4. 모달 열기 이벤트 발생
    // 🔧 수정: openModalWithData는 "신규 등록 + 연결 데이터 전달"용이므로
    // editData가 아닌 splitPanelParentData로 전달해야 채번 등이 정상 작동함
    const isPassDataMode = passSelectedData && selectedData.length > 0;

    // 🔧 isEditMode 옵션이 명시적으로 true인 경우에만 수정 모드로 처리
    const useAsEditData = config.isEditMode === true;

    const modalEvent = new CustomEvent("openScreenModal", {
      detail: {
        screenId: config.targetScreenId,
        title: finalTitle,
        description: description,
        size: config.modalSize || "md",
        selectedData: selectedData,
        selectedIds: selectedData.map((row: any) => row.id).filter(Boolean),
        // 🔧 수정: isEditMode가 명시적으로 true인 경우에만 editData로 전달
        // 기본적으로는 splitPanelParentData로 전달하여 신규 등록 + 연결 데이터 모드
        editData: useAsEditData && isPassDataMode ? parentData : undefined,
        splitPanelParentData: isPassDataMode ? parentData : undefined,
        urlParams: dataSourceId ? { dataSourceId } : undefined,
        // 🆕 필드 매핑 정보 전달 - ScreenModal에서 매핑된 필드를 필터링하지 않도록
        fieldMappings: config.fieldMappings,
      },
    });

    window.dispatchEvent(modalEvent);

    // 모달 열기는 UI 전환이므로 성공 토스트를 표시하지 않음
    // (저장 등 실제 액션 완료 시에만 토스트 표시)

    return true;
  }

  /**
   * 🆕 데이터를 전달하면서 모달 열기 액션 처리
   */
  private static async handleOpenModalWithData(
    config: ButtonActionConfig,
    context: ButtonActionContext,
  ): Promise<boolean> {
    // 🆕 1. 현재 화면의 TableList 또는 SplitPanelLayout 자동 감지
    let dataSourceId = config.dataSourceId;

    if (!dataSourceId && context.allComponents) {
      // TableList 우선 감지
      const tableListComponent = context.allComponents.find(
        (comp: any) => comp.componentType === "table-list" && comp.componentConfig?.tableName,
      );

      if (tableListComponent) {
        dataSourceId = tableListComponent.componentConfig.tableName;
        console.log("✨ TableList 자동 감지:", {
          componentId: tableListComponent.id,
          tableName: dataSourceId,
        });
      } else {
        // TableList가 없으면 SplitPanelLayout의 좌측 패널 감지
        const splitPanelComponent = context.allComponents.find(
          (comp: any) => comp.componentType === "split-panel-layout" && comp.componentConfig?.leftPanel?.tableName,
        );

        if (splitPanelComponent) {
          dataSourceId = splitPanelComponent.componentConfig.leftPanel.tableName;
          console.log("✨ 분할 패널 좌측 테이블 자동 감지:", {
            componentId: splitPanelComponent.id,
            tableName: dataSourceId,
          });
        }
      }
    }

    // 여전히 없으면 context.tableName 또는 "default" 사용
    if (!dataSourceId) {
      dataSourceId = context.tableName || "default";
    }

    // 🆕 2. modalDataStore에서 현재 선택된 데이터 확인
    try {
      const { useModalDataStore } = await import("@/stores/modalDataStore");
      const dataRegistry = useModalDataStore.getState().dataRegistry;

      const modalData = dataRegistry[dataSourceId] || [];

      if (modalData.length === 0) {
        console.warn("⚠️ 선택된 데이터가 없습니다:", dataSourceId);
        toast.warning("선택된 데이터가 없습니다. 먼저 항목을 선택해주세요.");
        return false;
      }
    } catch (error) {
      console.error("❌ 데이터 확인 실패:", error);
      showErrorToast("모달 데이터 확인에 실패했습니다", error, {
        guidance: "데이터 소스를 확인하고 다시 시도해 주세요.",
      });
      return false;
    }

    // 6. 동적 모달 제목 생성
    const { useModalDataStore } = await import("@/stores/modalDataStore");
    const dataRegistry = useModalDataStore.getState().dataRegistry;

    let finalTitle = "데이터 입력";

    // 🆕 블록 기반 제목 (우선순위 1)
    if (config.modalTitleBlocks && config.modalTitleBlocks.length > 0) {
      const titleParts: string[] = [];

      config.modalTitleBlocks.forEach((block) => {
        if (block.type === "text") {
          // 텍스트 블록: 그대로 추가
          titleParts.push(block.value);
        } else if (block.type === "field") {
          // 필드 블록: 데이터에서 값 가져오기
          const tableName = block.tableName;
          const columnName = block.value;

          if (tableName && columnName) {
            const tableData = dataRegistry[tableName];
            if (tableData && tableData.length > 0) {
              const firstItem = tableData[0].originalData || tableData[0];
              const value = firstItem[columnName];

              if (value !== undefined && value !== null) {
                titleParts.push(String(value));
                console.log(`✨ 동적 필드: ${tableName}.${columnName} → ${value}`);
              } else {
                // 데이터 없으면 라벨 표시
                titleParts.push(block.label || columnName);
              }
            } else {
              // 테이블 데이터 없으면 라벨 표시
              titleParts.push(block.label || columnName);
            }
          }
        }
      });

      finalTitle = titleParts.join("");
    }
    // 기존 방식: {tableName.columnName} 패턴 (우선순위 2)
    else if (config.modalTitle) {
      finalTitle = config.modalTitle;

      if (finalTitle.includes("{")) {
        const matches = finalTitle.match(/\{([^}]+)\}/g);

        if (matches) {
          matches.forEach((match) => {
            const path = match.slice(1, -1); // {item_info.item_name} → item_info.item_name
            const [tableName, columnName] = path.split(".");

            if (tableName && columnName) {
              const tableData = dataRegistry[tableName];
              if (tableData && tableData.length > 0) {
                const firstItem = tableData[0].originalData || tableData[0];
                const value = firstItem[columnName];

                if (value !== undefined && value !== null) {
                  finalTitle = finalTitle.replace(match, String(value));
                  console.log(`✨ 동적 제목: ${match} → ${value}`);
                }
              }
            }
          });
        }
      }
    }

    // 7. 모달 열기 + URL 파라미터로 dataSourceId 전달
    if (config.targetScreenId) {
      // config에 modalDescription이 있으면 우선 사용
      let description = config.modalDescription || "";

      // config에 없으면 화면 정보에서 가져오기
      if (!description) {
        try {
          const screenInfo = await screenApi.getScreen(config.targetScreenId);
          description = screenInfo?.description || "";
        } catch (error) {
          console.warn("화면 설명을 가져오지 못했습니다:", error);
        }
      }

      // 🆕 부모 화면의 선택된 데이터 가져오기 (excludeFilter에서 사용)
      const rawParentData = dataRegistry[dataSourceId]?.[0]?.originalData || dataRegistry[dataSourceId]?.[0] || {};

      // 🆕 필드 매핑 적용 (소스 컬럼 → 타겟 컬럼)
      const parentData = { ...rawParentData };
      if (config.fieldMappings && Array.isArray(config.fieldMappings) && config.fieldMappings.length > 0) {
        config.fieldMappings.forEach((mapping: { sourceField: string; targetField: string }) => {
          if (mapping.sourceField && mapping.targetField && rawParentData[mapping.sourceField] !== undefined) {
            // 타겟 필드에 소스 필드 값 복사
            parentData[mapping.targetField] = rawParentData[mapping.sourceField];
            console.log(`  ✅ ${mapping.sourceField} → ${mapping.targetField}: ${rawParentData[mapping.sourceField]}`);
          }
        });
      }

      // 🆕 modalDataStore에서 선택된 전체 데이터 가져오기 (RepeatScreenModal에서 사용)
      const modalData = dataRegistry[dataSourceId] || [];
      const selectedData = modalData.map((item: any) => item.originalData || item);
      const selectedIds = selectedData.map((row: any) => row.id).filter(Boolean);

      // 🆕 전역 모달 상태 업데이트를 위한 이벤트 발생 (URL 파라미터 포함)
      const modalEvent = new CustomEvent("openScreenModal", {
        detail: {
          screenId: config.targetScreenId,
          title: finalTitle, // 🆕 동적 제목 사용
          description: description,
          size: config.modalSize || "lg", // 데이터 입력 화면은 기본 large
          urlParams: { dataSourceId }, // 🆕 주 데이터 소스만 전달 (나머지는 modalDataStore에서 자동으로 찾음)
          splitPanelParentData: parentData, // 🆕 부모 데이터 전달 (excludeFilter에서 사용)
          // 🆕 선택된 데이터 전달 (RepeatScreenModal에서 groupedData로 사용)
          selectedData: selectedData,
          selectedIds: selectedIds,
        },
      });

      window.dispatchEvent(modalEvent);

      // 모달 열기는 UI 전환이므로 성공 토스트를 표시하지 않음

      return true;
    } else {
      console.error("모달로 열 화면이 지정되지 않았습니다.");
      toast.error("대상 화면이 지정되지 않았습니다.");
      return false;
    }
  }

  /**
   * 새 창 액션 처리
   */
  private static handleNewWindow(config: ButtonActionConfig, context: ButtonActionContext): boolean {
    let targetUrl = config.targetUrl;

    // 화면 ID가 지정된 경우 URL 생성
    if (config.targetScreenId) {
      targetUrl = `/screens/${config.targetScreenId}`;
    }

    if (targetUrl) {
      const windowFeatures = `width=${config.popupWidth || 800},height=${config.popupHeight || 600},scrollbars=yes,resizable=yes`;
      window.open(targetUrl, "_blank", windowFeatures);
      return true;
    }

    toast.error("열 페이지가 지정되지 않았습니다.");
    return false;
  }

  /**
   * 팝업 액션 처리
   */
  private static handlePopup(config: ButtonActionConfig, context: ButtonActionContext): boolean {
    // 팝업은 새 창과 유사하지만 더 작은 크기
    return this.handleNewWindow(
      {
        ...config,
        popupWidth: config.popupWidth || 600,
        popupHeight: config.popupHeight || 400,
      },
      context,
    );
  }

  /**
   * 검색 액션 처리
   */
  private static handleSearch(config: ButtonActionConfig, context: ButtonActionContext): boolean {
    const { formData, onRefresh } = context;

    console.log("검색 실행:", formData);

    // 검색 조건 검증
    const hasSearchCriteria = Object.values(formData).some(
      (value) => value !== null && value !== undefined && value !== "",
    );

    if (!hasSearchCriteria) {
      toast.warning("검색 조건을 입력해주세요.");
      return false;
    }

    // 검색 실행 (데이터 새로고침)
    onRefresh?.();

    // 검색 조건을 URL 파라미터로 추가 (선택사항)
    const searchParams = new URLSearchParams();
    Object.entries(formData).forEach(([key, value]) => {
      if (value !== null && value !== undefined && value !== "") {
        searchParams.set(key, String(value));
      }
    });

    // URL 업데이트 (히스토리에 추가하지 않음)
    if (searchParams.toString()) {
      const newUrl = `${window.location.pathname}?${searchParams.toString()}`;
      window.history.replaceState({}, "", newUrl);
    }

    toast.success(config.successMessage || "검색을 실행했습니다.");
    return true;
  }

  /**
   * 추가 액션 처리
   */
  private static handleAdd(config: ButtonActionConfig, context: ButtonActionContext): boolean {
    console.log("추가 액션 실행:", context);
    // 추가 로직 구현 (예: 새 레코드 생성 폼 열기)
    return true;
  }

  /**
   * 편집 액션 처리
   */
  private static async handleEdit(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    const { selectedRowsData, flowSelectedData } = context;

    // 플로우 선택 데이터 우선 사용
    const dataToEdit = flowSelectedData && flowSelectedData.length > 0 ? flowSelectedData : selectedRowsData;

    // 선택된 데이터가 없는 경우
    if (!dataToEdit || dataToEdit.length === 0) {
      toast.error("수정할 항목을 선택해주세요.");
      return false;
    }

    // 편집 화면이 설정되지 않은 경우
    if (!config.targetScreenId) {
      toast.error("수정 폼 화면이 설정되지 않았습니다. 버튼 설정에서 수정 폼 화면을 선택해주세요.");
      return false;
    }

    if (dataToEdit.length === 1) {
      // 단일 항목 편집
      const rowData = dataToEdit[0];

      await this.openEditForm(config, rowData, context);
    } else {
      // 다중 항목 편집 - 현재는 단일 편집만 지원
      toast.error("현재 단일 항목 편집만 지원됩니다. 하나의 항목만 선택해주세요.");
      return false;
    }

    return true;
  }

  /**
   * 편집 폼 열기 (단일 항목)
   */
  private static async openEditForm(
    config: ButtonActionConfig,
    rowData: any,
    context: ButtonActionContext,
  ): Promise<void> {
    const editMode = config.editMode || "modal";

    switch (editMode) {
      case "modal":
        // 모달로 편집 폼 열기
        await this.openEditModal(config, rowData, context);
        break;

      case "navigate":
        // 새 페이지로 이동
        this.navigateToEditScreen(config, rowData, context);
        break;

      case "inline":
        // 현재 화면에서 인라인 편집 (향후 구현)
        toast.info("인라인 편집 기능은 향후 지원 예정입니다.");
        break;

      default:
        // 기본값: 모달
        await this.openEditModal(config, rowData, context);
    }
  }

  /**
   * 편집 모달 열기
   */
  private static async openEditModal(
    config: ButtonActionConfig,
    rowData: any,
    context: ButtonActionContext,
    isCreateMode: boolean = false, // 🆕 복사 모드에서 true로 전달
  ): Promise<void> {
    const { groupByColumns = [] } = config;

    // PK 값 추출 (우선순위: id > ID > 첫 번째 필드)
    let primaryKeyValue: any;
    if (rowData.id !== undefined && rowData.id !== null) {
      primaryKeyValue = rowData.id;
    } else if (rowData.ID !== undefined && rowData.ID !== null) {
      primaryKeyValue = rowData.ID;
    } else {
      primaryKeyValue = Object.values(rowData)[0];
    }

    // 1. config에 editModalDescription이 있으면 우선 사용
    let description = config.editModalDescription || "";

    // 2. config에 없으면 화면 정보에서 가져오기
    let screenInfo: any = null;
    if (config.targetScreenId) {
      try {
        screenInfo = await screenApi.getScreen(config.targetScreenId);
        if (!description) {
          description = screenInfo?.description || "";
        }
      } catch (error) {
        console.warn("화면 설명을 가져오지 못했습니다:", error);
      }
    }

    // 🆕 화면이 분할 패널을 포함하는지 확인 (레이아웃에 screen-split-panel 컴포넌트가 있는지)
    let hasSplitPanel = false;
    if (config.targetScreenId) {
      try {
        const layoutData = await screenApi.getLayout(config.targetScreenId);
        if (layoutData?.components) {
          hasSplitPanel = layoutData.components.some(
            (comp: any) =>
              comp.type === "screen-split-panel" ||
              comp.componentType === "screen-split-panel" ||
              comp.type === "split-panel-layout" ||
              comp.componentType === "split-panel-layout",
          );
        }
        // console.log("🔍 [openEditModal] 분할 패널 확인:", {
        //   targetScreenId: config.targetScreenId,
        //   hasSplitPanel,
        //   componentTypes: layoutData?.components?.map((c: any) => c.type || c.componentType) || [],
        // });
      } catch (error) {
        console.warn("레이아웃 정보를 가져오지 못했습니다:", error);
      }
    }

    // 🆕 분할 패널 화면인 경우 ScreenModal 사용 (editData 전달)
    if (hasSplitPanel) {
      const screenModalEvent = new CustomEvent("openScreenModal", {
        detail: {
          screenId: config.targetScreenId,
          title: isCreateMode ? config.editModalTitle || "데이터 복사" : config.editModalTitle || "데이터 수정",
          description: description,
          size: config.modalSize || "lg",
          // 🔧 복사 모드에서는 editData 대신 splitPanelParentData로 전달하여 채번이 생성되도록 함
          editData: isCreateMode ? undefined : rowData,
          splitPanelParentData: isCreateMode ? rowData : undefined,
          isCreateMode: isCreateMode, // 🆕 복사 모드 플래그 전달
        },
      });
      window.dispatchEvent(screenModalEvent);
      return;
    }

    // 🔧 일반 화면은 EditModal 사용 (groupByColumns는 EditModal에서 처리)
    const modalEvent = new CustomEvent("openEditModal", {
      detail: {
        screenId: config.targetScreenId,
        title: isCreateMode ? config.editModalTitle || "데이터 복사" : config.editModalTitle || "데이터 수정",
        description: description,
        modalSize: config.modalSize || "lg",
        editData: rowData,
        isCreateMode: isCreateMode, // 🆕 복사 모드에서 INSERT로 처리되도록
        groupByColumns: groupByColumns.length > 0 ? groupByColumns : undefined, // 🆕 그룹핑 컬럼 전달
        tableName: context.tableName, // 🆕 테이블명 전달
        buttonConfig: config, // 🆕 버튼 설정 전달 (제어로직 실행용)
        buttonContext: context, // 🆕 버튼 컨텍스트 전달 (screenId, userId 등)
        onSave: () => {
          context.onRefresh?.();
        },
      },
    });

    window.dispatchEvent(modalEvent);
  }

  /**
   * 편집 화면으로 이동
   */
  private static navigateToEditScreen(config: ButtonActionConfig, rowData: any, context: ButtonActionContext): void {
    const rowId = rowData.id || rowData.objid || rowData.pk || rowData.ID || rowData.OBJID || rowData.PK;

    if (!rowId) {
      toast.error("수정할 항목의 ID를 찾을 수 없습니다.");
      return;
    }

    const editUrl = `/screens/${config.targetScreenId}?mode=edit&id=${rowId}`;

    window.location.href = editUrl;
  }

  /**
   * 복사 액션 처리 (품목코드 초기화)
   */
  private static async handleCopy(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    try {
      const { selectedRowsData, flowSelectedData } = context;

      // 플로우 선택 데이터 우선 사용
      const dataToCopy = flowSelectedData && flowSelectedData.length > 0 ? flowSelectedData : selectedRowsData;

      // 선택된 데이터가 없는 경우
      if (!dataToCopy || dataToCopy.length === 0) {
        toast.error("복사할 항목을 선택해주세요.");
        return false;
      }

      // 복사 화면이 설정되지 않은 경우
      if (!config.targetScreenId) {
        toast.error("복사 폼 화면이 설정되지 않았습니다. 버튼 설정에서 복사 폼 화면을 선택해주세요.");
        return false;
      }

      if (dataToCopy.length === 1) {
        // 단일 항목 복사
        const rowData = dataToCopy[0];

        // 복사 시 제거할 필드들
        const copiedData = { ...rowData };
        const fieldsToRemove = [
          // ID 필드 (새 레코드 생성)
          "id",
          "ID",
          // 날짜 필드 (자동 생성)
          "created_date",
          "createdDate",
          "updated_date",
          "updatedDate",
          "created_at",
          "createdAt",
          "updated_at",
          "updatedAt",
          "reg_date",
          "regDate",
          "mod_date",
          "modDate",
        ];

        // 제거할 필드 삭제
        fieldsToRemove.forEach((field) => {
          if (copiedData[field] !== undefined) {
            delete copiedData[field];
          }
        });

        // 품목코드 필드 초기화 (여러 가능한 필드명 확인)
        const itemCodeFields = [
          "item_code",
          "itemCode",
          "item_no",
          "itemNo",
          "item_number",
          "itemNumber",
          "품목코드",
          "품번",
          "code",
        ];

        // 🆕 화면 설정에서 채번 규칙 가져오기
        const screenNumberingRules: Record<string, string> = {};
        if (config.targetScreenId) {
          try {
            const { screenApi } = await import("@/lib/api/screen");
            const layout = await screenApi.getLayout(config.targetScreenId);

            // 레이아웃에서 채번 규칙이 설정된 컴포넌트 찾기
            const findNumberingRules = (components: any[]): void => {
              for (const comp of components) {
                const compConfig = comp.componentConfig || {};
                // text-input 컴포넌트의 채번 규칙 확인
                if (
                  compConfig.autoGeneration?.type === "numbering_rule" &&
                  compConfig.autoGeneration?.options?.numberingRuleId
                ) {
                  const columnName = compConfig.columnName || comp.columnName;
                  if (columnName) {
                    screenNumberingRules[columnName] = compConfig.autoGeneration.options.numberingRuleId;
                  }
                }
                // 중첩된 컴포넌트 확인
                if (comp.children && Array.isArray(comp.children)) {
                  findNumberingRules(comp.children);
                }
              }
            };

            if (layout?.components) {
              findNumberingRules(layout.components);
            }
          } catch (error) {
            console.warn("⚠️ 화면 레이아웃 조회 실패:", error);
          }
        }

        // 품목코드 필드를 찾아서 무조건 공백으로 초기화
        let resetFieldName = "";
        for (const field of itemCodeFields) {
          if (copiedData[field] !== undefined) {
            const originalValue = copiedData[field];
            const ruleIdKey = `${field}_numberingRuleId`;

            // 1순위: 원본 데이터에서 채번 규칙 ID 확인
            // 2순위: 화면 설정에서 채번 규칙 ID 확인
            const numberingRuleId = rowData[ruleIdKey] || screenNumberingRules[field];
            const hasNumberingRule =
              numberingRuleId !== undefined && numberingRuleId !== null && numberingRuleId !== "";

            // 품목코드를 무조건 공백으로 초기화
            copiedData[field] = "";

            // 채번 규칙 ID가 있으면 복사 (저장 시 자동 생성)
            if (hasNumberingRule) {
              copiedData[ruleIdKey] = numberingRuleId;
            } else {
            }

            resetFieldName = field;
            break;
          }
        }

        // 작성자 정보를 현재 사용자로 변경
        const writerFields = ["writer", "creator", "reg_user", "regUser", "created_by", "createdBy"];
        writerFields.forEach((field) => {
          if (copiedData[field] !== undefined && context.userId) {
            copiedData[field] = context.userId;
          }
        });

        if (resetFieldName) {
          toast.success("복사본이 생성되었습니다. 품목코드는 저장 시 자동으로 생성됩니다.");
        } else {
          console.warn("⚠️ 품목코드 필드를 찾을 수 없습니다. 전체 데이터를 복사합니다.");
          console.warn("⚠️ 사용 가능한 필드:", Object.keys(copiedData));
          toast.info("복사본이 생성됩니다.");
        }

        await this.openCopyForm(config, copiedData, context);
      } else {
        // 다중 항목 복사 - 현재는 단일 복사만 지원
        toast.error("현재 단일 항목 복사만 지원됩니다. 하나의 항목만 선택해주세요.");
        return false;
      }

      return true;
    } catch (error: any) {
      console.error("❌ 복사 액션 실행 중 오류:", error);
      showErrorToast("데이터 복사에 실패했습니다", error, {
        guidance: "복사 대상 데이터를 확인하고 다시 시도해 주세요.",
      });
      return false;
    }
  }

  /**
   * 복사 폼 열기 (단일 항목)
   */
  private static async openCopyForm(
    config: ButtonActionConfig,
    rowData: any,
    context: ButtonActionContext,
  ): Promise<void> {
    try {
      const editMode = config.editMode || "modal";

      switch (editMode) {
        case "modal":
          // 모달로 복사 폼 열기 (편집 모달 재사용, INSERT 모드로)

          await this.openEditModal(config, rowData, context, true); // 🆕 isCreateMode: true
          break;

        case "navigate":
          // 새 페이지로 이동

          this.navigateToCopyScreen(config, rowData, context);
          break;

        default:
          // 기본값: 모달

          this.openEditModal(config, rowData, context, true); // 🆕 isCreateMode: true
      }
    } catch (error: any) {
      console.error("❌ openCopyForm 실행 중 오류:", error);
      throw error;
    }
  }

  /**
   * 복사 화면으로 네비게이션
   */
  private static navigateToCopyScreen(config: ButtonActionConfig, rowData: any, context: ButtonActionContext): void {
    const copyUrl = `/screens/${config.targetScreenId}?mode=copy`;

    // 복사할 데이터를 sessionStorage에 저장
    sessionStorage.setItem("copyData", JSON.stringify(rowData));

    window.location.href = copyUrl;
  }

  /**
   * 닫기 액션 처리
   */
  private static handleClose(config: ButtonActionConfig, context: ButtonActionContext): boolean {
    console.log("닫기 액션 실행:", context);
    context.onClose?.();
    return true;
  }

  /**
   * 제어 전용 액션 처리 (조건 체크만 수행)
   */
  private static async handleControl(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    // 🔥 제어 조건이 설정되어 있는지 확인

    if (!config.dataflowConfig || !config.enableDataflowControl) {
      console.warn("⚠️ 제어관리가 비활성화되어 있습니다:", {
        enableDataflowControl: config.enableDataflowControl,
        hasDataflowConfig: !!config.dataflowConfig,
      });
      toast.warning(
        "제어관리가 활성화되지 않았습니다. 버튼 설정에서 '제어관리 활성화'를 체크하고 조건을 설정해주세요.",
      );
      return false;
    }

    try {
      // 🔥 확장된 제어 컨텍스트 생성
      // 자동으로 적절한 controlDataSource 결정
      let controlDataSource = config.dataflowConfig.controlDataSource;

      if (!controlDataSource) {
        // 설정이 없으면 자동 판단 (우선순위 순서대로)
        if (context.flowSelectedData && context.flowSelectedData.length > 0) {
          controlDataSource = "flow-selection";
        } else if (context.selectedRowsData && context.selectedRowsData.length > 0) {
          controlDataSource = "table-selection";
        } else if (context.formData && Object.keys(context.formData).length > 0) {
          controlDataSource = "form";
        } else {
          controlDataSource = "form"; // 기본값
        }
      }

      const extendedContext: ExtendedControlContext = {
        formData: context.formData || {},
        selectedRows: context.selectedRows || [],
        selectedRowsData: context.selectedRowsData || [],
        flowSelectedData: context.flowSelectedData || [],
        flowSelectedStepId: context.flowSelectedStepId,
        controlDataSource,
      };

      // 🔥 새로운 버튼 액션 실행 시스템 사용
      // flowConfig가 있으면 controlMode가 명시되지 않아도 플로우 모드로 간주
      const hasFlowConfig = config.dataflowConfig?.flowConfig && config.dataflowConfig.flowConfig.flowId;
      const isFlowMode = config.dataflowConfig?.controlMode === "flow" || hasFlowConfig;

      if (isFlowMode && config.dataflowConfig?.flowConfig) {
        const { flowId, executionTiming } = config.dataflowConfig.flowConfig;

        if (!flowId) {
          console.error("❌ 플로우 ID가 없습니다");
          toast.error("플로우가 설정되지 않았습니다.");
          return false;
        }

        try {
          // 노드 플로우 실행 API 호출 (API 클라이언트 사용)
          const { executeNodeFlow } = await import("@/lib/api/nodeFlows");

          // 데이터 소스 준비: controlDataSource 설정 기반
          let sourceData: any = null;
          let dataSourceType: string = controlDataSource || "none";

          // controlDataSource 설정에 따라 데이터 선택
          switch (controlDataSource) {
            case "flow-selection":
              if (context.flowSelectedData && context.flowSelectedData.length > 0) {
                sourceData = context.flowSelectedData;
                console.log("🌊 플로우 선택 데이터 사용:", {
                  stepId: context.flowSelectedStepId,
                  dataCount: sourceData.length,
                  sourceData,
                });
              } else {
                console.warn("⚠️ flow-selection 모드이지만 선택된 플로우 데이터가 없습니다.");
                toast.error("플로우에서 데이터를 먼저 선택해주세요.");
                return false;
              }
              break;

            case "table-selection":
              if (context.selectedRowsData && context.selectedRowsData.length > 0) {
                sourceData = context.selectedRowsData;
              } else {
                console.warn("⚠️ table-selection 모드이지만 선택된 행이 없습니다.");
                toast.error("테이블에서 처리할 항목을 먼저 선택해주세요.");
                return false;
              }
              break;

            case "form":
              if (context.formData && Object.keys(context.formData).length > 0) {
                // 🔧 배열 값을 쉼표 구분 문자열로 변환
                sourceData = [normalizeFormDataArrays(context.formData)];
              } else {
                console.warn("⚠️ form 모드이지만 폼 데이터가 없습니다.");
              }
              break;

            case "both":
              // 폼 + 테이블 선택 (데이터 병합)
              // 🔥 각 selectedRowsData 항목에 formData를 병합하여 전달
              // 이렇게 해야 메일 발송 시 부모 데이터(상품명 등)와 폼 데이터(수신자 등)가 모두 변수로 사용 가능
              if (context.selectedRowsData && context.selectedRowsData.length > 0) {
                sourceData = context.selectedRowsData.map((row: any) => ({
                  ...row,
                  ...(context.formData || {}),
                }));
                console.log("🔀 폼 + 테이블 선택 데이터 병합:", {
                  dataCount: sourceData.length,
                  sourceData,
                });
              } else if (context.formData && Object.keys(context.formData).length > 0) {
                sourceData = [context.formData];
                console.log("🔀 폼 데이터만 사용 (선택된 행 없음):", sourceData);
              }
              break;

            default:
              // 자동 판단 (설정이 없는 경우)
              if (context.flowSelectedData && context.flowSelectedData.length > 0) {
                sourceData = context.flowSelectedData;
                dataSourceType = "flow-selection";
                console.log("🌊 [자동] 플로우 선택 데이터 사용");
              } else if (context.selectedRowsData && context.selectedRowsData.length > 0) {
                // 🔥 selectedRowsData가 있으면 formData도 함께 병합
                // 모달에서 부모 데이터(selectedRowsData)와 폼 입력(formData)을 모두 사용할 수 있도록
                if (context.formData && Object.keys(context.formData).length > 0) {
                  sourceData = context.selectedRowsData.map((row: any) => ({
                    ...row,
                    ...context.formData,
                  }));
                  dataSourceType = "both";
                } else {
                  sourceData = context.selectedRowsData;
                  dataSourceType = "table-selection";
                }
              } else if (context.formData && Object.keys(context.formData).length > 0) {
                // 🔧 배열 값을 쉼표 구분 문자열로 변환
                sourceData = [normalizeFormDataArrays(context.formData)];
                dataSourceType = "form";
              }
              break;
          }

          const result = await executeNodeFlow(flowId, {
            dataSourceType,
            sourceData,
            context,
          });

          if (result.success) {
            toast.success("플로우 실행이 완료되었습니다.");

            // 플로우 새로고침 (플로우 위젯용)
            if (context.onFlowRefresh) {
              context.onFlowRefresh();
            }

            // 테이블 새로고침 (일반 테이블용)
            if (context.onRefresh) {
              context.onRefresh();
            }

            return true;
          } else {
            console.error("❌ 노드 플로우 실행 실패:", result);
            showErrorToast(
              config.errorMessage || "플로우 실행에 실패했습니다",
              result.message,
              { guidance: "플로우 설정과 데이터를 확인해 주세요." }
            );
            return false;
          }
        } catch (error) {
          console.error("❌ 노드 플로우 실행 오류:", error);
          showErrorToast("플로우 실행 중 오류가 발생했습니다", error, {
            guidance: "플로우 연결 상태와 데이터를 확인해 주세요.",
          });
          return false;
        }
      } else if (config.dataflowConfig?.controlMode === "relationship" && config.dataflowConfig?.relationshipConfig) {
        // 🔥 table-selection 모드일 때 선택된 행 데이터를 formData에 병합
        let mergedFormData = { ...context.formData } || {};

        if (
          controlDataSource === "table-selection" &&
          context.selectedRowsData &&
          context.selectedRowsData.length > 0
        ) {
          // 선택된 첫 번째 행의 데이터를 formData에 병합
          const selectedRowData = context.selectedRowsData[0];
          mergedFormData = { ...mergedFormData, ...selectedRowData };
        }

        // 새로운 ImprovedButtonActionExecutor 사용
        const buttonConfig = {
          actionType: config.type,
          dataflowConfig: config.dataflowConfig,
          enableDataflowControl: true, // 관계 기반 제어가 설정된 경우 활성화
        };

        const executionResult = await ImprovedButtonActionExecutor.executeButtonAction(buttonConfig, mergedFormData, {
          buttonId: context.buttonId || "unknown",
          screenId: context.screenId || "unknown",
          userId: context.userId || "unknown",
          companyCode: context.companyCode || "*",
          startTime: Date.now(),
          contextData: context,
        });

        if (executionResult.success) {
          toast.success(config.successMessage || "관계 실행이 완료되었습니다.");

          // 새로고침이 필요한 경우
          if (context.onRefresh) {
            context.onRefresh();
          }

          return true;
        } else {
          console.error("❌ 관계 실행 실패:", executionResult);
          showErrorToast(
            config.errorMessage || "관계 실행에 실패했습니다",
            executionResult.message || executionResult.error,
            { guidance: "관계 설정과 데이터를 확인해 주세요." }
          );
          return false;
        }
      } else {
        // 제어 없음 - 성공 처리

        // 새로고침이 필요한 경우
        if (context.onRefresh) {
          context.onRefresh();
        }

        return true;
      }
    } catch (error) {
      console.error("제어 조건 검증 중 오류:", error);
      showErrorToast("제어 조건 검증에 실패했습니다", error, {
        guidance: "제어 설정을 확인해 주세요.",
      });
      return false;
    }
  }

  /**
   * 저장 후 제어 실행 (After Timing)
   * EditModal 등 외부에서도 호출 가능하도록 public으로 변경
   * 다중 제어 순차 실행 지원
   */
  public static async executeAfterSaveControl(config: ButtonActionConfig, context: ButtonActionContext): Promise<void> {
    // 제어 데이터 소스 결정
    let controlDataSource = config.dataflowConfig?.controlDataSource;
    if (!controlDataSource) {
      controlDataSource = "form"; // 저장 후에는 기본적으로 form 데이터 사용
    }

    const extendedContext: ExtendedControlContext = {
      formData: context.formData || {},
      selectedRows: context.selectedRows || [],
      selectedRowsData: context.selectedRowsData || [],
      controlDataSource,
    };

    // 🔥 다중 제어 지원 (flowControls 배열)
    const flowControls = config.dataflowConfig?.flowControls || [];
    if (flowControls.length > 0) {
      // 순서대로 정렬
      const sortedControls = [...flowControls].sort((a: any, b: any) => (a.order || 0) - (b.order || 0));

      // 노드 플로우 실행 API
      const { executeNodeFlow } = await import("@/lib/api/nodeFlows");

      // 데이터 소스 준비: context-data 모드는 배열을 기대함
      // 🔧 저장 후 제어: savedData > formData > selectedRowsData
      // - 저장 후 제어에서는 방금 저장된 데이터(savedData)가 가장 중요!
      // - selectedRowsData는 왼쪽 패널 선택 데이터일 수 있으므로 마지막 순위
      let sourceData: any[];
      if (context.savedData) {
        // 저장된 데이터가 있으면 우선 사용 (저장 API 응답)
        sourceData = Array.isArray(context.savedData) ? context.savedData : [context.savedData];
        console.log("📦 [executeAfterSaveControl] savedData 사용:", sourceData);
        console.log("📦 [executeAfterSaveControl] savedData 필드:", Object.keys(context.savedData));
        console.log("📦 [executeAfterSaveControl] savedData.sabun:", context.savedData.sabun);
      } else if (context.formData && Object.keys(context.formData).length > 0) {
        // 폼 데이터 사용 (🔧 배열 값을 쉼표 구분 문자열로 변환)
        sourceData = [normalizeFormDataArrays(context.formData)];
        console.log("📦 [executeAfterSaveControl] formData 사용:", sourceData);
      } else if (context.selectedRowsData && context.selectedRowsData.length > 0) {
        // 테이블 섹션 데이터 (마지막 순위)
        sourceData = context.selectedRowsData;
        console.log("📦 [executeAfterSaveControl] selectedRowsData 사용:", sourceData);
      } else {
        sourceData = [];
        console.warn("⚠️ [executeAfterSaveControl] 데이터 소스 없음!");
      }

      let allSuccess = true;
      const results: Array<{ flowId: number; flowName: string; success: boolean; message?: string }> = [];

      for (let i = 0; i < sortedControls.length; i++) {
        const control = sortedControls[i];

        // 유효하지 않은 flowId 스킵
        if (!control.flowId || control.flowId <= 0) {
          console.warn(`⚠️ [${i + 1}/${sortedControls.length}] 유효하지 않은 flowId, 스킵:`, control);
          continue;
        }

        // executionTiming 체크 (after만 실행)
        if (control.executionTiming && control.executionTiming !== "after") {
          continue;
        }

        console.log(
          `\n📍 [${i + 1}/${sortedControls.length}] 제어 실행: ${control.flowName} (flowId: ${control.flowId})`,
        );

        try {
          const result = await executeNodeFlow(control.flowId, {
            dataSourceType: controlDataSource,
            sourceData,
            context: extendedContext,
            // 저장 전 원본 데이터 전달 (after 타이밍에서 DB 기존값 비교용)
            originalData: context.originalData || null,
          });

          results.push({
            flowId: control.flowId,
            flowName: control.flowName,
            success: result.success,
            message: result.message,
          });

          if (result.success) {
          } else {
            console.error(`❌ [${i + 1}/${sortedControls.length}] 제어 실패: ${control.flowName} - ${result.message}`);
            allSuccess = false;
            // 이전 제어 실패 시 다음 제어 실행 중단
            console.warn("⚠️ 이전 제어 실패로 인해 나머지 제어 실행 중단");
            break;
          }
        } catch (error: any) {
          console.error(`❌ [${i + 1}/${sortedControls.length}] 제어 실행 오류: ${control.flowName}`, error);
          results.push({
            flowId: control.flowId,
            flowName: control.flowName,
            success: false,
            message: error.message,
          });
          allSuccess = false;
          break;
        }
      }

      // 결과 요약
      const successCount = results.filter((r) => r.success).length;
      const failCount = results.filter((r) => !r.success).length;
      console.log("\n📊 다중 제어 실행 완료:", {
        total: sortedControls.length,
        executed: results.length,
        success: successCount,
        failed: failCount,
      });

      if (allSuccess) {
        toast.success(`${successCount}개 제어 실행 완료`);
      } else {
        const failedNames = results.filter((r) => !r.success).map((r) => r.flowName || r.flowId).join(", ");
        showErrorToast(
          `제어 실행 중 일부 실패 (${successCount}/${results.length} 성공)`,
          failedNames ? `실패 항목: ${failedNames}` : undefined,
          { guidance: "실패한 제어 플로우 설정을 확인해 주세요." }
        );
      }

      return;
    }

    // 🔥 기존 단일 제어 실행 (하위 호환성)
    // dataflowTiming이 'after'가 아니면 실행하지 않음
    if (config.dataflowTiming && config.dataflowTiming !== "after") {
      return;
    }

    // 노드 플로우 방식 실행 (flowConfig가 있는 경우)
    const hasFlowConfig = config.dataflowConfig?.flowConfig && config.dataflowConfig.flowConfig.flowId;
    if (hasFlowConfig) {
      // executionTiming 체크
      const flowTiming = config.dataflowConfig.flowConfig.executionTiming;
      if (flowTiming && flowTiming !== "after") {
        return;
      }

      const { flowId } = config.dataflowConfig.flowConfig;

      try {
        // 노드 플로우 실행 API 호출
        const { executeNodeFlow } = await import("@/lib/api/nodeFlows");

        // 데이터 소스 준비: context-data 모드는 배열을 기대함
        // 🔧 저장 후 제어: savedData > formData > selectedRowsData
        // - 저장 후 제어에서는 방금 저장된 데이터(savedData)가 가장 중요!
        // - selectedRowsData는 왼쪽 패널 선택 데이터일 수 있으므로 마지막 순위
        let sourceData: any[];
        if (context.savedData) {
          // 저장된 데이터가 있으면 우선 사용 (저장 API 응답)
          sourceData = Array.isArray(context.savedData) ? context.savedData : [context.savedData];
          console.log("📦 [executeSingleFlowControl] savedData 사용:", sourceData);
        } else if (context.formData && Object.keys(context.formData).length > 0) {
          // 폼 데이터 사용 (🔧 배열 값을 쉼표 구분 문자열로 변환)
          sourceData = [normalizeFormDataArrays(context.formData)];
          console.log("📦 [executeSingleFlowControl] formData 사용:", sourceData);
        } else if (context.selectedRowsData && context.selectedRowsData.length > 0) {
          // 테이블 섹션 데이터 (마지막 순위)
          sourceData = context.selectedRowsData;
          console.log("📦 [executeSingleFlowControl] selectedRowsData 사용:", sourceData);
        } else {
          sourceData = [];
          console.warn("⚠️ [executeSingleFlowControl] 데이터 소스 없음!");
        }

        // repeat-screen-modal 데이터가 있으면 병합
        const repeatScreenModalKeys = Object.keys(context.formData || {}).filter((key) =>
          key.startsWith("_repeatScreenModal_"),
        );
        if (repeatScreenModalKeys.length > 0) {
        }

        const result = await executeNodeFlow(flowId, {
          dataSourceType: controlDataSource,
          sourceData,
          context: extendedContext,
        });

        if (result.success) {
          toast.success("제어 로직 실행이 완료되었습니다.");
        } else {
          console.error("❌ 저장 후 노드 플로우 실행 실패:", result);
          showErrorToast("저장은 완료되었으나 후속 제어 실행에 실패했습니다", result.message, {
            guidance: "제어 플로우 설정을 확인해 주세요. 데이터는 정상 저장되었습니다.",
          });
        }
      } catch (error: any) {
        console.error("❌ 저장 후 노드 플로우 실행 오류:", error);
        showErrorToast("저장은 완료되었으나 후속 제어 실행에 실패했습니다", error, {
          guidance: "제어 플로우 설정을 확인해 주세요. 데이터는 정상 저장되었습니다.",
        });
      }

      return; // 노드 플로우 실행 후 종료
    }

    // 관계 기반 제어 실행
    if (config.dataflowConfig?.controlMode === "relationship" && config.dataflowConfig?.relationshipConfig) {
      const buttonConfig = {
        actionType: config.type,
        dataflowConfig: config.dataflowConfig,
        enableDataflowControl: true,
      };

      const executionResult = await ImprovedButtonActionExecutor.executeButtonAction(
        buttonConfig,
        context.formData || {},
        {
          buttonId: context.buttonId || "unknown",
          screenId: context.screenId || "unknown",
          userId: context.userId || "unknown",
          companyCode: context.companyCode || "*",
          startTime: Date.now(),
          contextData: context,
        },
      );

      if (executionResult.success) {
        // 성공 토스트는 save 액션에서 이미 표시했으므로 추가로 표시하지 않음
      } else {
        console.error("❌ 저장 후 제어 실행 실패:", executionResult);
        showErrorToast("저장은 완료되었으나 연결된 제어 실행에 실패했습니다", executionResult.message || executionResult.error, {
          guidance: "제어 관계 설정을 확인해 주세요. 데이터는 정상 저장되었습니다.",
        });
      }
    }
  }

  /**
   * 관계도에서 가져온 액션들을 실행
   */
  private static async executeRelationshipActions(actions: any[], context: ButtonActionContext): Promise<void> {
    for (let i = 0; i < actions.length; i++) {
      const action = actions[i];

      try {
        const actionType = action.actionType || action.type; // actionType 우선, type 폴백

        switch (actionType) {
          case "save":
            await this.executeActionSave(action, context);
            break;
          case "update":
            await this.executeActionUpdate(action, context);
            break;
          case "delete":
            await this.executeActionDelete(action, context);
            break;
          case "insert":
            await this.executeActionInsert(action, context);
            break;
          default:
            console.warn(`❌ 지원되지 않는 액션 타입 (${i + 1}/${actions.length}):`, {
              actionType,
              actionName: action.name,
              fullAction: action,
            });
            // 지원되지 않는 액션은 오류로 처리하여 중단
            throw new Error(`지원되지 않는 액션 타입: ${actionType}`);
        }

        // 성공 토스트 (개별 액션별)
        toast.success(`${action.name || `액션 ${i + 1}`} 완료`);
      } catch (error) {
        const actionType = action.actionType || action.type;
        console.error(`❌ 액션 ${i + 1}/${actions.length} 실행 실패:`, action.name, error);

        showErrorToast(
          `'${action.name || `액션 ${i + 1}`}' 실행에 실패했습니다`,
          error,
          { guidance: `전체 ${actions.length}개 액션 중 ${i + 1}번째에서 중단되었습니다.` }
        );

        // 🚨 순차 실행 중단: 하나라도 실패하면 전체 중단
        throw new Error(
          `액션 ${i + 1}(${action.name})에서 실패하여 제어 프로세스를 중단합니다: ${error instanceof Error ? error.message : error}`,
        );
      }
    }

    toast.success(`총 ${actions.length}개 액션이 모두 성공적으로 완료되었습니다.`);
  }

  /**
   * 저장 액션 실행
   */
  private static async executeActionSave(action: any, context: ButtonActionContext): Promise<void> {
    // 🎯 필드 매핑 정보 사용하여 저장 데이터 구성
    let saveData: Record<string, any> = {};

    // 액션에 필드 매핑 정보가 있는지 확인
    if (action.fieldMappings && Array.isArray(action.fieldMappings)) {
      // 필드 매핑에 따라 데이터 구성
      action.fieldMappings.forEach((mapping: any) => {
        const { sourceField, targetField, defaultValue, valueType } = mapping;

        let value: any;

        // 값 소스에 따라 데이터 가져오기
        if (valueType === "form" && context.formData && sourceField) {
          value = context.formData[sourceField];
        } else if (valueType === "selected" && context.selectedRowsData?.[0] && sourceField) {
          value = context.selectedRowsData[0][sourceField];
        } else if (valueType === "default" || !sourceField) {
          value = defaultValue;
        }

        // 타겟 필드에 값 설정
        if (targetField && value !== undefined) {
          saveData[targetField] = value;
        }
      });
    } else {
      console.log("⚠️ 필드 매핑 정보가 없음, 기본 데이터 사용");
      // 폴백: 기존 방식
      saveData = {
        ...context.formData,
        ...context.selectedRowsData?.[0], // 선택된 데이터도 포함
      };
    }

    try {
      // 🔥 실제 저장 API 호출
      if (!context.tableName) {
        throw new Error("테이블명이 설정되지 않았습니다.");
      }

      const result = await DynamicFormApi.saveFormData({
        screenId: 0, // 임시값
        tableName: context.tableName,
        data: saveData,
      });

      if (result.success) {
        toast.success("데이터가 저장되었습니다.");
      } else {
        throw new Error(result.message || "저장 실패");
      }
    } catch (error: any) {
      console.error("❌ 저장 실패:", error);
      showErrorToast(`'${context.tableName}' 테이블 저장에 실패했습니다`, error, {
        guidance: "입력 데이터를 확인하고 다시 시도해 주세요.",
      });
      throw error;
    }
  }

  /**
   * 업데이트 액션 실행
   */
  private static async executeActionUpdate(action: any, context: ButtonActionContext): Promise<void> {
    // 🎯 필드 매핑 정보 사용하여 업데이트 데이터 구성
    let updateData: Record<string, any> = {};

    // 액션에 필드 매핑 정보가 있는지 확인
    if (action.fieldMappings && Array.isArray(action.fieldMappings)) {
      // 🔑 먼저 선택된 데이터의 모든 필드를 기본으로 포함 (기본키 보존)
      if (context.selectedRowsData?.[0]) {
        updateData = { ...context.selectedRowsData[0] };
      }

      // 필드 매핑에 따라 데이터 구성 (덮어쓰기)
      action.fieldMappings.forEach((mapping: any) => {
        const { sourceField, targetField, defaultValue, valueType } = mapping;

        let value: any;

        // 값 소스에 따라 데이터 가져오기
        if (valueType === "form" && context.formData && sourceField) {
          value = context.formData[sourceField];
        } else if (valueType === "selected" && context.selectedRowsData?.[0] && sourceField) {
          value = context.selectedRowsData[0][sourceField];
        } else if (valueType === "default" || !sourceField) {
          value = defaultValue;
        }

        // 타겟 필드에 값 설정 (덮어쓰기)
        if (targetField && value !== undefined) {
          updateData[targetField] = value;
        }
      });
    } else {
      console.log("⚠️ 필드 매핑 정보가 없음, 기본 데이터 사용");
      // 폴백: 기존 방식
      updateData = {
        ...context.formData,
        ...context.selectedRowsData?.[0],
      };
    }

    try {
      // 🔥 실제 업데이트 API 호출
      if (!context.tableName) {
        throw new Error("테이블명이 설정되지 않았습니다.");
      }

      // 먼저 ID 찾기
      const primaryKeysResult = await DynamicFormApi.getTablePrimaryKeys(context.tableName);
      let updateId: string | undefined;

      if (primaryKeysResult.success && primaryKeysResult.data && primaryKeysResult.data.length > 0) {
        updateId = updateData[primaryKeysResult.data[0]];
      }

      if (!updateId) {
        // 폴백: 일반적인 ID 필드들 확인
        const commonIdFields = ["id", "objid", "pk", "sales_no", "contract_no"];
        for (const field of commonIdFields) {
          if (updateData[field]) {
            updateId = updateData[field];
            break;
          }
        }
      }

      if (!updateId) {
        throw new Error("업데이트할 항목의 ID를 찾을 수 없습니다.");
      }

      const result = await DynamicFormApi.updateFormData(updateId, {
        tableName: context.tableName,
        data: updateData,
      });

      if (result.success) {
        toast.success("데이터가 업데이트되었습니다.");
      } else {
        throw new Error(result.message || "업데이트 실패");
      }
    } catch (error: any) {
      console.error("❌ 업데이트 실패:", error);
      showErrorToast(`'${context.tableName}' 데이터 수정에 실패했습니다`, error, {
        guidance: "수정 데이터를 확인하고 다시 시도해 주세요.",
      });
      throw error;
    }
  }

  /**
   * 삭제 액션 실행
   */
  private static async executeActionDelete(action: any, context: ButtonActionContext): Promise<void> {
    // 실제 삭제 로직 (기존 handleDelete와 유사)
    if (!context.selectedRowsData || context.selectedRowsData.length === 0) {
      throw new Error("삭제할 항목을 선택해주세요.");
    }

    const deleteData = context.selectedRowsData[0];
    console.log("삭제할 데이터:", deleteData);

    try {
      // 🔥 실제 삭제 API 호출
      if (!context.tableName) {
        throw new Error("테이블명이 설정되지 않았습니다.");
      }

      // 기존 handleDelete와 동일한 로직으로 ID 찾기
      const primaryKeysResult = await DynamicFormApi.getTablePrimaryKeys(context.tableName);
      let deleteId: string | undefined;

      if (primaryKeysResult.success && primaryKeysResult.data && primaryKeysResult.data.length > 0) {
        deleteId = deleteData[primaryKeysResult.data[0]];
      }

      if (!deleteId) {
        // 폴백: 일반적인 ID 필드들 확인
        const commonIdFields = ["id", "objid", "pk", "sales_no", "contract_no"];
        for (const field of commonIdFields) {
          if (deleteData[field]) {
            deleteId = deleteData[field];
            break;
          }
        }
      }

      if (!deleteId) {
        throw new Error("삭제할 항목의 ID를 찾을 수 없습니다.");
      }

      // screenId 전달하여 제어관리 실행 가능하도록 함
      const result = await DynamicFormApi.deleteFormDataFromTable(deleteId, context.tableName, context.screenId);

      if (result.success) {
        toast.success("데이터가 삭제되었습니다.");
      } else {
        throw new Error(result.message || "삭제 실패");
      }
    } catch (error: any) {
      console.error("❌ 삭제 실패:", error);
      showErrorToast(`'${context.tableName}' 데이터 삭제에 실패했습니다`, error, {
        guidance: "삭제 대상을 확인하고 다시 시도해 주세요.",
      });
      throw error;
    }
  }

  /**
   * 삽입 액션 실행 (체크박스 선택된 데이터를 필드매핑에 따라 새 테이블에 삽입)
   */
  private static async executeActionInsert(action: any, context: ButtonActionContext): Promise<void> {
    console.log("➕ 삽입 액션 실행:", action);

    let insertData: Record<string, any> = {};

    // 액션에 필드 매핑 정보가 있는지 확인
    if (action.fieldMappings && Array.isArray(action.fieldMappings)) {
      // 🎯 체크박스로 선택된 데이터가 있는지 확인
      if (!context.selectedRowsData || context.selectedRowsData.length === 0) {
        throw new Error("삽입할 소스 데이터를 선택해주세요. (테이블에서 체크박스 선택 필요)");
      }

      const sourceData = context.selectedRowsData[0]; // 첫 번째 선택된 데이터 사용

      // 필드 매핑에 따라 데이터 구성
      action.fieldMappings.forEach((mapping: any) => {
        const { sourceField, targetField, defaultValue } = mapping;
        // valueType이 없으면 기본값을 "selection"으로 설정
        const valueType = mapping.valueType || "selection";

        let value: any;

        // 값 소스에 따라 데이터 가져오기
        if (valueType === "form" && context.formData && sourceField) {
          // 폼 데이터에서 가져오기
          value = context.formData[sourceField];
        } else if (valueType === "selection" && sourceField) {
          // 선택된 테이블 데이터에서 가져오기 (다양한 필드명 시도)
          value =
            sourceData[sourceField] ||
            sourceData[sourceField + "_name"] || // 조인된 필드 (_name 접미사)
            sourceData[sourceField + "Name"]; // 카멜케이스
        } else if (valueType === "default" || (defaultValue !== undefined && defaultValue !== "")) {
          // 기본값 사용 (valueType이 "default"이거나 defaultValue가 있을 때)
          value = defaultValue;
        } else {
          console.warn(`⚠️ 매핑 실패: ${sourceField} → ${targetField} (값을 찾을 수 없음)`);
          console.warn(`  - valueType: ${valueType}, defaultValue: ${defaultValue}`);
          console.warn("  - 소스 데이터 키들:", Object.keys(sourceData));
          console.warn(`  - sourceData[${sourceField}] =`, sourceData[sourceField]);
          return; // 값이 없으면 해당 필드는 스킵
        }

        // 대상 필드에 값 설정
        if (targetField && value !== undefined && value !== null) {
          insertData[targetField] = value;
        }
      });
    } else {
      // 필드 매핑이 없으면 폼 데이터를 기본으로 사용
      insertData = { ...context.formData };
    }

    try {
      // 🔥 실제 삽입 API 호출 - 필수 매개변수 포함
      // 필드 매핑에서 첫 번째 targetTable을 찾거나 기본값 사용
      const targetTable = action.fieldMappings?.[0]?.targetTable || action.targetTable || "test_project_info";

      const formDataPayload = {
        screenId: 0, // 제어 관리에서는 screenId가 없으므로 0 사용
        tableName: targetTable, // 필드 매핑에서 대상 테이블명 가져오기
        data: insertData,
      };

      const result = await DynamicFormApi.saveFormData(formDataPayload);

      if (result.success) {
        toast.success("데이터가 타겟 테이블에 성공적으로 삽입되었습니다.");
      } else {
        throw new Error(result.message || "삽입 실패");
      }
    } catch (error) {
      console.error("❌ 삽입 실패:", error);
      showErrorToast("데이터 삽입에 실패했습니다", error, {
        guidance: "필수 입력 항목과 데이터 형식을 확인해 주세요.",
      });
      throw error;
    }
  }

  /**
   * 테이블 이력 보기 액션 처리
   */
  private static async handleViewTableHistory(
    config: ButtonActionConfig,
    context: ButtonActionContext,
  ): Promise<boolean> {
    console.log("📜 테이블 이력 보기 액션 실행:", { config, context });

    // 테이블명 결정 (설정 > 컨텍스트 > 폼 데이터)
    const tableName = config.historyTableName || context.tableName;
    if (!tableName) {
      toast.error("테이블명이 지정되지 않았습니다.");
      return false;
    }

    // 레코드 ID 가져오기 (선택사항 - 없으면 전체 테이블 이력)
    const recordIdField = config.historyRecordIdField || "id";
    const recordIdSource = config.historyRecordIdSource || "selected_row";

    let recordId: any = null;
    let recordLabel: string | undefined;

    switch (recordIdSource) {
      case "selected_row":
        // 선택된 행에서 가져오기 (선택사항)
        if (context.selectedRowsData && context.selectedRowsData.length > 0) {
          const selectedRow = context.selectedRowsData[0];
          recordId = selectedRow[recordIdField];

          // 라벨 필드가 지정되어 있으면 사용
          if (config.historyRecordLabelField) {
            recordLabel = selectedRow[config.historyRecordLabelField];
          }
        } else if (context.flowSelectedData && context.flowSelectedData.length > 0) {
          // 플로우 선택 데이터 폴백
          const selectedRow = context.flowSelectedData[0];
          recordId = selectedRow[recordIdField];

          if (config.historyRecordLabelField) {
            recordLabel = selectedRow[config.historyRecordLabelField];
          }
        }
        break;

      case "form_field":
        // 폼 필드에서 가져오기
        recordId = context.formData?.[recordIdField];
        if (config.historyRecordLabelField) {
          recordLabel = context.formData?.[config.historyRecordLabelField];
        }
        break;

      case "context":
        // 원본 데이터에서 가져오기
        recordId = context.originalData?.[recordIdField];
        if (config.historyRecordLabelField) {
          recordLabel = context.originalData?.[config.historyRecordLabelField];
        }
        break;
    }

    // recordId가 없어도 괜찮음 - 전체 테이블 이력 보기

    // 이력 모달 열기 (동적 import)
    try {
      const { TableHistoryModal } = await import("@/components/common/TableHistoryModal");
      const { createRoot } = await import("react-dom/client");

      // 모달 컨테이너 생성
      const modalContainer = document.createElement("div");
      document.body.appendChild(modalContainer);

      const root = createRoot(modalContainer);

      const closeModal = () => {
        root.unmount();
        document.body.removeChild(modalContainer);
      };

      root.render(
        React.createElement(TableHistoryModal, {
          open: true,
          onOpenChange: (open: boolean) => {
            if (!open) closeModal();
          },
          tableName,
          recordId,
          recordLabel,
          displayColumn: config.historyDisplayColumn,
        }),
      );

      return true;
    } catch (error) {
      console.error("❌ 이력 모달 열기 실패:", error);
      showErrorToast("이력 조회에 실패했습니다", error, {
        guidance: "이력 테이블 설정을 확인해 주세요.",
      });
      return false;
    }
  }

  /**
   * 엑셀 다운로드 액션 처리
   */
  private static async handleExcelDownload(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    try {
      // 동적 import로 엑셀 유틸리티 로드
      const { exportToExcel } = await import("@/lib/utils/excelExport");

      let dataToExport: any[] = [];
      let visibleColumns: string[] | undefined = undefined;
      let columnLabels: Record<string, string> | undefined = undefined;

      // 🆕 마스터-디테일 구조 확인 및 처리
      if (context.screenId) {
        const { DynamicFormApi } = await import("@/lib/api/dynamicForm");
        const relationResponse = await DynamicFormApi.getMasterDetailRelation(context.screenId);

        if (relationResponse.success && relationResponse.data) {
          // 마스터-디테일 구조인 경우 전용 API 사용

          const downloadResponse = await DynamicFormApi.getMasterDetailDownloadData(
            context.screenId,
            context.filterConditions,
          );

          if (downloadResponse.success && downloadResponse.data) {
            dataToExport = downloadResponse.data.data;
            visibleColumns = downloadResponse.data.columns;

            // 헤더와 컬럼 매핑
            columnLabels = {};
            downloadResponse.data.columns.forEach((col: string, index: number) => {
              columnLabels![col] = downloadResponse.data.headers[index] || col;
            });
          } else {
            showErrorToast("마스터-디테일 데이터 조회에 실패했습니다", null, {
              guidance: "데이터 소스 설정을 확인해 주세요.",
            });
            return false;
          }

          // 마스터-디테일 데이터 변환 및 다운로드
          if (visibleColumns && visibleColumns.length > 0 && dataToExport.length > 0) {
            dataToExport = dataToExport.map((row: any) => {
              const filteredRow: Record<string, any> = {};
              visibleColumns!.forEach((columnName: string) => {
                const label = columnLabels?.[columnName] || columnName;
                let value = row[columnName];

                // 카테고리 코드를 라벨로 변환 (CATEGORY_로 시작하는 값)
                if (value && typeof value === "string" && value.includes("CATEGORY_")) {
                  // 먼저 _label 필드 확인 (API에서 제공하는 경우)
                  const labelFieldName = `${columnName}_label`;
                  if (row[labelFieldName]) {
                    value = row[labelFieldName];
                  } else {
                    // _value_label 필드 확인
                    const valueLabelFieldName = `${columnName}_value_label`;
                    if (row[valueLabelFieldName]) {
                      value = row[valueLabelFieldName];
                    }
                  }
                }

                filteredRow[label] = value;
              });
              return filteredRow;
            });
          }

          // 파일명 생성
          let defaultFileName = relationResponse.data.masterTable || "데이터";
          if (typeof window !== "undefined") {
            const menuName = localStorage.getItem("currentMenuName");
            if (menuName) defaultFileName = menuName;
          }
          const _xd = new Date();
          const fileName = config.excelFileName || `${defaultFileName}_${_xd.getFullYear()}-${String(_xd.getMonth() + 1).padStart(2, "0")}-${String(_xd.getDate()).padStart(2, "0")}.xlsx`;
          const sheetName = config.excelSheetName || "Sheet1";

          await exportToExcel(dataToExport, fileName, sheetName, true);
          toast.success(`${dataToExport.length}개 행이 다운로드되었습니다.`);
          return true;
        }
      }

      // ✅ 기존 로직: 단일 테이블 처리
      if (context.tableName) {
        const { tableDisplayStore } = await import("@/stores/tableDisplayStore");
        const storedData = tableDisplayStore.getTableData(context.tableName);

        // 필터 조건은 저장소 또는 context에서 가져오기
        const filterConditions = storedData?.filterConditions || context.filterConditions;
        const searchTerm = storedData?.searchTerm || context.searchTerm;

        try {
          const { entityJoinApi } = await import("@/lib/api/entityJoin");

          const apiParams = {
            page: 1,
            size: 10000, // 최대 10,000개
            sortBy: context.sortBy || storedData?.sortBy || "id",
            sortOrder: (context.sortOrder || storedData?.sortOrder || "asc") as "asc" | "desc",
            search: filterConditions, // ✅ 필터 조건
            enableEntityJoin: true, // ✅ Entity 조인
            // autoFilter는 entityJoinApi.getTableDataWithJoins 내부에서 자동으로 적용됨
          };

          // 🔒 멀티테넌시 준수: autoFilter로 company_code 자동 적용
          const response = await entityJoinApi.getTableDataWithJoins(context.tableName, apiParams);

          // 🔒 멀티테넌시 확인
          const allData = Array.isArray(response) ? response : response?.data || [];
          const companyCodesInData = [...new Set(allData.map((row: any) => row.company_code))];

          if (companyCodesInData.length > 1) {
            console.error("❌ 멀티테넌시 위반! 여러 회사의 데이터가 섞여있습니다:", companyCodesInData);
          }

          // entityJoinApi는 EntityJoinResponse 또는 data 배열을 반환
          if (Array.isArray(response)) {
            // 배열로 직접 반환된 경우
            dataToExport = response;
          } else if (response && "data" in response) {
            // EntityJoinResponse 객체인 경우
            dataToExport = response.data;
          } else {
            console.error("❌ 예상치 못한 응답 형식:", response);
            showErrorToast("엑셀 데이터 조회에 실패했습니다", null, {
              guidance: "서버 응답 형식이 예상과 다릅니다. 관리자에게 문의해 주세요.",
            });
            return false;
          }
        } catch (error) {
          console.error("엑셀 다운로드: 데이터 조회 실패:", error);
          showErrorToast("엑셀 다운로드용 데이터 조회에 실패했습니다", error, {
            guidance: "네트워크 연결을 확인하고 다시 시도해 주세요.",
          });
          return false;
        }
      }
      // 폴백: 폼 데이터
      else if (context.formData && Object.keys(context.formData).length > 0) {
        dataToExport = [context.formData];
      }
      // 테이블명도 없고 폼 데이터도 없으면 에러
      else {
        toast.error("다운로드할 데이터 소스가 없습니다.", {
          description: "테이블 또는 폼 데이터가 설정되어 있지 않습니다. 버튼 설정을 확인해 주세요.",
        });
        return false;
      }

      // 배열이 아니면 배열로 변환
      if (!Array.isArray(dataToExport)) {
        if (typeof dataToExport === "object" && dataToExport !== null) {
          dataToExport = [dataToExport];
        } else {
          toast.error("다운로드할 데이터 형식이 올바르지 않습니다.", {
            description: "서버에서 받은 데이터 형식이 예상과 다릅니다. 관리자에게 문의해 주세요.",
          });
          return false;
        }
      }

      if (dataToExport.length === 0) {
        toast.error("다운로드할 데이터가 없습니다.", {
          description: "조회 조건에 맞는 데이터가 없습니다. 검색 조건을 변경해 보세요.",
        });
        return false;
      }

      // 파일명 생성 (메뉴 이름 우선 사용)
      let defaultFileName = context.tableName || "데이터";

      // localStorage에서 메뉴 이름 가져오기
      if (typeof window !== "undefined") {
        const menuName = localStorage.getItem("currentMenuName");
        if (menuName) {
          defaultFileName = menuName;
        }
      }

      const _xd2 = new Date();
      const fileName = config.excelFileName || `${defaultFileName}_${_xd2.getFullYear()}-${String(_xd2.getMonth() + 1).padStart(2, "0")}-${String(_xd2.getDate()).padStart(2, "0")}.xlsx`;
      const sheetName = config.excelSheetName || "Sheet1";
      const includeHeaders = config.excelIncludeHeaders !== false;

      // 🎨 화면 레이아웃에서 테이블 리스트 컴포넌트의 컬럼 설정 가져오기
      // visibleColumns, columnLabels는 함수 상단에서 이미 선언됨

      try {
        // 화면 레이아웃 데이터 가져오기 (별도 API 사용)
        const { apiClient } = await import("@/lib/api/client");
        const layoutResponse = await apiClient.get(`/screen-management/screens/${context.screenId}/layout`);

        if (layoutResponse.data?.success && layoutResponse.data?.data) {
          const layoutData = layoutResponse.data.data;

          // components가 문자열이면 파싱
          if (typeof layoutData.components === "string") {
            layoutData.components = JSON.parse(layoutData.components);
          }

          // 테이블 리스트 컴포넌트 찾기
          const findTableListComponent = (components: any[]): any => {
            if (!Array.isArray(components)) return null;

            for (const comp of components) {
              // componentType이 'table-list'인지 확인
              const isTableList = comp.componentType === "table-list";

              // componentConfig 안에서 테이블명 확인
              const matchesTable =
                comp.componentConfig?.selectedTable === context.tableName ||
                comp.componentConfig?.tableName === context.tableName;

              if (isTableList && matchesTable) {
                return comp;
              }
              if (comp.children && comp.children.length > 0) {
                const found = findTableListComponent(comp.children);
                if (found) return found;
              }
            }
            return null;
          };

          const tableListComponent = findTableListComponent(layoutData.components || []);

          if (tableListComponent && tableListComponent.componentConfig?.columns) {
            const columns = tableListComponent.componentConfig.columns;

            // visible이 true인 컬럼만 추출
            visibleColumns = columns.filter((col: any) => col.visible !== false).map((col: any) => col.columnName);

            // column_labels 테이블에서 실제 라벨 가져오기
            try {
              const columnsResponse = await apiClient.get(`/table-management/tables/${context.tableName}/columns`, {
                params: { page: 1, size: 9999 },
              });

              if (columnsResponse.data?.success && columnsResponse.data?.data) {
                let columnData = columnsResponse.data.data;

                // data가 객체이고 columns 필드가 있으면 추출
                if (columnData.columns && Array.isArray(columnData.columns)) {
                  columnData = columnData.columns;
                }

                if (Array.isArray(columnData)) {
                  columnLabels = {};

                  // API에서 가져온 라벨로 매핑
                  columnData.forEach((colData: any) => {
                    const colName = colData.column_name || colData.columnName;
                    // 우선순위: column_label > label > displayName > columnName
                    const labelValue = colData.column_label || colData.label || colData.displayName || colName;
                    if (colName && labelValue) {
                      columnLabels![colName] = labelValue;
                    }
                  });
                }
              }
            } catch (error) {
              // 실패 시 컴포넌트 설정의 displayName 사용
              columnLabels = {};
              columns.forEach((col: any) => {
                if (col.columnName) {
                  columnLabels![col.columnName] = col.displayName || col.label || col.columnName;
                }
              });
            }
          }
        }
      } catch (error) {
        console.error("❌ 화면 레이아웃 조회 실패:", error);
      }

      // Fallback: 레이아웃에서 컬럼 정보를 못 가져온 경우, table_type_columns에서 직접 조회
      // 시스템 컬럼 제외 + 라벨 적용으로 raw 컬럼명 노출 방지
      const SYSTEM_COLUMNS = ["id", "company_code", "created_date", "updated_date", "writer"];
      if ((!visibleColumns || visibleColumns.length === 0) && context.tableName && dataToExport.length > 0) {
        console.log("⚠️ 레이아웃에서 컬럼 설정을 찾지 못함 → table_type_columns에서 fallback 조회");
        try {
          const { apiClient } = await import("@/lib/api/client");
          const columnsResponse = await apiClient.get(`/table-management/tables/${context.tableName}/columns`, {
            params: { page: 1, size: 9999 },
          });

          if (columnsResponse.data?.success && columnsResponse.data?.data) {
            let columnData = columnsResponse.data.data;
            if (columnData.columns && Array.isArray(columnData.columns)) {
              columnData = columnData.columns;
            }

            if (Array.isArray(columnData) && columnData.length > 0) {
              // visible이 false가 아닌 컬럼만 + 시스템 컬럼 제외
              const filteredCols = columnData.filter((col: any) => {
                const colName = (col.column_name || col.columnName || "").toLowerCase();
                if (SYSTEM_COLUMNS.includes(colName)) return false;
                if (col.isVisible === false || col.is_visible === false) return false;
                return true;
              });

              visibleColumns = filteredCols.map((col: any) => col.column_name || col.columnName);
              columnLabels = {};
              filteredCols.forEach((col: any) => {
                const colName = col.column_name || col.columnName;
                const labelValue = col.column_label || col.label || col.displayName || colName;
                if (colName) {
                  columnLabels![colName] = labelValue;
                }
              });

              console.log(`✅ Fallback 컬럼 ${visibleColumns.length}개 로드 완료`);
            }
          }
        } catch (fallbackError) {
          console.error("❌ Fallback 컬럼 조회 실패:", fallbackError);
        }
      }

      // 최종 안전장치: 여전히 컬럼 정보가 없으면 데이터의 키에서 시스템 컬럼만 제외
      if ((!visibleColumns || visibleColumns.length === 0) && dataToExport.length > 0) {
        console.log("⚠️ 최종 fallback: 데이터 키에서 시스템 컬럼 제외");
        const allKeys = Object.keys(dataToExport[0]);
        visibleColumns = allKeys.filter((key) => {
          const lowerKey = key.toLowerCase();
          // 시스템 컬럼 제외
          if (SYSTEM_COLUMNS.includes(lowerKey)) return false;
          // _name, _label 등 조인된 보조 필드 제외
          if (lowerKey.endsWith("_name") || lowerKey.endsWith("_label") || lowerKey.endsWith("_value_label"))
            return false;
          return true;
        });
        // 라벨이 없으므로 최소한 column_labels 비워두지 않음 (컬럼명 그대로 표시되지만 시스템 컬럼은 제외됨)
        if (!columnLabels) {
          columnLabels = {};
        }
      }

      // 카테고리 값들 조회 (한 번만)
      const categoryMap: Record<string, Record<string, string>> = {};
      let categoryColumns: string[] = [];
      if (context.tableName) {
        try {
          const { getCategoryColumns, getCategoryValues } = await import("@/lib/api/tableCategoryValue");

          const categoryColumnsResponse = await getCategoryColumns(context.tableName);

          if (categoryColumnsResponse.success && categoryColumnsResponse.data) {
            // 백엔드에서 정의된 카테고리 컬럼들
            categoryColumns = categoryColumnsResponse.data
              .map((col: any) => col.column_name || col.columnName || col.name)
              .filter(Boolean); // undefined 제거

            // 각 카테고리 컬럼의 값들 조회
            for (const columnName of categoryColumns) {
              try {
                const valuesResponse = await getCategoryValues(context.tableName, columnName, false);

                if (valuesResponse.success && valuesResponse.data) {
                  // valueCode → valueLabel 매핑
                  categoryMap[columnName] = {};
                  valuesResponse.data.forEach((catValue: any) => {
                    const code = catValue.valueCode || catValue.category_value_id;
                    const label = catValue.valueLabel || catValue.label || code;
                    if (code) {
                      categoryMap[columnName][code] = label;
                    }
                  });
                }
              } catch (error) {
                console.error(`❌ 카테고리 "${columnName}" 조회 실패:`, error);
              }
            }
          }
        } catch (error) {
          console.error("❌ 카테고리 정보 조회 실패:", error);
        }
      }

      // 컬럼 필터링 및 라벨 적용
      if (visibleColumns && visibleColumns.length > 0 && dataToExport.length > 0) {
        dataToExport = dataToExport.map((row: any) => {
          const filteredRow: Record<string, any> = {};

          visibleColumns.forEach((columnName: string) => {
            // __checkbox__ 컬럼은 제외
            if (columnName === "__checkbox__") return;

            if (columnName in row) {
              // 라벨 우선 사용, 없으면 컬럼명 사용
              const label = columnLabels?.[columnName] || columnName;

              // 🎯 Entity 조인된 값 우선 사용
              let value = row[columnName];

              // writer → writer_name 사용
              if (columnName === "writer" && row["writer_name"]) {
                value = row["writer_name"];
              }
              // 다른 엔티티 필드들도 _name 우선 사용
              else if (row[`${columnName}_name`]) {
                value = row[`${columnName}_name`];
              }
              // 카테고리 타입 필드는 라벨로 변환 (백엔드에서 정의된 컬럼만)
              else if (categoryMap[columnName] && typeof value === "string") {
                // 쉼표로 구분된 다중 값 처리
                if (value.includes(",")) {
                  const values = value
                    .split(",")
                    .map((v) => v.trim())
                    .filter((v) => v);
                  const labels = values.map((v) => categoryMap[columnName][v] || v);
                  value = labels.join(", ");
                } else if (categoryMap[columnName][value]) {
                  value = categoryMap[columnName][value];
                }
              }

              filteredRow[label] = value;
            }
          });

          return filteredRow;
        });
      }

      // 최대 행 수 제한
      const MAX_ROWS = 10000;
      if (dataToExport.length > MAX_ROWS) {
        toast.warning(`최대 ${MAX_ROWS.toLocaleString()}개 행까지만 다운로드됩니다.`);
        dataToExport = dataToExport.slice(0, MAX_ROWS);
      }

      // 엑셀 다운로드 실행
      await exportToExcel(dataToExport, fileName, sheetName, includeHeaders);

      toast.success(`${dataToExport.length}개 행이 다운로드되었습니다.`);
      return true;
    } catch (error) {
      console.error("❌ 엑셀 다운로드 실패:", error);
      showErrorToast(config.errorMessage || "엑셀 파일 다운로드에 실패했습니다", error, {
        guidance: "데이터를 확인하고 다시 시도해 주세요.",
      });
      return false;
    }
  }

  /**
   * 엑셀 업로드 액션 처리
   */
  private static async handleExcelUpload(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    try {
      // 마스터-디테일 구조 확인 (화면에 분할 패널이 있으면 자동 감지)
      let isMasterDetail = false;
      let masterDetailRelation: any = null;
      let masterDetailExcelConfig: any = undefined;

      // 화면 레이아웃에서 분할 패널 자동 감지
      if (context.screenId) {
        const { DynamicFormApi } = await import("@/lib/api/dynamicForm");
        const relationResponse = await DynamicFormApi.getMasterDetailRelation(context.screenId);

        if (relationResponse.success && relationResponse.data) {
          isMasterDetail = true;
          masterDetailRelation = relationResponse.data;

          // 버튼 설정에서 채번 규칙 등 추가 설정 가져오기
          // 업로드 후 제어: excelAfterUploadFlows를 우선 사용 (통합된 설정)
          // masterDetailExcel.afterUploadFlows는 레거시 호환성을 위해 fallback으로만 사용
          const afterUploadFlows =
            config.excelAfterUploadFlows?.length > 0
              ? config.excelAfterUploadFlows
              : config.masterDetailExcel?.afterUploadFlows;

          // masterDetailExcel 설정이 명시적으로 있을 때만 간단 모드 (디테일만 업로드)
          // 설정이 없으면 기본 모드 (마스터+디테일 둘 다 업로드)
          if (config.masterDetailExcel) {
            masterDetailExcelConfig = {
              ...config.masterDetailExcel,
              // 분할 패널에서 감지한 테이블 정보로 덮어쓰기
              masterTable: relationResponse.data.masterTable,
              detailTable: relationResponse.data.detailTable,
              masterKeyColumn: relationResponse.data.masterKeyColumn,
              detailFkColumn: relationResponse.data.detailFkColumn,
              // 채번은 ExcelUploadModal에서 마스터 테이블 기반 자동 감지
              // 업로드 후 제어 설정 (통합: excelAfterUploadFlows 우선)
              afterUploadFlows,
            };
          }
          // masterDetailExcel 설정 없으면 masterDetailExcelConfig는 undefined 유지
          // → ExcelUploadModal에서 기본 모드로 동작 (마스터+디테일 둘 다 매핑/업로드)
        }
      }

      // 동적 import로 모달 컴포넌트 로드
      const { ExcelUploadModal } = await import("@/components/common/ExcelUploadModal");
      const { createRoot } = await import("react-dom/client");

      // 모달 컨테이너 생성
      const modalContainer = document.createElement("div");
      document.body.appendChild(modalContainer);

      const root = createRoot(modalContainer);

      const closeModal = () => {
        root.unmount();
        document.body.removeChild(modalContainer);
      };

      // localStorage 디버깅
      const modalId = `excel-upload-${context.tableName || ""}`;
      const storageKey = `modal_size_${modalId}_${context.userId || "guest"}`;

      root.render(
        React.createElement(ExcelUploadModal, {
          open: true,
          onOpenChange: (open: boolean) => {
            if (!open) {
              // 모달 닫을 때 localStorage 확인

              closeModal();
            }
          },
          tableName: context.tableName || "",
          uploadMode: config.excelUploadMode || "insert",
          keyColumn: config.excelKeyColumn,
          userId: context.userId,
          // 🆕 마스터-디테일 관련 props
          screenId: context.screenId,
          isMasterDetail,
          masterDetailRelation,
          masterDetailExcelConfig,
          // 채번은 ExcelUploadModal에서 테이블 타입 관리 기반 자동 감지
          // 🆕 업로드 후 제어 실행 설정
          afterUploadFlows: config.excelAfterUploadFlows,
          onSuccess: () => {
            // 성공 메시지는 ExcelUploadModal 내부에서 이미 표시됨
            context.onRefresh?.();
            closeModal();
          },
        }),
      );

      return true;
    } catch (error) {
      console.error("❌ 엑셀 업로드 모달 열기 실패:", error);
      showErrorToast(config.errorMessage || "엑셀 업로드 화면을 열 수 없습니다", error, {
        guidance: "잠시 후 다시 시도해 주세요.",
      });
      return false;
    }
  }

  /**
   * 다중 테이블 엑셀 업로드 액션 처리
   */
  private static async handleMultiTableExcelUpload(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    try {
      const rootTable = context.tableName;

      if (!rootTable) {
        toast.error("화면에 테이블이 설정되지 않았습니다. 화면 설정을 확인하세요.");
        return false;
      }

      toast.loading("테이블 구조를 분석하고 있습니다...", { id: "multi-table-detect" });

      const { autoDetectMultiTableConfig } = await import("@/lib/api/multiTableExcel");
      const result = await autoDetectMultiTableConfig(rootTable, context.screenId);

      toast.dismiss("multi-table-detect");

      if (!result.success || !result.data) {
        toast.error(result.message || `테이블 구조를 분석할 수 없습니다: ${rootTable}`);
        return false;
      }

      const chainConfig = result.data;

      const { MultiTableExcelUploadModal } = await import("@/components/common/MultiTableExcelUploadModal");
      const { createRoot } = await import("react-dom/client");

      const modalContainer = document.createElement("div");
      document.body.appendChild(modalContainer);

      const root = createRoot(modalContainer);

      const closeModal = () => {
        root.unmount();
        document.body.removeChild(modalContainer);
      };

      root.render(
        React.createElement(MultiTableExcelUploadModal, {
          open: true,
          onOpenChange: (open: boolean) => {
            if (!open) closeModal();
          },
          config: chainConfig,
          onSuccess: () => {
            context.onRefresh?.();
          },
        }),
      );

      return true;
    } catch (error) {
      toast.dismiss("multi-table-detect");
      console.error("다중 테이블 엑셀 업로드 모달 열기 실패:", error);
      showErrorToast(config.errorMessage || "다중 테이블 엑셀 업로드 화면을 열 수 없습니다", error, {
        guidance: "잠시 후 다시 시도해 주세요.",
      });
      return false;
    }
  }

  /**
   * 바코드 스캔 액션 처리
   */
  private static async handleBarcodeScan(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    try {
      console.log("📷 바코드 스캔 모달 열기:", { config, context });

      // 동적 import로 모달 컴포넌트 로드
      const { BarcodeScanModal } = await import("@/components/common/BarcodeScanModal");
      const { createRoot } = await import("react-dom/client");

      // 모달 컨테이너 생성
      const modalContainer = document.createElement("div");
      document.body.appendChild(modalContainer);

      const root = createRoot(modalContainer);

      const closeModal = () => {
        root.unmount();
        document.body.removeChild(modalContainer);
      };

      root.render(
        React.createElement(BarcodeScanModal, {
          open: true,
          onOpenChange: (open: boolean) => {
            if (!open) closeModal();
          },
          targetField: config.barcodeTargetField,
          barcodeFormat: config.barcodeFormat || "all",
          autoSubmit: config.barcodeAutoSubmit || false,
          userId: context.userId,
          onScanSuccess: (barcode: string) => {
            // 대상 필드에 값 입력
            if (config.barcodeTargetField && context.onFormDataChange) {
              context.onFormDataChange({
                ...context.formData,
                [config.barcodeTargetField]: barcode,
              });
            }

            toast.success(`바코드 스캔 완료: ${barcode}`);

            // 자동 제출 옵션이 켜져있으면 저장
            if (config.barcodeAutoSubmit) {
              this.handleSave(config, context);
            }

            closeModal();
          },
        }),
      );

      return true;
    } catch (error) {
      console.error("❌ 바코드 스캔 모달 열기 실패:", error);
      showErrorToast("바코드 스캔 화면을 열 수 없습니다", error, {
        guidance: "카메라 권한을 확인하고 다시 시도해 주세요.",
      });
      return false;
    }
  }

  /**
   * 코드 병합 액션 처리
   */
  private static async handleCodeMerge(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    try {
      console.log("🔀 코드 병합 액션 실행:", { config, context });

      // 선택된 행 데이터 확인
      const selectedRows = context.selectedRowsData || context.flowSelectedData;
      if (!selectedRows || selectedRows.length !== 2) {
        toast.error("병합할 두 개의 항목을 선택해주세요.");
        return false;
      }

      // 병합할 컬럼명 확인
      const columnName = config.mergeColumnName;
      if (!columnName) {
        toast.error("병합할 컬럼명이 설정되지 않았습니다.");
        return false;
      }

      // 두 개의 선택된 행에서 컬럼 값 추출
      const [row1, row2] = selectedRows;
      const value1 = row1[columnName];
      const value2 = row2[columnName];

      if (!value1 || !value2) {
        toast.error(`선택한 항목에 "${columnName}" 값이 없습니다.`);
        return false;
      }

      if (value1 === value2) {
        toast.error("같은 값은 병합할 수 없습니다.");
        return false;
      }

      // 병합 방향 선택 모달 표시
      const confirmed = await new Promise<{ confirmed: boolean; oldValue: string; newValue: string }>((resolve) => {
        const modalHtml = `
          <div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 9999;">
            <div style="background: white; padding: 24px; border-radius: 8px; max-width: 500px; width: 90%;">
              <h3 style="margin: 0 0 16px 0; font-size: 18px; font-weight: 600;">코드 병합 방향 선택</h3>
              <p style="margin: 0 0 24px 0; color: #666;">어느 코드로 병합하시겠습니까?</p>
              
              <div style="display: flex; gap: 12px; margin-bottom: 24px;">
                <button id="merge-option-1" style="flex: 1; padding: 16px; border: 2px solid #e5e7eb; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
                  <div style="font-weight: 600; margin-bottom: 4px;">${value1}</div>
                  <div style="font-size: 12px; color: #666;">← ${value2} 병합</div>
                </button>
                
                <button id="merge-option-2" style="flex: 1; padding: 16px; border: 2px solid #e5e7eb; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
                  <div style="font-weight: 600; margin-bottom: 4px;">${value2}</div>
                  <div style="font-size: 12px; color: #666;">← ${value1} 병합</div>
                </button>
              </div>
              
              <div style="display: flex; gap: 8px; justify-content: flex-end;">
                <button id="merge-cancel" style="padding: 8px 16px; border: 1px solid #e5e7eb; border-radius: 6px; background: white; cursor: pointer;">취소</button>
              </div>
            </div>
          </div>
        `;

        const modalContainer = document.createElement("div");
        modalContainer.innerHTML = modalHtml;
        document.body.appendChild(modalContainer);

        const option1Btn = modalContainer.querySelector("#merge-option-1") as HTMLButtonElement;
        const option2Btn = modalContainer.querySelector("#merge-option-2") as HTMLButtonElement;
        const cancelBtn = modalContainer.querySelector("#merge-cancel") as HTMLButtonElement;

        // 호버 효과
        [option1Btn, option2Btn].forEach((btn) => {
          btn.addEventListener("mouseenter", () => {
            btn.style.borderColor = "#3b82f6";
            btn.style.background = "#eff6ff";
          });
          btn.addEventListener("mouseleave", () => {
            btn.style.borderColor = "#e5e7eb";
            btn.style.background = "white";
          });
        });

        option1Btn.addEventListener("click", () => {
          document.body.removeChild(modalContainer);
          resolve({ confirmed: true, oldValue: value2, newValue: value1 });
        });

        option2Btn.addEventListener("click", () => {
          document.body.removeChild(modalContainer);
          resolve({ confirmed: true, oldValue: value1, newValue: value2 });
        });

        cancelBtn.addEventListener("click", () => {
          document.body.removeChild(modalContainer);
          resolve({ confirmed: false, oldValue: "", newValue: "" });
        });
      });

      if (!confirmed.confirmed) {
        return false;
      }

      const { oldValue, newValue } = confirmed;

      // 미리보기 표시 (값 기반 검색 - 모든 테이블의 모든 컬럼에서 검색)
      if (config.mergeShowPreview !== false) {
        const { apiClient } = await import("@/lib/api/client");

        toast.loading("영향받는 데이터 검색 중...", { duration: Infinity });

        const previewResponse = await apiClient.post("/code-merge/preview-by-value", {
          oldValue,
        });

        toast.dismiss();

        if (previewResponse.data.success) {
          const preview = previewResponse.data.data;
          const totalRows = preview.totalAffectedRows;

          // 상세 정보 생성
          const detailList = preview.preview
            .map((p: any) => `  - ${p.tableName}.${p.columnName}: ${p.affectedRows}건`)
            .join("\n");

          const confirmMerge = confirm(
            "코드 병합 확인\n\n" +
              `${oldValue} → ${newValue}\n\n` +
              "영향받는 데이터:\n" +
              `- 테이블/컬럼 수: ${preview.preview.length}개\n` +
              `- 총 행 수: ${totalRows}개\n\n` +
              (preview.preview.length <= 10 ? `상세:\n${detailList}\n\n` : "") +
              "모든 테이블에서 해당 값이 변경됩니다.\n\n" +
              "계속하시겠습니까?",
          );

          if (!confirmMerge) {
            return false;
          }
        }
      }

      // 병합 실행 (값 기반 - 모든 테이블의 모든 컬럼)
      toast.loading("코드 병합 중...", { duration: Infinity });

      const { apiClient } = await import("@/lib/api/client");

      const response = await apiClient.post("/code-merge/merge-by-value", {
        oldValue,
        newValue,
      });

      toast.dismiss();

      if (response.data.success) {
        const data = response.data.data;

        // 변경된 테이블/컬럼 목록 생성
        const changedList = data.affectedData
          .map((d: any) => `${d.tableName}.${d.columnName}: ${d.rowsUpdated}건`)
          .join(", ");

        toast.success(
          `코드 병합 완료! ${data.affectedData.length}개 테이블/컬럼, ${data.totalRowsUpdated}개 행 업데이트`,
        );

        console.log("코드 병합 결과:", data.affectedData);

        // 화면 새로고침
        context.onRefresh?.();
        context.onFlowRefresh?.();

        return true;
      } else {
        showErrorToast("코드 병합에 실패했습니다", response.data.message, {
          guidance: "병합 대상 코드를 확인해 주세요.",
        });
        return false;
      }
    } catch (error: any) {
      console.error("❌ 코드 병합 실패:", error);
      toast.dismiss();
      showErrorToast("코드 병합 중 오류가 발생했습니다", error.response?.data?.message || error);
      return false;
    }
  }

  // 🆕 연속 위치 추적 상태 저장 (전역)
  private static trackingIntervalId: NodeJS.Timeout | null = null;
  private static currentTripId: string | null = null;
  private static trackingContext: ButtonActionContext | null = null;
  private static trackingConfig: ButtonActionConfig | null = null;

  /**
   * 연속 위치 추적 시작
   */
  private static async handleTrackingStart(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    try {
      // 이미 추적 중인지 확인
      if (this.trackingIntervalId) {
        toast.warning("이미 위치 추적이 진행 중입니다.");
        return false;
      }

      // 위치 권한 확인
      if (!navigator.geolocation) {
        toast.error("이 브라우저는 위치 정보를 지원하지 않습니다.");
        return false;
      }

      // Trip ID 생성
      const tripId =
        config.trackingAutoGenerateTripId !== false
          ? `TRIP_${Date.now()}_${context.userId || "unknown"}`
          : context.formData?.[config.trackingTripIdField || "trip_id"] || `TRIP_${Date.now()}`;

      this.currentTripId = tripId;
      this.trackingContext = context;
      this.trackingConfig = config;

      // 출발지/도착지 정보
      const departure = context.formData?.[config.trackingDepartureField || "departure"] || null;
      const arrival = context.formData?.[config.trackingArrivalField || "arrival"] || null;
      const departureName = context.formData?.["departure_name"] || null;
      const destinationName = context.formData?.["destination_name"] || null;
      const vehicleId = context.formData?.[config.trackingVehicleIdField || "vehicle_id"] || null;

      console.log("📍 [handleTrackingStart] 운행 정보:", {
        tripId,
        departure,
        arrival,
        departureName,
        destinationName,
        vehicleId,
      });

      // 상태 변경 (vehicles 테이블 등)
      if (config.trackingStatusOnStart && config.trackingStatusField) {
        try {
          const { apiClient } = await import("@/lib/api/client");
          const statusTableName = config.trackingStatusTableName || context.tableName;
          const keyField = config.trackingStatusKeyField || "user_id";
          const keyValue = resolveSpecialKeyword(config.trackingStatusKeySourceField || "__userId__", context);

          if (keyValue) {
            // 상태 업데이트
            await apiClient.put("/dynamic-form/update-field", {
              tableName: statusTableName,
              keyField: keyField,
              keyValue: keyValue,
              updateField: config.trackingStatusField,
              updateValue: config.trackingStatusOnStart,
            });

            // 🆕 출발지/도착지도 vehicles 테이블에 저장
            if (departure) {
              try {
                await apiClient.put("/dynamic-form/update-field", {
                  tableName: statusTableName,
                  keyField: keyField,
                  keyValue: keyValue,
                  updateField: "departure",
                  updateValue: departure,
                });
              } catch {
                // 컬럼이 없으면 무시
              }
            }

            if (arrival) {
              try {
                await apiClient.put("/dynamic-form/update-field", {
                  tableName: statusTableName,
                  keyField: keyField,
                  keyValue: keyValue,
                  updateField: "arrival",
                  updateValue: arrival,
                });
              } catch {
                // 컬럼이 없으면 무시
              }
            }
          }
        } catch (statusError) {
          console.warn("⚠️ 상태 변경 실패:", statusError);
        }
      }

      // 첫 번째 위치 저장
      await this.saveLocationToHistory(tripId, departure, arrival, departureName, destinationName, vehicleId);

      // 주기적 위치 저장 시작
      const interval = config.trackingInterval || 10000; // 기본 10초
      this.trackingIntervalId = setInterval(async () => {
        await this.saveLocationToHistory(tripId, departure, arrival, departureName, destinationName, vehicleId);
      }, interval);

      toast.success(config.successMessage || `위치 추적이 시작되었습니다. (${interval / 1000}초 간격)`);

      // 추적 시작 이벤트 발생 (UI 업데이트용)
      window.dispatchEvent(
        new CustomEvent("trackingStarted", {
          detail: { tripId, interval },
        }),
      );

      return true;
    } catch (error: any) {
      console.error("❌ 위치 추적 시작 실패:", error);
      showErrorToast(config.errorMessage || "위치 추적을 시작할 수 없습니다", error, {
        guidance: "위치 권한 설정과 GPS 상태를 확인해 주세요.",
      });
      return false;
    }
  }

  /**
   * 연속 위치 추적 종료
   */
  private static async handleTrackingStop(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    try {
      console.log("🛑 [handleTrackingStop] 위치 추적 종료:", { config, context });

      // 추적 중인지 확인 (새로고침 후에도 DB 상태 기반 종료 가능하도록 수정)
      const isTrackingActive = !!this.trackingIntervalId;

      if (!isTrackingActive) {
        // 추적 중이 아니어도 DB 상태 변경은 진행 (새로고침 후 종료 지원)
        console.log("⚠️ [handleTrackingStop] trackingIntervalId 없음 - DB 상태 기반 종료 진행");
      } else {
        // 타이머 정리 (추적 중인 경우에만)
        clearInterval(this.trackingIntervalId);
        this.trackingIntervalId = null;
      }

      const tripId = this.currentTripId;

      // 🆕 DB에서 출발지/목적지 조회 (운전자가 중간에 바꿔도 원래 값 사용)
      let dbDeparture: string | null = null;
      let dbArrival: string | null = null;
      let dbVehicleId: string | null = null;

      const userId = context.userId || this.trackingUserId;
      if (userId) {
        try {
          const { apiClient } = await import("@/lib/api/client");
          const statusTableName =
            config.trackingStatusTableName ||
            this.trackingConfig?.trackingStatusTableName ||
            context.tableName ||
            "vehicles";
          const keyField = config.trackingStatusKeyField || this.trackingConfig?.trackingStatusKeyField || "user_id";

          // DB에서 현재 차량 정보 조회
          const vehicleResponse = await apiClient.post(`/table-management/tables/${statusTableName}/data`, {
            page: 1,
            size: 1,
            search: { [keyField]: userId },
            autoFilter: true,
          });

          const vehicleData = vehicleResponse.data?.data?.data?.[0] || vehicleResponse.data?.data?.rows?.[0];
          if (vehicleData) {
            dbDeparture = vehicleData.departure || null;
            dbArrival = vehicleData.arrival || null;
            dbVehicleId = vehicleData.id || vehicleData.vehicle_id || null;
            console.log("📍 [handleTrackingStop] DB에서 출발지/목적지 조회:", { dbDeparture, dbArrival, dbVehicleId });
          }
        } catch (dbError) {
          console.warn("⚠️ [handleTrackingStop] DB 조회 실패, formData 사용:", dbError);
        }
      }

      // 마지막 위치 저장 (추적 중이었던 경우에만)
      if (isTrackingActive) {
        // DB 값 우선, 없으면 formData 사용
        const departure =
          dbDeparture ||
          this.trackingContext?.formData?.[this.trackingConfig?.trackingDepartureField || "departure"] ||
          null;
        const arrival =
          dbArrival || this.trackingContext?.formData?.[this.trackingConfig?.trackingArrivalField || "arrival"] || null;
        const departureName = this.trackingContext?.formData?.["departure_name"] || null;
        const destinationName = this.trackingContext?.formData?.["destination_name"] || null;
        const vehicleId =
          dbVehicleId ||
          this.trackingContext?.formData?.[this.trackingConfig?.trackingVehicleIdField || "vehicle_id"] ||
          null;

        await this.saveLocationToHistory(
          tripId,
          departure,
          arrival,
          departureName,
          destinationName,
          vehicleId,
          "completed",
        );
      }

      // 🆕 거리/시간 계산 및 저장 (추적 중이었던 경우에만)
      if (isTrackingActive && tripId) {
        try {
          const tripStats = await this.calculateTripStats(tripId);

          // 운행 통계를 두 테이블에 저장
          if (tripStats) {
            const distanceMeters = Math.round(tripStats.totalDistanceKm * 1000); // km → m
            const timeMinutes = tripStats.totalTimeMinutes;
            const userId = this.trackingUserId || context.userId;

            const { apiClient } = await import("@/lib/api/client");

            // 1️⃣ vehicle_location_history 마지막 레코드에 통계 저장 (이력용)
            try {
              const lastRecordResponse = await apiClient.post(
                "/table-management/tables/vehicle_location_history/data",
                {
                  page: 1,
                  size: 1,
                  search: { trip_id: tripId },
                  sortBy: "recorded_at",
                  sortOrder: "desc",
                  autoFilter: true,
                },
              );

              const lastRecordData = lastRecordResponse.data?.data?.data || lastRecordResponse.data?.data?.rows || [];
              if (lastRecordData.length > 0) {
                const lastRecordId = lastRecordData[0].id;
                console.log("📍 마지막 레코드 ID:", lastRecordId);

                const historyUpdates = [
                  { field: "trip_distance", value: distanceMeters },
                  { field: "trip_time", value: timeMinutes },
                  { field: "trip_start", value: tripStats.startTime },
                  { field: "trip_end", value: tripStats.endTime },
                ];

                for (const update of historyUpdates) {
                  await apiClient.put("/dynamic-form/update-field", {
                    tableName: "vehicle_location_history",
                    keyField: "id",
                    keyValue: lastRecordId,
                    updateField: update.field,
                    updateValue: update.value,
                  });
                }
              } else {
                console.warn("⚠️ trip_id에 해당하는 레코드를 찾을 수 없음:", tripId);
              }
            } catch (historyError) {
              console.warn("⚠️ vehicle_location_history 저장 실패:", historyError);
            }

            // 2️⃣ vehicles 테이블에도 마지막 운행 통계 업데이트 (최신 정보용)
            if (userId) {
              try {
                const vehicleUpdates = [
                  { field: "last_trip_distance", value: distanceMeters },
                  { field: "last_trip_time", value: timeMinutes },
                  { field: "last_trip_start", value: tripStats.startTime },
                  { field: "last_trip_end", value: tripStats.endTime },
                ];

                for (const update of vehicleUpdates) {
                  await apiClient.put("/dynamic-form/update-field", {
                    tableName: "vehicles",
                    keyField: "user_id",
                    keyValue: userId,
                    updateField: update.field,
                    updateValue: update.value,
                  });
                }
              } catch (vehicleError) {
                console.warn("⚠️ vehicles 테이블 저장 실패:", vehicleError);
              }
            }

            // 이벤트로 통계 전달 (UI에서 표시용)
            window.dispatchEvent(
              new CustomEvent("tripCompleted", {
                detail: {
                  tripId,
                  totalDistanceKm: tripStats.totalDistanceKm,
                  totalTimeMinutes: tripStats.totalTimeMinutes,
                  startTime: tripStats.startTime,
                  endTime: tripStats.endTime,
                },
              }),
            );

            toast.success(
              `운행 종료! 총 ${tripStats.totalDistanceKm.toFixed(1)}km, ${tripStats.totalTimeMinutes}분 소요`,
            );
          }
        } catch (statsError) {
          console.warn("⚠️ 운행 통계 계산 실패:", statsError);
        }
      }

      // 상태 변경 (vehicles 테이블 등) - 새로고침 후에도 동작하도록 config 우선 사용
      const effectiveConfig = config.trackingStatusOnStop ? config : this.trackingConfig || config;
      const effectiveContext = context.userId ? context : this.trackingContext || context;

      if (effectiveConfig?.trackingStatusOnStop && effectiveConfig?.trackingStatusField && effectiveContext) {
        try {
          const { apiClient } = await import("@/lib/api/client");
          const statusTableName = effectiveConfig.trackingStatusTableName || effectiveContext.tableName;
          const keyField = effectiveConfig.trackingStatusKeyField || "user_id";
          const keyValue = resolveSpecialKeyword(
            effectiveConfig.trackingStatusKeySourceField || "__userId__",
            effectiveContext,
          );

          if (keyValue) {
            await apiClient.put("/dynamic-form/update-field", {
              tableName: statusTableName,
              keyField: keyField,
              keyValue: keyValue,
              updateField: effectiveConfig.trackingStatusField,
              updateValue: effectiveConfig.trackingStatusOnStop,
            });

            // 🆕 운행 종료 시 vehicles 테이블의 출발지/도착지/위도/경도를 null로 초기화
            const fieldsToReset = ["departure", "arrival", "latitude", "longitude"];
            for (const field of fieldsToReset) {
              try {
                await apiClient.put("/dynamic-form/update-field", {
                  tableName: statusTableName,
                  keyField: keyField,
                  keyValue: keyValue,
                  updateField: field,
                  updateValue: null,
                });
              } catch {
                // 컬럼이 없으면 무시
              }
            }
          }
        } catch (statusError) {
          console.warn("⚠️ 상태 변경 실패:", statusError);
        }
      }

      // 컨텍스트 정리
      this.currentTripId = null;
      this.trackingContext = null;
      this.trackingConfig = null;

      toast.success(config.successMessage || "위치 추적이 종료되었습니다.");

      // 추적 종료 이벤트 발생 (UI 업데이트용)
      window.dispatchEvent(
        new CustomEvent("trackingStopped", {
          detail: { tripId },
        }),
      );

      // 화면 새로고침
      context.onRefresh?.();

      return true;
    } catch (error: any) {
      console.error("❌ 위치 추적 종료 실패:", error);
      showErrorToast(config.errorMessage || "위치 추적 종료에 실패했습니다", error, {
        guidance: "잠시 후 다시 시도해 주세요.",
      });
      return false;
    }
  }

  /**
   * 운행 통계 계산 (거리, 시간)
   */
  private static async calculateTripStats(tripId: string): Promise<{
    totalDistanceKm: number;
    totalTimeMinutes: number;
    startTime: string | null;
    endTime: string | null;
  } | null> {
    try {
      // vehicle_location_history에서 해당 trip의 모든 위치 조회
      const { apiClient } = await import("@/lib/api/client");

      const response = await apiClient.post("/table-management/tables/vehicle_location_history/data", {
        page: 1,
        size: 10000,
        search: { trip_id: tripId },
        sortBy: "recorded_at",
        sortOrder: "asc",
      });

      if (!response.data?.success) {
        return null;
      }

      // 응답 형식: data.data.data 또는 data.data.rows
      const rows = response.data?.data?.data || response.data?.data?.rows || [];

      if (!rows.length) {
        return null;
      }

      const locations = rows;

      // 시간 계산
      const startTime = locations[0].recorded_at;
      const endTime = locations[locations.length - 1].recorded_at;
      const totalTimeMs = new Date(endTime).getTime() - new Date(startTime).getTime();
      const totalTimeMinutes = Math.round(totalTimeMs / 60000);

      // 거리 계산 (Haversine 공식)
      let totalDistanceM = 0;
      for (let i = 1; i < locations.length; i++) {
        const prev = locations[i - 1];
        const curr = locations[i];

        if (prev.latitude && prev.longitude && curr.latitude && curr.longitude) {
          const distance = this.calculateDistance(
            parseFloat(prev.latitude),
            parseFloat(prev.longitude),
            parseFloat(curr.latitude),
            parseFloat(curr.longitude),
          );
          totalDistanceM += distance;
        }
      }

      const totalDistanceKm = totalDistanceM / 1000;

      return {
        totalDistanceKm,
        totalTimeMinutes,
        startTime,
        endTime,
      };
    } catch (error) {
      console.error("❌ 운행 통계 계산 오류:", error);
      return null;
    }
  }

  /**
   * 두 좌표 간 거리 계산 (Haversine 공식, 미터 단위)
   */
  private static calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
    const R = 6371000; // 지구 반경 (미터)
    const dLat = ((lat2 - lat1) * Math.PI) / 180;
    const dLon = ((lon2 - lon1) * Math.PI) / 180;
    const a =
      Math.sin(dLat / 2) * Math.sin(dLat / 2) +
      Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return R * c;
  }

  /**
   * 위치 이력 테이블에 저장 (내부 헬퍼)
   * + vehicles 테이블의 latitude/longitude도 함께 업데이트
   */
  private static async saveLocationToHistory(
    tripId: string | null,
    departure: string | null,
    arrival: string | null,
    departureName: string | null,
    destinationName: string | null,
    vehicleId: number | null,
    tripStatus: string = "active",
  ): Promise<void> {
    return new Promise((resolve, reject) => {
      navigator.geolocation.getCurrentPosition(
        async (position) => {
          try {
            const { apiClient } = await import("@/lib/api/client");

            const { latitude, longitude, accuracy, altitude, speed, heading } = position.coords;

            const locationData = {
              latitude,
              longitude,
              accuracy,
              altitude,
              speed,
              heading,
              tripId,
              tripStatus,
              departure,
              arrival,
              departureName,
              destinationName,
              recordedAt: new Date(position.timestamp).toISOString(),
              vehicleId,
            };

            console.log("📍 [saveLocationToHistory] 위치 저장:", locationData);

            // 1. vehicle_location_history에 저장
            const response = await apiClient.post("/dynamic-form/location-history", locationData);

            if (response.data?.success) {
            } else {
              console.warn("⚠️ 위치 이력 저장 실패:", response.data);
            }

            // 2. vehicles 테이블의 latitude/longitude도 업데이트 (실시간 위치 반영)
            if (this.trackingContext && this.trackingConfig) {
              const keyField = this.trackingConfig.trackingStatusKeyField || "user_id";
              const keySourceField = this.trackingConfig.trackingStatusKeySourceField || "__userId__";
              const keyValue = resolveSpecialKeyword(keySourceField, this.trackingContext);
              const vehiclesTableName = this.trackingConfig.trackingStatusTableName || "vehicles";

              if (keyValue) {
                try {
                  // latitude 업데이트
                  await apiClient.put("/dynamic-form/update-field", {
                    tableName: vehiclesTableName,
                    keyField,
                    keyValue,
                    updateField: "latitude",
                    updateValue: latitude,
                  });

                  // longitude 업데이트
                  await apiClient.put("/dynamic-form/update-field", {
                    tableName: vehiclesTableName,
                    keyField,
                    keyValue,
                    updateField: "longitude",
                    updateValue: longitude,
                  });
                } catch (vehicleUpdateError) {
                  // 컬럼이 없으면 조용히 무시
                  console.warn("⚠️ vehicles 테이블 위치 업데이트 실패 (무시):", vehicleUpdateError);
                }
              }
            }

            resolve();
          } catch (error) {
            console.error("❌ 위치 이력 저장 오류:", error);
            reject(error);
          }
        },
        (error) => {
          console.error("❌ 위치 획득 실패:", error.message);
          reject(error);
        },
        {
          enableHighAccuracy: true,
          timeout: 10000,
          maximumAge: 0,
        },
      );
    });
  }

  /**
   * 현재 추적 상태 확인 (외부에서 호출 가능)
   */
  static isTracking(): boolean {
    return this.trackingIntervalId !== null;
  }

  /**
   * 현재 Trip ID 가져오기 (외부에서 호출 가능)
   */
  static getCurrentTripId(): string | null {
    return this.currentTripId;
  }

  /**
   * 데이터 전달 액션 처리 (분할 패널에서 좌측 → 우측 데이터 전달)
   */
  private static async handleTransferData(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    try {
      // 선택된 행 데이터 확인
      const selectedRows = context.selectedRowsData || context.flowSelectedData || [];

      if (!selectedRows || selectedRows.length === 0) {
        toast.error("전달할 데이터를 선택해주세요.");
        return false;
      }

      // dataTransfer 설정 확인
      const dataTransfer = config.dataTransfer;

      if (!dataTransfer) {
        // dataTransfer 설정이 없으면 기본 동작: 전역 이벤트로 데이터 전달

        const transferEvent = new CustomEvent("splitPanelDataTransfer", {
          detail: {
            data: selectedRows,
            mode: "append",
            sourcePosition: "left",
          },
        });
        window.dispatchEvent(transferEvent);

        toast.success(`${selectedRows.length}개 항목이 전달되었습니다.`);
        return true;
      }

      // dataTransfer 설정이 있는 경우
      const { targetType, targetComponentId, targetScreenId, receiveMode } = dataTransfer;

      // multiTableMappings 우선: 소스 테이블에 맞는 매핑 규칙 선택
      let mappingRules = dataTransfer.mappingRules;
      const multiTableMappings = (dataTransfer as any).multiTableMappings as Array<{
        sourceTable: string;
        mappingRules: Array<{ sourceField: string; targetField: string }>;
      }> | undefined;

      if (multiTableMappings && multiTableMappings.length > 0) {
        const sourceTableName = context.tableName || (dataTransfer as any).sourceTable;
        const matchedGroup = multiTableMappings.find((g) => g.sourceTable === sourceTableName);
        if (matchedGroup && matchedGroup.mappingRules?.length > 0) {
          mappingRules = matchedGroup.mappingRules;
          console.log("📋 [transferData] multiTableMappings 매핑 적용:", {
            sourceTable: sourceTableName,
            rules: matchedGroup.mappingRules,
          });
        } else if (!mappingRules || mappingRules.length === 0) {
          // 매칭되는 그룹이 없고 기존 mappingRules도 없으면 첫 번째 그룹 사용
          const fallback = multiTableMappings[0];
          if (fallback?.mappingRules?.length > 0) {
            mappingRules = fallback.mappingRules;
            console.log("📋 [transferData] multiTableMappings 폴백 매핑 적용:", {
              sourceTable: fallback.sourceTable,
              rules: fallback.mappingRules,
            });
          }
        }
      }

      if (targetType === "component" && targetComponentId) {
        // 같은 화면 내 컴포넌트로 전달 + 레이어 활성화 이벤트 병행
        const activateEvent = new CustomEvent("activateLayerForComponent", {
          detail: {
            componentId: targetComponentId,
            targetLayerId: (dataTransfer as any).targetLayerId,
          },
        });
        window.dispatchEvent(activateEvent);

        const transferEvent = new CustomEvent("componentDataTransfer", {
          detail: {
            targetComponentId,
            data: selectedRows,
            mappingRules,
            mode: receiveMode || "append",
          },
        });
        window.dispatchEvent(transferEvent);

        toast.success(`${selectedRows.length}개 항목이 전달되었습니다.`);
        return true;
      } else if (targetType === "screen" && targetScreenId) {
        // 다른 화면으로 전달 (분할 패널 등)

        const transferEvent = new CustomEvent("screenDataTransfer", {
          detail: {
            targetScreenId,
            data: selectedRows,
            mappingRules,
            mode: receiveMode || "append",
          },
        });
        window.dispatchEvent(transferEvent);

        toast.success(`${selectedRows.length}개 항목이 전달되었습니다.`);
        return true;
      } else {
        // 기본: 분할 패널 데이터 전달 이벤트
        console.log("📤 [transferData] splitPanelDataTransfer 발송:", {
          rowCount: selectedRows.length,
          mappingRules,
          sampleRow: selectedRows[0],
          hasItemId: selectedRows[0]?.item_id,
        });

        const transferEvent = new CustomEvent("splitPanelDataTransfer", {
          detail: {
            data: selectedRows,
            mappingRules,
            mode: receiveMode || "append",
            sourcePosition: "left",
          },
        });
        window.dispatchEvent(transferEvent);

        toast.success(`${selectedRows.length}개 항목이 전달되었습니다.`);
        return true;
      }
    } catch (error: any) {
      console.error("❌ 데이터 전달 실패:", error);
      showErrorToast("데이터 전달에 실패했습니다", error, {
        guidance: "대상 화면 설정과 데이터를 확인해 주세요.",
      });
      return false;
    }
  }

  // 공차 추적용 watchId 저장
  private static emptyVehicleWatchId: number | null = null;
  private static emptyVehicleTripId: string | null = null;

  /**
   * 공차등록 액션 처리
   * - 위치 수집 + 상태 변경 (예: status → inactive)
   * - 연속 위치 추적 시작 (vehicle_location_history에 저장)
   */
  private static async handleEmptyVehicle(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    try {
      console.log("📍 공차등록 액션 실행:", { config, context });

      // 브라우저 Geolocation API 지원 확인
      if (!navigator.geolocation) {
        toast.error("이 브라우저는 위치정보를 지원하지 않습니다.");
        return false;
      }

      // 위도/경도 저장 필드 확인
      const latField = config.geolocationLatField || "latitude";
      const lngField = config.geolocationLngField || "longitude";

      // 로딩 토스트 표시
      const loadingToastId = toast.loading("위치 정보를 가져오는 중...");

      // Geolocation 옵션 설정
      const options: PositionOptions = {
        enableHighAccuracy: config.geolocationHighAccuracy !== false,
        timeout: config.geolocationTimeout || 10000,
        maximumAge: config.geolocationMaxAge || 0,
      };

      // 위치 정보 가져오기
      const position = await new Promise<GeolocationPosition>((resolve, reject) => {
        navigator.geolocation.getCurrentPosition(resolve, reject, options);
      });

      toast.dismiss(loadingToastId);

      const { latitude, longitude, accuracy, speed, heading, altitude } = position.coords;
      const timestamp = new Date(position.timestamp);

      console.log("📍 위치정보 획득 성공:", { latitude, longitude, accuracy });

      // 폼 데이터 업데이트
      const updates: Record<string, any> = {
        [latField]: latitude,
        [lngField]: longitude,
      };

      if (config.geolocationAccuracyField && accuracy !== null) {
        updates[config.geolocationAccuracyField] = accuracy;
      }
      if (config.geolocationTimestampField) {
        updates[config.geolocationTimestampField] = timestamp.toISOString();
      }

      // onFormDataChange로 폼 업데이트
      if (context.onFormDataChange) {
        Object.entries(updates).forEach(([field, value]) => {
          context.onFormDataChange!(field, value);
        });
      }

      // 🆕 자동 저장 옵션이 활성화된 경우 DB UPDATE
      if (config.geolocationAutoSave) {
        const keyField = config.geolocationKeyField || "user_id";
        const keySourceField = config.geolocationKeySourceField || "__userId__";
        const keyValue = resolveSpecialKeyword(keySourceField, context);
        const targetTableName = config.geolocationTableName || context.tableName;

        if (keyValue && targetTableName) {
          try {
            const { apiClient } = await import("@/lib/api/client");

            // 위치 정보 필드들 업데이트 (위도, 경도, 정확도, 타임스탬프)
            const fieldsToUpdate = { ...updates };

            // formData에서 departure, arrival만 포함 (테이블에 있을 가능성 높은 필드만)
            if (context.formData?.departure) fieldsToUpdate.departure = context.formData.departure;
            if (context.formData?.arrival) fieldsToUpdate.arrival = context.formData.arrival;

            // 추가 필드 변경 (status 등)
            if (config.geolocationExtraField && config.geolocationExtraValue !== undefined) {
              fieldsToUpdate[config.geolocationExtraField] = config.geolocationExtraValue;
            }

            console.log("📍 DB UPDATE 시작:", { targetTableName, keyField, keyValue, fieldsToUpdate });

            // 각 필드를 개별적으로 UPDATE (에러 무시)
            let successCount = 0;
            for (const [field, value] of Object.entries(fieldsToUpdate)) {
              try {
                const response = await apiClient.put("/dynamic-form/update-field", {
                  tableName: targetTableName,
                  keyField,
                  keyValue,
                  updateField: field,
                  updateValue: value,
                });
                if (response.data?.success) {
                  successCount++;
                }
              } catch {
                // 컬럼이 없으면 조용히 무시 (에러 로그 안 찍음)
              }
            }
            console.log(`📍 DB UPDATE 완료: ${successCount}/${Object.keys(fieldsToUpdate).length} 필드 저장됨`);

            // 🆕 연속 위치 추적 시작 (공차 상태에서도 위치 기록)
            if (config.emptyVehicleTracking !== false) {
              await this.startEmptyVehicleTracking(config, context, {
                latitude,
                longitude,
                accuracy,
                speed,
                heading,
                altitude,
              });
            }

            toast.success(config.successMessage || "공차 등록이 완료되었습니다. 위치 추적을 시작합니다.");
          } catch (saveError) {
            console.error("❌ 위치정보 자동 저장 실패:", saveError);
            showErrorToast("위치 정보 저장에 실패했습니다", saveError, {
              guidance: "네트워크 연결을 확인해 주세요.",
            });
            return false;
          }
        } else {
          console.warn("⚠️ 키 값 또는 테이블명이 없어서 자동 저장을 건너뜁니다:", { keyValue, targetTableName });
          toast.success(config.successMessage || `위치: ${latitude.toFixed(6)}, ${longitude.toFixed(6)}`);
        }
      } else {
        // 자동 저장 없이 성공 메시지만
        toast.success(config.successMessage || `위치: ${latitude.toFixed(6)}, ${longitude.toFixed(6)}`);
      }

      return true;
    } catch (error: any) {
      console.error("❌ 공차등록 실패:", error);
      toast.dismiss();

      // GeolocationPositionError 처리
      if (error.code) {
        switch (error.code) {
          case 1: // PERMISSION_DENIED
            toast.error("위치 정보 접근이 거부되었습니다.\n브라우저 설정에서 위치 권한을 허용해주세요.");
            break;
          case 2: // POSITION_UNAVAILABLE
            toast.error("위치 정보를 사용할 수 없습니다.\nGPS 신호를 확인해주세요.");
            break;
          case 3: // TIMEOUT
            toast.error("위치 정보 요청 시간이 초과되었습니다.\n다시 시도해주세요.");
            break;
          default:
            showErrorToast(config.errorMessage || "위치 정보를 가져올 수 없습니다", null, {
              guidance: "브라우저 설정에서 위치 권한을 확인해 주세요.",
            });
        }
      } else {
        showErrorToast(config.errorMessage || "위치 정보를 가져올 수 없습니다", error, {
          guidance: "브라우저 설정에서 위치 권한을 확인해 주세요.",
        });
      }

      return false;
    }
  }

  /**
   * 공차 상태에서 연속 위치 추적 시작
   */
  private static async startEmptyVehicleTracking(
    config: ButtonActionConfig,
    context: ButtonActionContext,
    initialPosition: {
      latitude: number;
      longitude: number;
      accuracy: number | null;
      speed: number | null;
      heading: number | null;
      altitude: number | null;
    },
  ): Promise<void> {
    try {
      // 기존 추적이 있으면 중지
      if (this.emptyVehicleWatchId !== null) {
        navigator.geolocation.clearWatch(this.emptyVehicleWatchId);
        this.emptyVehicleWatchId = null;
      }

      const { apiClient } = await import("@/lib/api/client");

      // Trip ID 생성 (공차용)
      const tripId = `EMPTY-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`;
      this.emptyVehicleTripId = tripId;

      const userId = context.userId || "";
      const companyCode = context.companyCode || "";
      const departure = context.formData?.departure || "";
      const arrival = context.formData?.arrival || "";
      const departureName = context.formData?.departure_name || "";
      const destinationName = context.formData?.destination_name || "";

      // 시작 위치 기록
      try {
        await apiClient.post("/dynamic-form/location-history", {
          tripId,
          userId,
          latitude: initialPosition.latitude,
          longitude: initialPosition.longitude,
          accuracy: initialPosition.accuracy,
          speed: initialPosition.speed,
          heading: initialPosition.heading,
          altitude: initialPosition.altitude,
          tripStatus: "empty_start", // 공차 시작
          departure,
          arrival,
          departureName,
          destinationName,
          companyCode,
        });
        console.log("📍 공차 시작 위치 기록 완료:", tripId);
      } catch (err) {
        console.warn("⚠️ 공차 시작 위치 기록 실패 (테이블 없을 수 있음):", err);
      }

      // 추적 간격 (기본 10초)
      const trackingInterval = config.emptyVehicleTrackingInterval || 10000;

      // watchPosition으로 연속 추적
      this.emptyVehicleWatchId = navigator.geolocation.watchPosition(
        async (position) => {
          const { latitude, longitude, accuracy, speed, heading, altitude } = position.coords;

          try {
            await apiClient.post("/dynamic-form/location-history", {
              tripId: this.emptyVehicleTripId,
              userId,
              latitude,
              longitude,
              accuracy,
              speed,
              heading,
              altitude,
              tripStatus: "empty_tracking", // 공차 추적 중
              departure,
              arrival,
              departureName,
              destinationName,
              companyCode,
            });
            console.log("📍 공차 위치 기록:", { latitude: latitude.toFixed(6), longitude: longitude.toFixed(6) });
          } catch (err) {
            console.warn("⚠️ 공차 위치 기록 실패:", err);
          }
        },
        (error) => {
          console.error("❌ 공차 위치 추적 오류:", error.message);
        },
        {
          enableHighAccuracy: true,
          timeout: trackingInterval,
          maximumAge: 0,
        },
      );

      console.log("🚗 공차 위치 추적 시작:", { tripId, watchId: this.emptyVehicleWatchId });
    } catch (error) {
      console.error("❌ 공차 위치 추적 시작 실패:", error);
    }
  }

  /**
   * 공차 위치 추적 중지 (운행 전환 시 호출)
   */
  public static stopEmptyVehicleTracking(): void {
    if (this.emptyVehicleWatchId !== null) {
      navigator.geolocation.clearWatch(this.emptyVehicleWatchId);
      console.log("🛑 공차 위치 추적 중지:", { tripId: this.emptyVehicleTripId, watchId: this.emptyVehicleWatchId });
      this.emptyVehicleWatchId = null;
      this.emptyVehicleTripId = null;
    }
  }

  /**
   * 현재 공차 추적 Trip ID 반환
   */
  public static getEmptyVehicleTripId(): string | null {
    return this.emptyVehicleTripId;
  }

  /**
   * 필드 값 교환 액션 처리 (예: 출발지 ↔ 도착지)
   */
  private static async handleSwapFields(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    try {
      const { formData, onFormDataChange } = context;

      // 교환할 필드 확인
      const fieldA = config.swapFieldA;
      const fieldB = config.swapFieldB;

      if (!fieldA || !fieldB) {
        toast.error("교환할 필드가 설정되지 않았습니다.");
        return false;
      }

      // 현재 값 가져오기
      const valueA = formData?.[fieldA];
      const valueB = formData?.[fieldB];

      // 값 교환
      if (onFormDataChange) {
        onFormDataChange(fieldA, valueB);
        onFormDataChange(fieldB, valueA);
      }

      // 관련 필드도 함께 교환 (예: 위도/경도)
      if (config.swapRelatedFields && config.swapRelatedFields.length > 0) {
        for (const related of config.swapRelatedFields) {
          const relatedValueA = formData?.[related.fieldA];
          const relatedValueB = formData?.[related.fieldB];
          if (onFormDataChange) {
            onFormDataChange(related.fieldA, relatedValueB);
            onFormDataChange(related.fieldB, relatedValueA);
          }
        }
      }

      toast.success(config.successMessage || "값이 교환되었습니다.");
      return true;
    } catch (error) {
      console.error("❌ 필드 값 교환 오류:", error);
      showErrorToast(config.errorMessage || "필드 값 교환에 실패했습니다", error, {
        guidance: "교환 대상 필드 설정을 확인해 주세요.",
      });
      return false;
    }
  }

  /**
   * 즉시 저장 (Quick Insert) 액션 처리
   * 화면에서 선택한 데이터를 특정 테이블에 즉시 저장
   */
  private static async handleQuickInsert(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    try {
      const quickInsertConfig = config.quickInsertConfig;
      if (!quickInsertConfig?.targetTable) {
        toast.error("대상 테이블이 설정되지 않았습니다.");
        return false;
      }

      // ✅ allComponents가 있으면 기존 필수 항목 검증 수행
      if (context.allComponents && context.allComponents.length > 0) {
        const requiredValidation = this.validateRequiredFields(context);
        if (!requiredValidation.isValid) {
          console.log("❌ [handleQuickInsert] 필수 항목 누락:", requiredValidation.missingFields);
          toast.error(`필수 항목을 입력해주세요: ${requiredValidation.missingFields.join(", ")}`);
          return false;
        }
      }

      // ✅ quickInsert 전용 검증: component 타입 매핑에서 값이 비어있는지 확인
      const mappingsForValidation = quickInsertConfig.columnMappings || [];
      const missingMappingFields: string[] = [];

      for (const mapping of mappingsForValidation) {
        // component 타입 매핑은 필수 입력으로 간주
        if (mapping.sourceType === "component" && mapping.sourceComponentId) {
          let value: any = undefined;

          // 값 가져오기 (formData에서)
          if (mapping.sourceColumnName) {
            value = context.formData?.[mapping.sourceColumnName];
          }
          if (value === undefined || value === null) {
            value = context.formData?.[mapping.sourceComponentId];
          }
          // allComponents에서 컴포넌트 찾아서 columnName으로 시도
          if ((value === undefined || value === null) && context.allComponents) {
            const comp = context.allComponents.find((c: any) => c.id === mapping.sourceComponentId);
            if (comp?.columnName) {
              value = context.formData?.[comp.columnName];
            }
          }
          // targetColumn으로 폴백
          if ((value === undefined || value === null) && mapping.targetColumn) {
            value = context.formData?.[mapping.targetColumn];
          }

          // 값이 비어있으면 필수 누락으로 처리
          if (value === undefined || value === null || (typeof value === "string" && value.trim() === "")) {
            console.log("❌ [handleQuickInsert] component 매핑 값 누락:", {
              targetColumn: mapping.targetColumn,
              sourceComponentId: mapping.sourceComponentId,
              sourceColumnName: mapping.sourceColumnName,
              value,
            });
            missingMappingFields.push(mapping.targetColumn);
          }
        }
      }

      if (missingMappingFields.length > 0) {
        console.log("❌ [handleQuickInsert] 필수 입력 항목 누락:", missingMappingFields);
        toast.error(`다음 항목을 입력해주세요: ${missingMappingFields.join(", ")}`);
        return false;
      }

      const { formData, splitPanelContext, userId, userName, companyCode } = context;

      // 컬럼 매핑에 따라 저장할 데이터 구성
      const insertData: Record<string, any> = {};
      const columnMappings = quickInsertConfig.columnMappings || [];

      for (const mapping of columnMappings) {
        console.log("📍 매핑 처리 시작:", mapping);

        if (!mapping.targetColumn) {
          console.log("📍 targetColumn 없음, 스킵");
          continue;
        }

        let value: any = undefined;

        switch (mapping.sourceType) {
          case "component":
            console.log("📍 component 타입 처리:", {
              sourceComponentId: mapping.sourceComponentId,
              sourceColumnName: mapping.sourceColumnName,
              targetColumn: mapping.targetColumn,
            });

            // 컴포넌트의 현재 값
            if (mapping.sourceComponentId) {
              // 1. sourceColumnName이 있으면 직접 사용 (가장 확실한 방법)
              if (mapping.sourceColumnName) {
                value = formData?.[mapping.sourceColumnName];
                console.log(`📍 방법1 (sourceColumnName): ${mapping.sourceColumnName} = ${value}`);
              }

              // 2. 없으면 컴포넌트 ID로 직접 찾기
              if (value === undefined) {
                value = formData?.[mapping.sourceComponentId];
                console.log(`📍 방법2 (sourceComponentId): ${mapping.sourceComponentId} = ${value}`);
              }

              // 3. 없으면 allComponents에서 컴포넌트를 찾아 columnName으로 시도
              if (value === undefined && context.allComponents) {
                const comp = context.allComponents.find((c: any) => c.id === mapping.sourceComponentId);
                console.log("📍 방법3 찾은 컴포넌트:", comp);
                if (comp?.columnName) {
                  value = formData?.[comp.columnName];
                  console.log(`📍 방법3 (allComponents): ${mapping.sourceComponentId} → ${comp.columnName} = ${value}`);
                }
              }

              // 4. targetColumn과 같은 이름의 키가 formData에 있으면 사용 (폴백)
              if (value === undefined && mapping.targetColumn && formData?.[mapping.targetColumn] !== undefined) {
                value = formData[mapping.targetColumn];
                console.log(`📍 방법4 (targetColumn 폴백): ${mapping.targetColumn} = ${value}`);
              }

              // 5. 그래도 없으면 formData의 모든 키를 확인하고 로깅
              if (value === undefined) {
                console.log("📍 방법5: formData에서 값을 찾지 못함. formData 키들:", Object.keys(formData || {}));
              }

              // sourceColumn이 지정된 경우 해당 속성 추출
              if (mapping.sourceColumn && value && typeof value === "object") {
                value = value[mapping.sourceColumn];
                console.log(`📍 sourceColumn 추출: ${mapping.sourceColumn} = ${value}`);
              }
            }
            break;

          case "leftPanel":
            console.log("📍 leftPanel 타입 처리:", {
              sourceColumn: mapping.sourceColumn,
              selectedLeftData: splitPanelContext?.selectedLeftData,
            });
            // 좌측 패널 선택 데이터
            if (mapping.sourceColumn && splitPanelContext?.selectedLeftData) {
              value = splitPanelContext.selectedLeftData[mapping.sourceColumn];
              console.log(`📍 leftPanel 값: ${mapping.sourceColumn} = ${value}`);
            }
            break;

          case "fixed":
            console.log(`📍 fixed 타입 처리: fixedValue = ${mapping.fixedValue}`);
            // 고정값
            value = mapping.fixedValue;
            break;

          case "currentUser":
            console.log(`📍 currentUser 타입 처리: userField = ${mapping.userField}`);
            // 현재 사용자 정보
            switch (mapping.userField) {
              case "userId":
                value = userId;
                break;
              case "userName":
                value = userName;
                break;
              case "companyCode":
                value = companyCode;
                break;
            }
            console.log(`📍 currentUser 값: ${value}`);
            break;

          default:
            console.log(`📍 알 수 없는 sourceType: ${mapping.sourceType}`);
        }

        console.log(`📍 매핑 결과: targetColumn=${mapping.targetColumn}, value=${value}, type=${typeof value}`);

        if (value !== undefined && value !== null && value !== "") {
          insertData[mapping.targetColumn] = value;
          console.log(`📍 insertData에 추가됨: ${mapping.targetColumn} = ${value}`);
        } else {
          console.log("📍 값이 비어있어서 insertData에 추가 안됨");
        }
      }

      // 🆕 좌측 패널 선택 데이터에서 자동 매핑 (대상 테이블에 존재하는 컬럼만)
      if (splitPanelContext?.selectedLeftData) {
        const leftData = splitPanelContext.selectedLeftData;
        console.log("📍 좌측 패널 자동 매핑 시작:", leftData);

        // 대상 테이블의 컬럼 목록 조회
        let targetTableColumns: string[] = [];
        try {
          const columnsResponse = await apiClient.get(
            `/table-management/tables/${quickInsertConfig.targetTable}/columns`,
          );
          if (columnsResponse.data?.success && columnsResponse.data?.data) {
            const columnsData = columnsResponse.data.data.columns || columnsResponse.data.data;
            targetTableColumns = columnsData.map((col: any) => col.columnName || col.column_name || col.name);
            console.log("📍 대상 테이블 컬럼 목록:", targetTableColumns);
          }
        } catch (error) {
          console.error("대상 테이블 컬럼 조회 실패:", error);
        }

        for (const [key, val] of Object.entries(leftData)) {
          // 이미 매핑된 컬럼은 스킵
          if (insertData[key] !== undefined) {
            console.log(`📍 자동 매핑 스킵 (이미 존재): ${key}`);
            continue;
          }

          // 대상 테이블에 해당 컬럼이 없으면 스킵
          if (targetTableColumns.length > 0 && !targetTableColumns.includes(key)) {
            console.log(`📍 자동 매핑 스킵 (대상 테이블에 없는 컬럼): ${key}`);
            continue;
          }

          // 시스템 컬럼 제외 (id, created_date, updated_date, writer 등)
          const systemColumns = ["id", "created_date", "updated_date", "writer", "writer_name"];
          if (systemColumns.includes(key)) {
            console.log(`📍 자동 매핑 스킵 (시스템 컬럼): ${key}`);
            continue;
          }

          // _label, _name 으로 끝나는 표시용 컬럼 제외
          if (key.endsWith("_label") || key.endsWith("_name")) {
            console.log(`📍 자동 매핑 스킵 (표시용 컬럼): ${key}`);
            continue;
          }

          // 값이 있으면 자동 추가
          if (val !== undefined && val !== null && val !== "") {
            insertData[key] = val;
            console.log(`📍 자동 매핑 추가: ${key} = ${val}`);
          }
        }
      }

      // 필수 데이터 검증
      if (Object.keys(insertData).length === 0) {
        toast.error("저장할 데이터가 없습니다.");
        return false;
      }

      // 중복 체크
      console.log("📍 중복 체크 설정:", {
        enabled: quickInsertConfig.duplicateCheck?.enabled,
        columns: quickInsertConfig.duplicateCheck?.columns,
      });

      if (quickInsertConfig.duplicateCheck?.enabled && quickInsertConfig.duplicateCheck?.columns?.length > 0) {
        const duplicateCheckData: Record<string, any> = {};
        for (const col of quickInsertConfig.duplicateCheck.columns) {
          if (insertData[col] !== undefined) {
            // 백엔드가 { value, operator } 형식을 기대하므로 변환
            duplicateCheckData[col] = { value: insertData[col], operator: "equals" };
          }
        }

        console.log("📍 중복 체크 조건:", duplicateCheckData);

        if (Object.keys(duplicateCheckData).length > 0) {
          try {
            const checkResponse = await apiClient.post(
              `/table-management/tables/${quickInsertConfig.targetTable}/data`,
              {
                page: 1,
                pageSize: 1,
                search: duplicateCheckData,
              },
            );

            console.log("📍 중복 체크 응답:", checkResponse.data);

            // 응답 구조: { success: true, data: { data: [...], total: N } } 또는 { success: true, data: [...] }
            const existingData = checkResponse.data?.data?.data || checkResponse.data?.data || [];
            console.log(
              "📍 기존 데이터:",
              existingData,
              "길이:",
              Array.isArray(existingData) ? existingData.length : 0,
            );

            if (Array.isArray(existingData) && existingData.length > 0) {
              toast.error(quickInsertConfig.duplicateCheck.errorMessage || "이미 존재하는 데이터입니다.");
              return false;
            }
          } catch (error) {
            console.error("중복 체크 오류:", error);
            // 중복 체크 실패해도 저장은 시도
          }
        }
      } else {
        console.log("📍 중복 체크 비활성화 또는 컬럼 미설정");
      }

      // 데이터 저장
      const response = await apiClient.post(
        `/table-management/tables/${quickInsertConfig.targetTable}/add`,
        insertData,
      );

      if (response.data?.success) {
        // 저장 후 동작 설정 로그
        console.log("📍 afterInsert 설정:", quickInsertConfig.afterInsert);

        // 🆕 데이터 새로고침 (테이블리스트, 카드 디스플레이 컴포넌트 새로고침)
        // refreshData가 명시적으로 false가 아니면 기본적으로 새로고침 실행
        const shouldRefresh = quickInsertConfig.afterInsert?.refreshData !== false;
        console.log("📍 데이터 새로고침 여부:", shouldRefresh);

        if (shouldRefresh) {
          console.log("📍 데이터 새로고침 이벤트 발송");
          // 전역 이벤트로 테이블/카드 컴포넌트들에게 새로고침 알림
          if (typeof window !== "undefined") {
            window.dispatchEvent(new CustomEvent("refreshTable"));
            window.dispatchEvent(new CustomEvent("refreshCardDisplay"));
          }
        }

        // 컴포넌트 값 초기화
        if (quickInsertConfig.afterInsert?.clearComponents && context.onFormDataChange) {
          for (const mapping of columnMappings) {
            if (mapping.sourceType === "component" && mapping.sourceComponentId) {
              // sourceColumnName이 있으면 그것을 사용, 없으면 sourceComponentId 사용
              const fieldName = mapping.sourceColumnName || mapping.sourceComponentId;
              context.onFormDataChange(fieldName, null);
              console.log(`📍 컴포넌트 값 초기화: ${fieldName}`);
            }
          }
        }

        if (quickInsertConfig.afterInsert?.showSuccessMessage !== false) {
          toast.success(quickInsertConfig.afterInsert?.successMessage || "저장되었습니다.");
        }

        return true;
      } else {
        toast.error(response.data?.message || "저장에 실패했습니다.");
        return false;
      }
    } catch (error: any) {
      console.error("❌ Quick Insert 오류:", error);
      toast.error(error.response?.data?.message || "저장 중 오류가 발생했습니다.");
      return false;
    }
  }

  /**
   * 필드 값 변경 액션 처리 (예: status를 active로 변경)
   * 🆕 위치정보 수집 기능 추가
   * 🆕 연속 위치 추적 기능 추가
   */
  /**
   * 운행알림 및 종료 액션 처리
   * - 위치 수집 + 상태 변경 + 연속 추적 (시작/종료)
   */
  private static async handleOperationControl(
    config: ButtonActionConfig,
    context: ButtonActionContext,
  ): Promise<boolean> {
    try {
      // 🆕 출발지/도착지 필수 체크 (운행 시작 모드일 때만)
      // updateTrackingMode가 "start"이거나 updateTargetValue가 "active"/"inactive"인 경우
      const isStartMode =
        config.updateTrackingMode === "start" ||
        config.updateTargetValue === "active" ||
        config.updateTargetValue === "inactive";

      if (isStartMode) {
        // 출발지/도착지 필드명 (기본값: departure, destination)
        const departureField = config.trackingDepartureField || "departure";
        const destinationField = config.trackingArrivalField || "destination";

        const departure = context.formData?.[departureField];
        const destination = context.formData?.[destinationField];

        console.log("📍 출발지/도착지 체크:", { departureField, destinationField, departure, destination });

        if (!departure || departure === "" || !destination || destination === "") {
          toast.error("출발지와 도착지를 먼저 선택해주세요.");
          return false;
        }
      }

      // 🆕 공차 추적 중지 (운행 시작 시 공차 추적 종료)
      if (this.emptyVehicleWatchId !== null) {
        this.stopEmptyVehicleTracking();
        console.log("🛑 공차 추적 종료 후 운행 시작");
      }

      // 🆕 연속 위치 추적 모드 처리
      if (config.updateWithTracking) {
        const trackingConfig: ButtonActionConfig = {
          ...config,
          trackingInterval: config.updateTrackingInterval || config.trackingInterval || 10000,
          trackingStatusField: config.updateTargetField,
          trackingStatusTableName: config.updateTableName || context.tableName,
          trackingStatusKeyField: config.updateKeyField,
          trackingStatusKeySourceField: config.updateKeySourceField,
        };

        if (config.updateTrackingMode === "start") {
          trackingConfig.trackingStatusOnStart = config.updateTargetValue as string;
          return await this.handleTrackingStart(trackingConfig, context);
        } else if (config.updateTrackingMode === "stop") {
          trackingConfig.trackingStatusOnStop = config.updateTargetValue as string;
          return await this.handleTrackingStop(trackingConfig, context);
        }
      }

      const { formData, tableName, onFormDataChange, onSave } = context;

      // 변경할 필드 확인
      const targetField = config.updateTargetField;
      const targetValue = config.updateTargetValue;
      const multipleFields = config.updateMultipleFields || [];

      // 단일 필드 변경이나 다중 필드 변경 중 하나는 있어야 함
      if (!targetField && multipleFields.length === 0 && !config.updateWithGeolocation) {
        toast.error("변경할 필드가 설정되지 않았습니다.");
        return false;
      }

      // 확인 메시지 표시 (설정된 경우)
      if (config.confirmMessage) {
        const confirmed = window.confirm(config.confirmMessage);
        if (!confirmed) {
          return false;
        }
      }

      // 변경할 필드 목록 구성
      const updates: Record<string, any> = {};

      // 단일 필드 변경
      if (targetField && targetValue !== undefined) {
        updates[targetField] = targetValue;
      }

      // 다중 필드 변경
      multipleFields.forEach(({ field, value }) => {
        updates[field] = value;
      });

      // 🆕 위치정보 수집 (updateWithGeolocation이 true인 경우)
      if (config.updateWithGeolocation) {
        const latField = config.updateGeolocationLatField;
        const lngField = config.updateGeolocationLngField;

        if (!latField || !lngField) {
          toast.error("위도/경도 저장 필드가 설정되지 않았습니다.");
          return false;
        }

        // 브라우저 Geolocation API 지원 확인
        if (!navigator.geolocation) {
          toast.error("이 브라우저는 위치정보를 지원하지 않습니다.");
          return false;
        }

        // 로딩 토스트 표시
        const loadingToastId = toast.loading("위치 정보를 가져오는 중...");

        try {
          // 위치 정보 가져오기
          const position = await new Promise<GeolocationPosition>((resolve, reject) => {
            navigator.geolocation.getCurrentPosition(resolve, reject, {
              enableHighAccuracy: true,
              timeout: 10000,
              maximumAge: 0,
            });
          });

          toast.dismiss(loadingToastId);

          const { latitude, longitude, accuracy } = position.coords;
          const timestamp = new Date(position.timestamp);

          console.log("📍 위치정보 획득:", { latitude, longitude, accuracy });

          // 위치정보를 updates에 추가
          updates[latField] = latitude;
          updates[lngField] = longitude;

          if (config.updateGeolocationAccuracyField && accuracy !== null) {
            updates[config.updateGeolocationAccuracyField] = accuracy;
          }
          if (config.updateGeolocationTimestampField) {
            updates[config.updateGeolocationTimestampField] = timestamp.toISOString();
          }
        } catch (geoError: any) {
          toast.dismiss(loadingToastId);

          // GeolocationPositionError 처리
          if (geoError.code === 1) {
            toast.error("위치 정보 접근이 거부되었습니다.");
          } else if (geoError.code === 2) {
            toast.error("위치 정보를 사용할 수 없습니다.");
          } else if (geoError.code === 3) {
            toast.error("위치 정보 요청 시간이 초과되었습니다.");
          } else {
            toast.error("위치 정보를 가져오는 중 오류가 발생했습니다.");
          }
          return false;
        }
      }

      // formData 업데이트
      if (onFormDataChange) {
        Object.entries(updates).forEach(([field, value]) => {
          onFormDataChange(field, value);
        });
      }

      // 자동 저장 (기본값: true)
      const autoSave = config.updateAutoSave !== false;

      if (autoSave) {
        // 🆕 키 필드 설정이 있는 경우 (특수 키워드 지원) - 직접 DB UPDATE
        const keyField = config.updateKeyField;
        const keySourceField = config.updateKeySourceField;
        const targetTableName = config.updateTableName || tableName;

        if (keyField && keySourceField) {
          // 특수 키워드 변환 (예: __userId__ → 실제 사용자 ID)
          const keyValue = resolveSpecialKeyword(keySourceField, context);

          if (!keyValue) {
            console.warn("⚠️ 키 값이 없어서 업데이트를 건너뜁니다:", { keySourceField, keyValue });
            toast.error("레코드를 식별할 키 값이 없습니다.");
            return false;
          }

          try {
            // 각 필드에 대해 개별 UPDATE 호출
            const { apiClient } = await import("@/lib/api/client");

            for (const [field, value] of Object.entries(updates)) {
              const response = await apiClient.put("/dynamic-form/update-field", {
                tableName: targetTableName,
                keyField: keyField,
                keyValue: keyValue,
                updateField: field,
                updateValue: value,
              });

              if (!response.data?.success) {
                console.error(`❌ ${field} 업데이트 실패:`, response.data);
                toast.error(`${field} 업데이트에 실패했습니다.`);
                return false;
              }
            }

            toast.success(config.successMessage || "상태가 변경되었습니다.");

            // 테이블 새로고침 이벤트 발생
            window.dispatchEvent(
              new CustomEvent("refreshTableData", {
                detail: { tableName: targetTableName },
              }),
            );

            return true;
          } catch (apiError) {
            console.error("❌ 필드 값 변경 API 호출 실패:", apiError);
            toast.error(config.errorMessage || "상태 변경 중 오류가 발생했습니다.");
            return false;
          }
        }

        // onSave 콜백이 있으면 사용
        if (onSave) {
          try {
            await onSave();
            toast.success(config.successMessage || "상태가 변경되었습니다.");
            return true;
          } catch (saveError) {
            console.error("❌ 필드 값 변경 저장 실패:", saveError);
            toast.error(config.errorMessage || "상태 변경 저장에 실패했습니다.");
            return false;
          }
        }

        // API를 통한 직접 저장 (기존 방식: formData에 PK가 있는 경우)
        if (tableName && formData) {
          try {
            // PK 필드 찾기 (id 또는 테이블명_id)
            const pkField = formData.id !== undefined ? "id" : `${tableName}_id`;
            const pkValue = formData[pkField] || formData.id;

            if (!pkValue) {
              toast.error("레코드 ID를 찾을 수 없습니다. 키 필드를 설정해주세요.");
              return false;
            }

            // 업데이트할 데이터 구성 (변경할 필드들만)
            const updateData = {
              ...updates,
              [pkField]: pkValue, // PK 포함
            };

            const response = await DynamicFormApi.updateData(tableName, updateData);

            if (response.success) {
              toast.success(config.successMessage || "상태가 변경되었습니다.");

              // 테이블 새로고침 이벤트 발생
              window.dispatchEvent(
                new CustomEvent("refreshTableData", {
                  detail: { tableName },
                }),
              );

              return true;
            } else {
              toast.error(response.message || config.errorMessage || "상태 변경에 실패했습니다.");
              return false;
            }
          } catch (apiError) {
            console.error("❌ 필드 값 변경 API 호출 실패:", apiError);
            toast.error(config.errorMessage || "상태 변경 중 오류가 발생했습니다.");
            return false;
          }
        }
      }

      // 자동 저장이 비활성화된 경우 폼 데이터만 변경
      toast.success(config.successMessage || "필드 값이 변경되었습니다. 저장 버튼을 눌러 저장하세요.");
      return true;
    } catch (error) {
      console.error("❌ 필드 값 변경 실패:", error);
      toast.error(config.errorMessage || "필드 값 변경 중 오류가 발생했습니다.");
      return false;
    }
  }

  /**
   * 폼 데이터 유효성 검사
   */
  private static validateFormData(formData: Record<string, any>): {
    isValid: boolean;
    errors: string[];
  } {
    const errors: string[] = [];

    // 기본적인 유효성 검사 로직
    Object.entries(formData).forEach(([key, value]) => {
      // 빈 값 체크 (null, undefined, 빈 문자열)
      if (value === null || value === undefined || value === "") {
        // 필수 필드는 향후 컴포넌트 설정에서 확인 가능
        console.warn(`필드 '${key}'가 비어있습니다.`);
      }

      // 기본 타입 검증
      if (typeof value === "string" && value.trim() === "") {
        console.warn(`필드 '${key}'가 공백만 포함되어 있습니다.`);
      }
    });

    // 최소한 하나의 필드는 있어야 함
    if (Object.keys(formData).length === 0) {
      errors.push("저장할 데이터가 없습니다.");
    }

    return {
      isValid: errors.length === 0,
      errors,
    };
  }

  /**
   * 이벤트 버스로 이벤트 발송 (스케줄 생성 등)
   */
  private static async handleEvent(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    try {
      const { eventConfig } = config;

      if (!eventConfig?.eventName) {
        toast.error("이벤트 설정이 올바르지 않습니다.");
        console.error("[handleEvent] eventName이 설정되지 않음", { config });
        return false;
      }

      // V2_EVENTS에서 이벤트 이름 가져오기
      const { v2EventBus, V2_EVENTS } = await import("@/lib/v2-core");

      // 이벤트 이름 검증
      const eventName = eventConfig.eventName as keyof typeof V2_EVENTS;
      if (!V2_EVENTS[eventName]) {
        toast.error(`알 수 없는 이벤트: ${eventConfig.eventName}`);
        console.error("[handleEvent] 알 수 없는 이벤트", { eventName, V2_EVENTS });
        return false;
      }

      // 페이로드 구성
      const eventPayload = {
        requestId: crypto.randomUUID(),
        ...eventConfig.eventPayload,
      };

      console.log("[handleEvent] 이벤트 발송:", {
        eventName: V2_EVENTS[eventName],
        payload: eventPayload,
      });

      // 이벤트 발송
      v2EventBus.emit(V2_EVENTS[eventName], eventPayload);

      return true;
    } catch (error) {
      console.error("[handleEvent] 이벤트 발송 오류:", error);
      toast.error("이벤트 발송 중 오류가 발생했습니다.");
      return false;
    }
  }

  /**
   * 범용 API 호출 (생산계획 자동 스케줄 등)
   */
  private static async handleApiCall(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
    try {
      const { apiCallConfig } = config;

      if (!apiCallConfig?.endpoint) {
        toast.error("API 엔드포인트가 설정되지 않았습니다.");
        return false;
      }

      // 확인 메시지
      if (apiCallConfig.confirmMessage) {
        const confirmed = window.confirm(apiCallConfig.confirmMessage);
        if (!confirmed) return false;
      }

      // 페이로드 구성
      let payload: Record<string, any> = { ...(apiCallConfig.staticPayload || {}) };

      // formData에서 매핑
      if (apiCallConfig.payloadMapping && context.formData) {
        for (const [formField, apiField] of Object.entries(apiCallConfig.payloadMapping)) {
          if (context.formData[formField] !== undefined) {
            payload[apiField] = context.formData[formField];
          }
        }
      }

      // 선택된 행 데이터 포함
      if (apiCallConfig.useSelectedRows && context.selectedRowsData) {
        const key = apiCallConfig.selectedRowsKey || "items";
        payload[key] = context.selectedRowsData;
      }

      console.log("[handleApiCall] API 호출:", {
        method: apiCallConfig.method,
        endpoint: apiCallConfig.endpoint,
        payload,
      });

      // API 호출
      const { apiClient } = await import("@/lib/api/client");
      let response: any;

      switch (apiCallConfig.method) {
        case "GET":
          response = await apiClient.get(apiCallConfig.endpoint, { params: payload });
          break;
        case "POST":
          response = await apiClient.post(apiCallConfig.endpoint, payload);
          break;
        case "PUT":
          response = await apiClient.put(apiCallConfig.endpoint, payload);
          break;
        case "DELETE":
          response = await apiClient.delete(apiCallConfig.endpoint, { data: payload });
          break;
      }

      const result = response?.data;

      if (result?.success === false) {
        toast.error(result.message || "API 호출에 실패했습니다.");
        return false;
      }

      // 성공 메시지
      if (config.successMessage) {
        toast.success(config.successMessage);
      }

      // 테이블 새로고침
      if (apiCallConfig.refreshAfterSuccess !== false) {
        const { v2EventBus, V2_EVENTS } = await import("@/lib/v2-core");
        v2EventBus.emitSync(V2_EVENTS.TABLE_REFRESH, {
          tableName: context.tableName,
          target: "all",
        });
      }

      return true;
    } catch (error: any) {
      console.error("[handleApiCall] API 호출 오류:", error);
      const msg = error?.response?.data?.message || error?.message || "API 호출 중 오류가 발생했습니다.";
      toast.error(msg);
      return false;
    }
  }

  /**
   * 결재 요청 모달 열기
   */
  private static handleApproval(config: ButtonActionConfig, context: ButtonActionContext): boolean {
    try {
      const selectedRow = context.selectedRowsData?.[0] || context.formData || {};
      const targetTable = (config as any).approvalTargetTable || context.tableName || "";
      const recordIdField = (config as any).approvalRecordIdField || "id";
      const targetRecordId = selectedRow?.[recordIdField] || "";

      window.dispatchEvent(
        new CustomEvent("open-approval-modal", {
          detail: {
            targetTable,
            targetRecordId: targetRecordId ? String(targetRecordId) : "",
            targetRecordData: Object.keys(selectedRow).length > 0 ? selectedRow : undefined,
            definitionId: (config as any).approvalDefinitionId || undefined,
            screenId: context.screenId ? Number(context.screenId) : undefined,
            buttonComponentId: context.formData?.buttonId,
          },
        }),
      );

      return true;
    } catch (error) {
      console.error("[handleApproval] 결재 요청 오류:", error);
      toast.error("결재 요청 모달을 열 수 없습니다.");
      return false;
    }
  }
}

/**
 * 기본 버튼 액션 설정들
 */
export const DEFAULT_BUTTON_ACTIONS: Record<ButtonActionType, Partial<ButtonActionConfig>> = {
  save: {
    type: "save",
    validateForm: true,
    confirmMessage: "저장하시겠습니까?",
    successMessage: "저장되었습니다.",
    errorMessage: "저장 중 오류가 발생했습니다.",
  },
  delete: {
    type: "delete",
    confirmMessage: "정말 삭제하시겠습니까?",
    successMessage: "삭제되었습니다.",
    errorMessage: "삭제 중 오류가 발생했습니다.",
  },
  navigate: {
    type: "navigate",
  },
  openModalWithData: {
    // deprecated: modal로 통합 (하위 호환성 유지)
    type: "openModalWithData",
    modalSize: "md",
    passSelectedData: true,
    autoDetectDataSource: true,
    // 모달 열기는 UI 전환이므로 successMessage 제거
  },
  modal: {
    type: "modal",
    modalSize: "md",
    passSelectedData: true, // 기본: 선택된 데이터 전달
    autoDetectDataSource: false, // 기본: 자동 감지 비활성화
  },
  edit: {
    type: "edit",
    successMessage: "편집되었습니다.",
  },
  copy: {
    type: "copy",
    confirmMessage: "복사하시겠습니까?",
    successMessage: "복사되었습니다.",
    errorMessage: "복사 중 오류가 발생했습니다.",
  },
  control: {
    type: "control",
  },
  view_table_history: {
    type: "view_table_history",
    historyRecordIdField: "id",
    historyRecordIdSource: "selected_row",
  },
  excel_download: {
    type: "excel_download",
    excelIncludeHeaders: true,
    successMessage: "엑셀 파일이 다운로드되었습니다.",
    errorMessage: "엑셀 다운로드 중 오류가 발생했습니다.",
  },
  excel_upload: {
    type: "excel_upload",
    excelUploadMode: "insert",
    confirmMessage: "엑셀 파일을 업로드하시겠습니까?",
    successMessage: "엑셀 파일이 업로드되었습니다.",
    errorMessage: "엑셀 업로드 중 오류가 발생했습니다.",
  },
  multi_table_excel_upload: {
    type: "multi_table_excel_upload",
    successMessage: "다중 테이블 엑셀 업로드가 완료되었습니다.",
    errorMessage: "다중 테이블 엑셀 업로드 중 오류가 발생했습니다.",
  },
  barcode_scan: {
    type: "barcode_scan",
    barcodeFormat: "all",
    barcodeAutoSubmit: false,
  },
  code_merge: {
    type: "code_merge",
    mergeShowPreview: true,
    confirmMessage: "선택한 두 항목을 병합하시겠습니까?",
    successMessage: "코드 병합이 완료되었습니다.",
    errorMessage: "코드 병합 중 오류가 발생했습니다.",
  },
  transferData: {
    type: "transferData",
    successMessage: "데이터가 전달되었습니다.",
    errorMessage: "데이터 전달 중 오류가 발생했습니다.",
  },
  empty_vehicle: {
    type: "empty_vehicle",
    geolocationHighAccuracy: true,
    geolocationTimeout: 10000,
    geolocationMaxAge: 0,
    geolocationLatField: "latitude",
    geolocationLngField: "longitude",
    geolocationAutoSave: true,
    geolocationKeyField: "user_id",
    geolocationKeySourceField: "__userId__",
    geolocationExtraField: "status",
    geolocationExtraValue: "inactive",
    successMessage: "공차등록이 완료되었습니다.",
    errorMessage: "공차등록 중 오류가 발생했습니다.",
  },
  operation_control: {
    type: "operation_control",
    updateAutoSave: true,
    updateWithGeolocation: true,
    updateGeolocationLatField: "latitude",
    updateGeolocationLngField: "longitude",
    updateKeyField: "user_id",
    updateKeySourceField: "__userId__",
    confirmMessage: "운행 상태를 변경하시겠습니까?",
    successMessage: "운행 상태가 변경되었습니다.",
    errorMessage: "운행 상태 변경 중 오류가 발생했습니다.",
  },
  swap_fields: {
    type: "swap_fields",
    successMessage: "필드 값이 교환되었습니다.",
    errorMessage: "필드 값 교환 중 오류가 발생했습니다.",
  },
  quickInsert: {
    type: "quickInsert",
    successMessage: "저장되었습니다.",
    errorMessage: "저장 중 오류가 발생했습니다.",
  },
  event: {
    type: "event",
  },
  approval: {
    type: "approval",
  },
  apiCall: {
    type: "apiCall",
    successMessage: "처리되었습니다.",
  },
};
