2025-09-11 18:38:28 +09:00
|
|
|
"use client";
|
|
|
|
|
|
2025-10-23 18:23:01 +09:00
|
|
|
import React, { useState, useRef, useEffect, useMemo } from "react";
|
2025-09-11 18:38:28 +09:00
|
|
|
import { ComponentRendererProps } from "@/types/component";
|
|
|
|
|
import { ButtonPrimaryConfig } from "./types";
|
2025-09-12 14:24:25 +09:00
|
|
|
import {
|
|
|
|
|
ButtonActionExecutor,
|
|
|
|
|
ButtonActionContext,
|
|
|
|
|
ButtonActionType,
|
|
|
|
|
DEFAULT_BUTTON_ACTIONS,
|
|
|
|
|
} from "@/lib/utils/buttonActions";
|
|
|
|
|
import {
|
|
|
|
|
AlertDialog,
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
AlertDialogCancel,
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
AlertDialogTitle,
|
|
|
|
|
} from "@/components/ui/alert-dialog";
|
|
|
|
|
import { toast } from "sonner";
|
2025-09-19 02:15:21 +09:00
|
|
|
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
2025-10-23 18:23:01 +09:00
|
|
|
import { useCurrentFlowStep } from "@/stores/flowStepStore";
|
2025-10-28 15:39:22 +09:00
|
|
|
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
2025-11-27 12:54:57 +09:00
|
|
|
import { useScreenContextOptional } from "@/contexts/ScreenContext";
|
2025-11-28 14:56:11 +09:00
|
|
|
import { useSplitPanelContext, SplitPanelPosition } from "@/contexts/SplitPanelContext";
|
2025-11-27 12:54:57 +09:00
|
|
|
import { applyMappingRules } from "@/lib/utils/dataMapping";
|
2025-12-05 11:03:15 +09:00
|
|
|
import { apiClient } from "@/lib/api/client";
|
2025-09-11 18:38:28 +09:00
|
|
|
|
|
|
|
|
export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
|
|
|
|
config?: ButtonPrimaryConfig;
|
2025-09-12 14:24:25 +09:00
|
|
|
// 추가 props
|
|
|
|
|
screenId?: number;
|
|
|
|
|
tableName?: string;
|
2025-10-29 11:26:00 +09:00
|
|
|
userId?: string; // 🆕 현재 사용자 ID
|
|
|
|
|
userName?: string; // 🆕 현재 사용자 이름
|
|
|
|
|
companyCode?: string; // 🆕 현재 사용자의 회사 코드
|
2025-09-12 14:24:25 +09:00
|
|
|
onRefresh?: () => void;
|
|
|
|
|
onClose?: () => void;
|
2025-10-23 17:55:04 +09:00
|
|
|
onFlowRefresh?: () => void;
|
2025-11-25 12:07:14 +09:00
|
|
|
onSave?: () => Promise<void>; // 🆕 EditModal의 handleSave 콜백
|
2025-09-18 18:49:30 +09:00
|
|
|
|
|
|
|
|
// 폼 데이터 관련
|
|
|
|
|
originalData?: Record<string, any>; // 부분 업데이트용 원본 데이터
|
|
|
|
|
|
|
|
|
|
// 테이블 선택된 행 정보 (다중 선택 액션용)
|
|
|
|
|
selectedRows?: any[];
|
|
|
|
|
selectedRowsData?: any[];
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-04 18:31:26 +09:00
|
|
|
// 테이블 정렬 정보 (엑셀 다운로드용)
|
|
|
|
|
sortBy?: string;
|
|
|
|
|
sortOrder?: "asc" | "desc";
|
|
|
|
|
columnOrder?: string[];
|
2025-11-05 10:23:00 +09:00
|
|
|
tableDisplayData?: any[]; // 화면에 표시된 데이터 (컬럼 순서 포함)
|
2025-10-27 11:11:08 +09:00
|
|
|
|
2025-10-23 17:26:14 +09:00
|
|
|
// 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용)
|
|
|
|
|
flowSelectedData?: any[];
|
|
|
|
|
flowSelectedStepId?: number | null;
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-17 15:25:08 +09:00
|
|
|
// 🆕 같은 화면의 모든 컴포넌트 (TableList 자동 감지용)
|
|
|
|
|
allComponents?: any[];
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-12-09 14:17:45 +09:00
|
|
|
// 🆕 부모창에서 전달된 그룹 데이터 (모달에서 부모 데이터 접근용)
|
|
|
|
|
groupedData?: Record<string, any>[];
|
2025-09-11 18:38:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ButtonPrimary 컴포넌트
|
|
|
|
|
* button-primary 컴포넌트입니다
|
|
|
|
|
*/
|
|
|
|
|
export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|
|
|
|
component,
|
|
|
|
|
isDesignMode = false,
|
|
|
|
|
isSelected = false,
|
2025-09-12 14:24:25 +09:00
|
|
|
isInteractive = false,
|
2025-09-11 18:38:28 +09:00
|
|
|
onClick,
|
|
|
|
|
onDragStart,
|
|
|
|
|
onDragEnd,
|
|
|
|
|
config,
|
|
|
|
|
className,
|
|
|
|
|
style,
|
2025-09-12 14:24:25 +09:00
|
|
|
formData,
|
2025-09-18 18:49:30 +09:00
|
|
|
originalData,
|
2025-09-12 14:24:25 +09:00
|
|
|
onFormDataChange,
|
|
|
|
|
screenId,
|
|
|
|
|
tableName,
|
2025-10-29 11:26:00 +09:00
|
|
|
userId, // 🆕 사용자 ID
|
|
|
|
|
userName, // 🆕 사용자 이름
|
|
|
|
|
companyCode, // 🆕 회사 코드
|
2025-09-12 14:24:25 +09:00
|
|
|
onRefresh,
|
|
|
|
|
onClose,
|
2025-10-23 17:55:04 +09:00
|
|
|
onFlowRefresh,
|
2025-11-25 12:07:14 +09:00
|
|
|
onSave, // 🆕 EditModal의 handleSave 콜백
|
2025-11-04 18:31:26 +09:00
|
|
|
sortBy, // 🆕 정렬 컬럼
|
|
|
|
|
sortOrder, // 🆕 정렬 방향
|
|
|
|
|
columnOrder, // 🆕 컬럼 순서
|
2025-11-05 10:23:00 +09:00
|
|
|
tableDisplayData, // 🆕 화면에 표시된 데이터
|
2025-09-18 18:49:30 +09:00
|
|
|
selectedRows,
|
|
|
|
|
selectedRowsData,
|
2025-10-23 17:26:14 +09:00
|
|
|
flowSelectedData,
|
|
|
|
|
flowSelectedStepId,
|
2025-11-17 15:25:08 +09:00
|
|
|
allComponents, // 🆕 같은 화면의 모든 컴포넌트
|
2025-12-09 14:17:45 +09:00
|
|
|
groupedData, // 🆕 부모창에서 전달된 그룹 데이터
|
2025-09-11 18:38:28 +09:00
|
|
|
...props
|
|
|
|
|
}) => {
|
2025-10-28 15:39:22 +09:00
|
|
|
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
2025-11-27 12:54:57 +09:00
|
|
|
const screenContext = useScreenContextOptional(); // 화면 컨텍스트
|
2025-11-28 14:56:11 +09:00
|
|
|
const splitPanelContext = useSplitPanelContext(); // 분할 패널 컨텍스트
|
|
|
|
|
// 🆕 ScreenContext에서 splitPanelPosition 가져오기 (중첩 화면에서도 작동)
|
|
|
|
|
const splitPanelPosition = screenContext?.splitPanelPosition;
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
// 🆕 tableName이 props로 전달되지 않으면 ScreenContext에서 가져오기
|
|
|
|
|
const effectiveTableName = tableName || screenContext?.tableName;
|
|
|
|
|
const effectiveScreenId = screenId || screenContext?.screenId;
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-25 12:07:14 +09:00
|
|
|
// 🆕 props에서 onSave 추출 (명시적으로 선언되지 않은 경우 ...props에서 추출)
|
|
|
|
|
const propsOnSave = (props as any).onSave as (() => Promise<void>) | undefined;
|
|
|
|
|
const finalOnSave = onSave || propsOnSave;
|
2025-10-28 15:39:22 +09:00
|
|
|
|
2025-10-23 18:23:01 +09:00
|
|
|
// 🆕 플로우 단계별 표시 제어
|
|
|
|
|
const flowConfig = (component as any).webTypeConfig?.flowVisibilityConfig;
|
|
|
|
|
const currentStep = useCurrentFlowStep(flowConfig?.targetFlowComponentId);
|
|
|
|
|
|
|
|
|
|
// 🆕 버튼 표시 여부 계산
|
|
|
|
|
const shouldShowButton = useMemo(() => {
|
|
|
|
|
// 플로우 제어 비활성화 시 항상 표시
|
|
|
|
|
if (!flowConfig?.enabled) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 플로우 단계가 선택되지 않은 경우 처리
|
|
|
|
|
if (currentStep === null) {
|
|
|
|
|
// 🔧 화이트리스트 모드일 때는 단계 미선택 시 숨김
|
|
|
|
|
if (flowConfig.mode === "whitelist") {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
// 블랙리스트나 all 모드는 표시
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { mode, visibleSteps = [], hiddenSteps = [] } = flowConfig;
|
|
|
|
|
|
|
|
|
|
let result = true;
|
|
|
|
|
if (mode === "whitelist") {
|
|
|
|
|
result = visibleSteps.includes(currentStep);
|
|
|
|
|
} else if (mode === "blacklist") {
|
|
|
|
|
result = !hiddenSteps.includes(currentStep);
|
|
|
|
|
} else if (mode === "all") {
|
|
|
|
|
result = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}, [flowConfig, currentStep, component.id, component.label]);
|
|
|
|
|
|
2025-12-05 11:03:15 +09:00
|
|
|
// 🆕 운행알림 버튼 조건부 비활성화 (출발지/도착지 필수, 상태 체크)
|
|
|
|
|
// 상태는 API로 조회 (formData에 없는 경우)
|
|
|
|
|
const [vehicleStatus, setVehicleStatus] = useState<string | null>(null);
|
|
|
|
|
const [statusLoading, setStatusLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
// 상태 조회 (operation_control + enableOnStatusCheck일 때)
|
|
|
|
|
const actionConfig = component.componentConfig?.action;
|
|
|
|
|
const shouldFetchStatus = actionConfig?.type === "operation_control" && actionConfig?.enableOnStatusCheck && userId;
|
|
|
|
|
const statusTableName = actionConfig?.statusCheckTableName || "vehicles";
|
|
|
|
|
const statusKeyField = actionConfig?.statusCheckKeyField || "user_id";
|
|
|
|
|
const statusFieldName = actionConfig?.statusCheckField || "status";
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!shouldFetchStatus) return;
|
|
|
|
|
|
|
|
|
|
let isMounted = true;
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-12-05 11:03:15 +09:00
|
|
|
const fetchStatus = async () => {
|
|
|
|
|
if (!isMounted) return;
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-12-05 11:03:15 +09:00
|
|
|
try {
|
|
|
|
|
const response = await apiClient.post(`/table-management/tables/${statusTableName}/data`, {
|
|
|
|
|
page: 1,
|
|
|
|
|
size: 1,
|
|
|
|
|
search: { [statusKeyField]: userId },
|
|
|
|
|
autoFilter: true,
|
|
|
|
|
});
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-12-05 11:03:15 +09:00
|
|
|
if (!isMounted) return;
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-12-05 11:03:15 +09:00
|
|
|
const rows = response.data?.data?.data || response.data?.data?.rows || response.data?.rows || [];
|
|
|
|
|
const firstRow = Array.isArray(rows) ? rows[0] : null;
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-12-05 11:03:15 +09:00
|
|
|
if (response.data?.success && firstRow) {
|
|
|
|
|
const newStatus = firstRow[statusFieldName];
|
|
|
|
|
if (newStatus !== vehicleStatus) {
|
|
|
|
|
// console.log("🔄 [ButtonPrimary] 상태 변경 감지:", { 이전: vehicleStatus, 현재: newStatus, buttonLabel: component.label });
|
|
|
|
|
}
|
|
|
|
|
setVehicleStatus(newStatus);
|
|
|
|
|
} else {
|
|
|
|
|
setVehicleStatus(null);
|
|
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
// console.error("❌ [ButtonPrimary] 상태 조회 오류:", error?.message);
|
|
|
|
|
if (isMounted) setVehicleStatus(null);
|
|
|
|
|
} finally {
|
|
|
|
|
if (isMounted) setStatusLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 즉시 실행
|
|
|
|
|
setStatusLoading(true);
|
|
|
|
|
fetchStatus();
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-12-05 11:03:15 +09:00
|
|
|
// 2초마다 갱신
|
|
|
|
|
const interval = setInterval(fetchStatus, 2000);
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-12-05 11:03:15 +09:00
|
|
|
return () => {
|
|
|
|
|
isMounted = false;
|
|
|
|
|
clearInterval(interval);
|
|
|
|
|
};
|
|
|
|
|
}, [shouldFetchStatus, statusTableName, statusKeyField, statusFieldName, userId, component.label]);
|
|
|
|
|
|
|
|
|
|
// 버튼 비활성화 조건 계산
|
|
|
|
|
const isOperationButtonDisabled = useMemo(() => {
|
|
|
|
|
const actionConfig = component.componentConfig?.action;
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-12-05 11:03:15 +09:00
|
|
|
if (actionConfig?.type !== "operation_control") return false;
|
|
|
|
|
|
|
|
|
|
// 1. 출발지/도착지 필수 체크
|
|
|
|
|
if (actionConfig?.requireLocationFields) {
|
|
|
|
|
const departureField = actionConfig.trackingDepartureField || "departure";
|
|
|
|
|
const destinationField = actionConfig.trackingArrivalField || "destination";
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-12-05 11:03:15 +09:00
|
|
|
const departure = formData?.[departureField];
|
|
|
|
|
const destination = formData?.[destinationField];
|
2025-12-12 14:37:24 +09:00
|
|
|
|
|
|
|
|
// console.log("🔍 [ButtonPrimary] 출발지/도착지 체크:", {
|
|
|
|
|
// departureField, destinationField, departure, destination,
|
|
|
|
|
// buttonLabel: component.label
|
2025-12-05 11:03:15 +09:00
|
|
|
// });
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-12-05 11:03:15 +09:00
|
|
|
if (!departure || departure === "" || !destination || destination === "") {
|
|
|
|
|
// console.log("🚫 [ButtonPrimary] 출발지/도착지 미선택 → 비활성화:", component.label);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 상태 기반 활성화 조건 (API로 조회한 vehicleStatus 우선 사용)
|
|
|
|
|
if (actionConfig?.enableOnStatusCheck) {
|
|
|
|
|
const statusField = actionConfig.statusCheckField || "status";
|
|
|
|
|
// API 조회 결과를 우선 사용 (실시간 DB 상태 반영)
|
|
|
|
|
const currentStatus = vehicleStatus || formData?.[statusField];
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-12-05 11:03:15 +09:00
|
|
|
const conditionType = actionConfig.statusConditionType || "enableOn";
|
|
|
|
|
const conditionValues = (actionConfig.statusConditionValues || "")
|
|
|
|
|
.split(",")
|
|
|
|
|
.map((v: string) => v.trim())
|
|
|
|
|
.filter((v: string) => v);
|
|
|
|
|
|
2025-12-12 14:37:24 +09:00
|
|
|
// console.log("🔍 [ButtonPrimary] 상태 조건 체크:", {
|
2025-12-05 11:03:15 +09:00
|
|
|
// statusField,
|
|
|
|
|
// formDataStatus: formData?.[statusField],
|
|
|
|
|
// apiStatus: vehicleStatus,
|
2025-12-12 14:37:24 +09:00
|
|
|
// currentStatus,
|
|
|
|
|
// conditionType,
|
|
|
|
|
// conditionValues,
|
2025-12-05 11:03:15 +09:00
|
|
|
// buttonLabel: component.label,
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
// 상태 로딩 중이면 비활성화
|
|
|
|
|
if (statusLoading) {
|
|
|
|
|
// console.log("⏳ [ButtonPrimary] 상태 로딩 중 → 비활성화:", component.label);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 상태값이 없으면 → 비활성화 (조건 확인 불가)
|
|
|
|
|
if (!currentStatus) {
|
|
|
|
|
// console.log("🚫 [ButtonPrimary] 상태값 없음 → 비활성화:", component.label);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-12-05 11:03:15 +09:00
|
|
|
if (conditionValues.length > 0) {
|
|
|
|
|
if (conditionType === "enableOn") {
|
|
|
|
|
// 이 상태일 때만 활성화
|
|
|
|
|
if (!conditionValues.includes(currentStatus)) {
|
|
|
|
|
// console.log(`🚫 [ButtonPrimary] 상태 ${currentStatus} ∉ [${conditionValues}] → 비활성화:`, component.label);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
} else if (conditionType === "disableOn") {
|
|
|
|
|
// 이 상태일 때 비활성화
|
|
|
|
|
if (conditionValues.includes(currentStatus)) {
|
|
|
|
|
// console.log(`🚫 [ButtonPrimary] 상태 ${currentStatus} ∈ [${conditionValues}] → 비활성화:`, component.label);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// console.log("✅ [ButtonPrimary] 버튼 활성화:", component.label);
|
|
|
|
|
return false;
|
|
|
|
|
}, [component.componentConfig?.action, formData, vehicleStatus, statusLoading, component.label]);
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
// 확인 다이얼로그 상태
|
|
|
|
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
|
|
|
|
const [pendingAction, setPendingAction] = useState<{
|
|
|
|
|
type: ButtonActionType;
|
|
|
|
|
config: any;
|
|
|
|
|
context: ButtonActionContext;
|
|
|
|
|
} | null>(null);
|
2025-09-19 12:19:34 +09:00
|
|
|
|
|
|
|
|
// 토스트 정리를 위한 ref
|
2025-11-28 18:35:07 +09:00
|
|
|
const currentLoadingToastRef = useRef<string | number | undefined>(undefined);
|
2025-09-19 12:19:34 +09:00
|
|
|
|
|
|
|
|
// 컴포넌트 언마운트 시 토스트 정리
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
return () => {
|
|
|
|
|
if (currentLoadingToastRef.current !== undefined) {
|
|
|
|
|
toast.dismiss(currentLoadingToastRef.current);
|
|
|
|
|
currentLoadingToastRef.current = undefined;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-09-24 18:07:36 +09:00
|
|
|
// 삭제 액션 감지 로직 (실제 필드명 사용)
|
|
|
|
|
const isDeleteAction = () => {
|
2025-10-17 15:31:23 +09:00
|
|
|
const deleteKeywords = ["삭제", "delete", "remove", "제거", "del"];
|
2025-09-24 18:07:36 +09:00
|
|
|
return (
|
2025-10-17 15:31:23 +09:00
|
|
|
component.componentConfig?.action?.type === "delete" ||
|
|
|
|
|
component.config?.action?.type === "delete" ||
|
|
|
|
|
component.webTypeConfig?.actionType === "delete" ||
|
|
|
|
|
component.text?.toLowerCase().includes("삭제") ||
|
|
|
|
|
component.text?.toLowerCase().includes("delete") ||
|
|
|
|
|
component.label?.toLowerCase().includes("삭제") ||
|
|
|
|
|
component.label?.toLowerCase().includes("delete") ||
|
|
|
|
|
deleteKeywords.some(
|
|
|
|
|
(keyword) =>
|
|
|
|
|
component.config?.buttonText?.toLowerCase().includes(keyword) ||
|
|
|
|
|
component.config?.text?.toLowerCase().includes(keyword),
|
2025-09-24 18:07:36 +09:00
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 삭제 액션일 때 라벨 색상 자동 설정
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isDeleteAction() && !component.style?.labelColor) {
|
|
|
|
|
// 삭제 액션이고 라벨 색상이 설정되지 않은 경우 빨간색으로 자동 설정
|
|
|
|
|
if (component.style) {
|
2025-10-17 15:31:23 +09:00
|
|
|
component.style.labelColor = "#ef4444";
|
2025-09-24 18:07:36 +09:00
|
|
|
} else {
|
2025-10-17 15:31:23 +09:00
|
|
|
component.style = { labelColor: "#ef4444" };
|
2025-09-24 18:07:36 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [component.componentConfig?.action?.type, component.config?.action?.type, component.webTypeConfig?.actionType]);
|
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
// 컴포넌트 설정
|
2025-11-28 18:35:07 +09:00
|
|
|
// 🔥 component.componentConfig도 병합해야 함 (화면 디자이너에서 저장된 설정)
|
2025-09-11 18:38:28 +09:00
|
|
|
const componentConfig = {
|
|
|
|
|
...config,
|
|
|
|
|
...component.config,
|
2025-11-28 18:35:07 +09:00
|
|
|
...component.componentConfig, // 🔥 화면 디자이너에서 저장된 action 등 포함
|
2025-09-11 18:38:28 +09:00
|
|
|
} as ButtonPrimaryConfig;
|
|
|
|
|
|
2025-09-24 18:07:36 +09:00
|
|
|
// 🎨 동적 색상 설정 (속성편집 모달의 "색상" 필드와 연동)
|
|
|
|
|
const getLabelColor = () => {
|
|
|
|
|
if (isDeleteAction()) {
|
2025-10-17 15:31:23 +09:00
|
|
|
return component.style?.labelColor || "#ef4444"; // 빨간색 기본값 (Tailwind red-500)
|
2025-09-24 18:07:36 +09:00
|
|
|
}
|
2025-10-17 15:31:23 +09:00
|
|
|
return component.style?.labelColor || "#212121"; // 검은색 기본값 (shadcn/ui primary)
|
2025-09-24 18:07:36 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const buttonColor = getLabelColor();
|
2025-10-17 15:31:23 +09:00
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
// 액션 설정 처리 - DB에서 문자열로 저장된 액션을 객체로 변환
|
|
|
|
|
const processedConfig = { ...componentConfig };
|
|
|
|
|
if (componentConfig.action && typeof componentConfig.action === "string") {
|
|
|
|
|
const actionType = componentConfig.action as ButtonActionType;
|
|
|
|
|
processedConfig.action = {
|
|
|
|
|
...DEFAULT_BUTTON_ACTIONS[actionType],
|
|
|
|
|
type: actionType,
|
2025-09-19 12:19:34 +09:00
|
|
|
// 🔥 제어관리 설정 추가 (webTypeConfig에서 가져옴)
|
|
|
|
|
enableDataflowControl: component.webTypeConfig?.enableDataflowControl,
|
|
|
|
|
dataflowConfig: component.webTypeConfig?.dataflowConfig,
|
2025-12-15 14:46:32 +09:00
|
|
|
dataflowTiming: component.webTypeConfig?.dataflowTiming,
|
2025-09-19 12:19:34 +09:00
|
|
|
};
|
|
|
|
|
} else if (componentConfig.action && typeof componentConfig.action === "object") {
|
|
|
|
|
// 🔥 이미 객체인 경우에도 제어관리 설정 추가
|
|
|
|
|
processedConfig.action = {
|
|
|
|
|
...componentConfig.action,
|
|
|
|
|
enableDataflowControl: component.webTypeConfig?.enableDataflowControl,
|
|
|
|
|
dataflowConfig: component.webTypeConfig?.dataflowConfig,
|
2025-12-15 14:46:32 +09:00
|
|
|
dataflowTiming: component.webTypeConfig?.dataflowTiming,
|
2025-09-12 14:24:25 +09:00
|
|
|
};
|
|
|
|
|
}
|
2025-12-15 14:46:32 +09:00
|
|
|
|
2025-10-22 17:19:47 +09:00
|
|
|
// 스타일 계산
|
|
|
|
|
// height: 100%로 부모(RealtimePreviewDynamic의 내부 div)의 높이를 따라감
|
2025-11-28 18:35:07 +09:00
|
|
|
// width는 항상 100%로 고정 (부모 컨테이너가 gridColumns로 크기 제어)
|
2025-09-11 18:38:28 +09:00
|
|
|
const componentStyle: React.CSSProperties = {
|
|
|
|
|
...component.style,
|
|
|
|
|
...style,
|
2025-11-04 16:17:19 +09:00
|
|
|
width: "100%",
|
2025-11-28 18:35:07 +09:00
|
|
|
height: "100%",
|
2025-09-11 18:38:28 +09:00
|
|
|
};
|
|
|
|
|
|
2025-10-22 17:19:47 +09:00
|
|
|
// 디자인 모드 스타일 (border 속성 분리하여 충돌 방지)
|
2025-09-11 18:38:28 +09:00
|
|
|
if (isDesignMode) {
|
2025-10-22 17:19:47 +09:00
|
|
|
componentStyle.borderWidth = "1px";
|
|
|
|
|
componentStyle.borderStyle = "dashed";
|
2025-09-11 18:38:28 +09:00
|
|
|
componentStyle.borderColor = isSelected ? "#3b82f6" : "#cbd5e1";
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
// 확인 다이얼로그가 필요한 액션 타입들
|
2025-10-23 13:15:52 +09:00
|
|
|
const confirmationRequiredActions: ButtonActionType[] = ["save", "delete"];
|
2025-09-12 14:24:25 +09:00
|
|
|
|
|
|
|
|
// 실제 액션 실행 함수
|
|
|
|
|
const executeAction = async (actionConfig: any, context: ButtonActionContext) => {
|
|
|
|
|
try {
|
2025-09-19 12:19:34 +09:00
|
|
|
// 기존 토스트가 있다면 먼저 제거
|
|
|
|
|
if (currentLoadingToastRef.current !== undefined) {
|
|
|
|
|
toast.dismiss(currentLoadingToastRef.current);
|
|
|
|
|
currentLoadingToastRef.current = undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 추가 안전장치: 모든 로딩 토스트 제거
|
|
|
|
|
toast.dismiss();
|
|
|
|
|
|
2025-11-04 09:41:58 +09:00
|
|
|
// UI 전환 액션 및 모달 액션은 로딩 토스트 표시하지 않음
|
|
|
|
|
const silentActions = ["edit", "modal", "navigate", "excel_upload", "barcode_scan"];
|
2025-10-23 13:15:52 +09:00
|
|
|
if (!silentActions.includes(actionConfig.type)) {
|
2025-09-19 12:19:34 +09:00
|
|
|
currentLoadingToastRef.current = toast.loading(
|
2025-09-18 18:49:30 +09:00
|
|
|
actionConfig.type === "save"
|
|
|
|
|
? "저장 중..."
|
|
|
|
|
: actionConfig.type === "delete"
|
|
|
|
|
? "삭제 중..."
|
|
|
|
|
: actionConfig.type === "submit"
|
|
|
|
|
? "제출 중..."
|
|
|
|
|
: "처리 중...",
|
2025-09-19 12:19:34 +09:00
|
|
|
{
|
|
|
|
|
duration: Infinity, // 명시적으로 무한대로 설정
|
|
|
|
|
},
|
2025-09-18 18:49:30 +09:00
|
|
|
);
|
|
|
|
|
}
|
2025-09-12 14:24:25 +09:00
|
|
|
|
|
|
|
|
const success = await ButtonActionExecutor.executeAction(actionConfig, context);
|
|
|
|
|
|
2025-09-18 18:49:30 +09:00
|
|
|
// 로딩 토스트 제거 (있는 경우에만)
|
2025-09-19 12:19:34 +09:00
|
|
|
if (currentLoadingToastRef.current !== undefined) {
|
|
|
|
|
toast.dismiss(currentLoadingToastRef.current);
|
|
|
|
|
currentLoadingToastRef.current = undefined;
|
2025-09-18 18:49:30 +09:00
|
|
|
}
|
2025-09-12 14:24:25 +09:00
|
|
|
|
2025-10-17 15:31:23 +09:00
|
|
|
// 실패한 경우 오류 처리
|
|
|
|
|
if (!success) {
|
2025-11-04 09:41:58 +09:00
|
|
|
// UI 전환 액션 및 모달 액션은 에러도 조용히 처리 (모달 내부에서 자체 에러 표시)
|
|
|
|
|
const silentErrorActions = ["edit", "modal", "navigate", "excel_upload", "barcode_scan"];
|
|
|
|
|
if (silentErrorActions.includes(actionConfig.type)) {
|
2025-10-23 13:15:52 +09:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 기본 에러 메시지 결정
|
2025-10-27 11:11:08 +09:00
|
|
|
const defaultErrorMessage =
|
2025-10-23 13:15:52 +09:00
|
|
|
actionConfig.type === "save"
|
2025-10-17 15:31:23 +09:00
|
|
|
? "저장 중 오류가 발생했습니다."
|
|
|
|
|
: actionConfig.type === "delete"
|
|
|
|
|
? "삭제 중 오류가 발생했습니다."
|
|
|
|
|
: actionConfig.type === "submit"
|
|
|
|
|
? "제출 중 오류가 발생했습니다."
|
2025-10-23 13:15:52 +09:00
|
|
|
: "처리 중 오류가 발생했습니다.";
|
2025-10-27 11:11:08 +09:00
|
|
|
|
2025-10-23 13:15:52 +09:00
|
|
|
// 커스텀 메시지 사용 조건:
|
|
|
|
|
// 1. 커스텀 메시지가 있고
|
|
|
|
|
// 2. (액션 타입이 save이거나 OR 메시지에 "저장"이 포함되지 않은 경우)
|
2025-10-27 11:11:08 +09:00
|
|
|
const useCustomMessage =
|
|
|
|
|
actionConfig.errorMessage && (actionConfig.type === "save" || !actionConfig.errorMessage.includes("저장"));
|
|
|
|
|
|
2025-10-23 13:15:52 +09:00
|
|
|
const errorMessage = useCustomMessage ? actionConfig.errorMessage : defaultErrorMessage;
|
2025-10-27 11:11:08 +09:00
|
|
|
|
2025-10-17 15:31:23 +09:00
|
|
|
toast.error(errorMessage);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 성공한 경우에만 성공 토스트 표시
|
2025-11-04 09:41:58 +09:00
|
|
|
// edit, modal, navigate, excel_upload, barcode_scan 액션은 조용히 처리
|
|
|
|
|
// (UI 전환만 하거나 모달 내부에서 자체적으로 메시지 표시)
|
|
|
|
|
const silentSuccessActions = ["edit", "modal", "navigate", "excel_upload", "barcode_scan"];
|
|
|
|
|
if (!silentSuccessActions.includes(actionConfig.type)) {
|
2025-10-23 13:15:52 +09:00
|
|
|
// 기본 성공 메시지 결정
|
2025-10-27 11:11:08 +09:00
|
|
|
const defaultSuccessMessage =
|
2025-10-23 13:15:52 +09:00
|
|
|
actionConfig.type === "save"
|
2025-09-18 18:49:30 +09:00
|
|
|
? "저장되었습니다."
|
|
|
|
|
: actionConfig.type === "delete"
|
|
|
|
|
? "삭제되었습니다."
|
|
|
|
|
: actionConfig.type === "submit"
|
|
|
|
|
? "제출되었습니다."
|
2025-10-23 13:15:52 +09:00
|
|
|
: "완료되었습니다.";
|
2025-10-27 11:11:08 +09:00
|
|
|
|
2025-10-23 13:15:52 +09:00
|
|
|
// 커스텀 메시지 사용 조건:
|
|
|
|
|
// 1. 커스텀 메시지가 있고
|
|
|
|
|
// 2. (액션 타입이 save이거나 OR 메시지에 "저장"이 포함되지 않은 경우)
|
2025-10-27 11:11:08 +09:00
|
|
|
const useCustomMessage =
|
2025-10-23 13:15:52 +09:00
|
|
|
actionConfig.successMessage &&
|
|
|
|
|
(actionConfig.type === "save" || !actionConfig.successMessage.includes("저장"));
|
2025-10-27 11:11:08 +09:00
|
|
|
|
2025-10-23 13:15:52 +09:00
|
|
|
const successMessage = useCustomMessage ? actionConfig.successMessage : defaultSuccessMessage;
|
2025-09-12 14:24:25 +09:00
|
|
|
|
2025-09-18 18:49:30 +09:00
|
|
|
toast.success(successMessage);
|
|
|
|
|
}
|
2025-09-12 14:24:25 +09:00
|
|
|
|
2025-10-02 14:34:15 +09:00
|
|
|
// 저장/수정 성공 시 자동 처리
|
|
|
|
|
if (actionConfig.type === "save" || actionConfig.type === "edit") {
|
2025-10-17 15:31:23 +09:00
|
|
|
if (typeof window !== "undefined") {
|
2025-10-02 14:34:15 +09:00
|
|
|
// 1. 테이블 새로고침 이벤트 먼저 발송 (모달이 닫히기 전에)
|
2025-10-17 15:31:23 +09:00
|
|
|
window.dispatchEvent(new CustomEvent("refreshTable"));
|
|
|
|
|
|
2025-10-02 14:34:15 +09:00
|
|
|
// 2. 모달 닫기 (약간의 딜레이)
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
// EditModal 내부인지 확인 (isInModal prop 사용)
|
|
|
|
|
const isInEditModal = (props as any).isInModal;
|
2025-10-17 15:31:23 +09:00
|
|
|
|
2025-10-02 14:34:15 +09:00
|
|
|
if (isInEditModal) {
|
2025-10-17 15:31:23 +09:00
|
|
|
window.dispatchEvent(new CustomEvent("closeEditModal"));
|
2025-10-02 14:34:15 +09:00
|
|
|
}
|
2025-10-17 15:31:23 +09:00
|
|
|
|
2025-11-06 17:32:29 +09:00
|
|
|
// ScreenModal은 연속 등록 모드를 지원하므로 saveSuccessInModal 이벤트 발생
|
|
|
|
|
window.dispatchEvent(new CustomEvent("saveSuccessInModal"));
|
2025-10-02 14:34:15 +09:00
|
|
|
}, 100);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-12 14:24:25 +09:00
|
|
|
} catch (error) {
|
|
|
|
|
// 로딩 토스트 제거
|
2025-09-19 12:19:34 +09:00
|
|
|
if (currentLoadingToastRef.current !== undefined) {
|
|
|
|
|
toast.dismiss(currentLoadingToastRef.current);
|
|
|
|
|
currentLoadingToastRef.current = undefined;
|
2025-09-12 14:24:25 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.error("❌ 버튼 액션 실행 오류:", error);
|
|
|
|
|
|
2025-10-17 15:31:23 +09:00
|
|
|
// 오류 토스트는 buttonActions.ts에서 이미 표시되므로 여기서는 제거
|
|
|
|
|
// (중복 토스트 방지)
|
2025-09-12 14:24:25 +09:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
// 이벤트 핸들러
|
2025-11-27 12:54:57 +09:00
|
|
|
/**
|
|
|
|
|
* transferData 액션 처리
|
|
|
|
|
*/
|
|
|
|
|
const handleTransferDataAction = async (actionConfig: any) => {
|
|
|
|
|
const dataTransferConfig = actionConfig.dataTransfer;
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-27 12:54:57 +09:00
|
|
|
if (!dataTransferConfig) {
|
|
|
|
|
toast.error("데이터 전달 설정이 없습니다.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!screenContext) {
|
|
|
|
|
toast.error("화면 컨텍스트를 찾을 수 없습니다.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 1. 소스 컴포넌트에서 데이터 가져오기
|
2025-11-28 14:56:11 +09:00
|
|
|
let sourceProvider = screenContext.getDataProvider(dataTransferConfig.sourceComponentId);
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
// 🆕 소스 컴포넌트를 찾을 수 없으면, 현재 화면에서 테이블 리스트 자동 탐색
|
|
|
|
|
// (조건부 컨테이너의 다른 섹션으로 전환했을 때 이전 컴포넌트 ID가 남아있는 경우 대응)
|
2025-11-27 12:54:57 +09:00
|
|
|
if (!sourceProvider) {
|
2025-11-28 14:56:11 +09:00
|
|
|
console.log(`⚠️ [ButtonPrimary] 지정된 소스 컴포넌트를 찾을 수 없음: ${dataTransferConfig.sourceComponentId}`);
|
|
|
|
|
console.log(`🔍 [ButtonPrimary] 현재 화면에서 DataProvider 자동 탐색...`);
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
const allProviders = screenContext.getAllDataProviders();
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
// 테이블 리스트 우선 탐색
|
|
|
|
|
for (const [id, provider] of allProviders) {
|
|
|
|
|
if (provider.componentType === "table-list") {
|
|
|
|
|
sourceProvider = provider;
|
|
|
|
|
console.log(`✅ [ButtonPrimary] 테이블 리스트 자동 발견: ${id}`);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
// 테이블 리스트가 없으면 첫 번째 DataProvider 사용
|
|
|
|
|
if (!sourceProvider && allProviders.size > 0) {
|
|
|
|
|
const firstEntry = allProviders.entries().next().value;
|
|
|
|
|
if (firstEntry) {
|
|
|
|
|
sourceProvider = firstEntry[1];
|
2025-12-12 14:37:24 +09:00
|
|
|
console.log(
|
|
|
|
|
`✅ [ButtonPrimary] 첫 번째 DataProvider 사용: ${firstEntry[0]} (${sourceProvider.componentType})`,
|
|
|
|
|
);
|
2025-11-28 14:56:11 +09:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
if (!sourceProvider) {
|
|
|
|
|
toast.error("데이터를 제공할 수 있는 컴포넌트를 찾을 수 없습니다.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-11-27 12:54:57 +09:00
|
|
|
}
|
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
const rawSourceData = sourceProvider.getSelectedData();
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
// 🆕 배열이 아닌 경우 배열로 변환
|
2025-12-12 14:37:24 +09:00
|
|
|
const sourceData = Array.isArray(rawSourceData) ? rawSourceData : rawSourceData ? [rawSourceData] : [];
|
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
console.log("📦 소스 데이터:", { rawSourceData, sourceData, isArray: Array.isArray(rawSourceData) });
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-27 12:54:57 +09:00
|
|
|
if (!sourceData || sourceData.length === 0) {
|
|
|
|
|
toast.warning("선택된 데이터가 없습니다.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
// 1.5. 추가 데이터 소스 처리 (예: 조건부 컨테이너의 카테고리 값)
|
|
|
|
|
let additionalData: Record<string, any> = {};
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
// 방법 1: additionalSources 설정에서 가져오기
|
|
|
|
|
if (dataTransferConfig.additionalSources && Array.isArray(dataTransferConfig.additionalSources)) {
|
|
|
|
|
for (const additionalSource of dataTransferConfig.additionalSources) {
|
|
|
|
|
const additionalProvider = screenContext.getDataProvider(additionalSource.componentId);
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
if (additionalProvider) {
|
|
|
|
|
const additionalValues = additionalProvider.getSelectedData();
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
if (additionalValues && additionalValues.length > 0) {
|
|
|
|
|
// 첫 번째 값 사용 (조건부 컨테이너는 항상 1개)
|
|
|
|
|
const firstValue = additionalValues[0];
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
// fieldName이 지정되어 있으면 그 필드만 추출
|
|
|
|
|
if (additionalSource.fieldName) {
|
2025-12-12 14:37:24 +09:00
|
|
|
additionalData[additionalSource.fieldName] =
|
|
|
|
|
firstValue[additionalSource.fieldName] || firstValue.condition || firstValue;
|
2025-11-28 14:56:11 +09:00
|
|
|
} else {
|
|
|
|
|
// fieldName이 없으면 전체 객체 병합
|
|
|
|
|
additionalData = { ...additionalData, ...firstValue };
|
|
|
|
|
}
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
console.log("📦 추가 데이터 수집 (additionalSources):", {
|
|
|
|
|
sourceId: additionalSource.componentId,
|
|
|
|
|
fieldName: additionalSource.fieldName,
|
2025-12-12 14:37:24 +09:00
|
|
|
value: additionalData[additionalSource.fieldName || "all"],
|
2025-11-28 14:56:11 +09:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 방법 2: formData에서 조건부 컨테이너 값 가져오기 (자동)
|
|
|
|
|
// ConditionalSectionViewer가 __conditionalContainerValue, __conditionalContainerControlField를 formData에 포함시킴
|
|
|
|
|
if (formData && formData.__conditionalContainerValue) {
|
|
|
|
|
// includeConditionalValue 설정이 true이거나 설정이 없으면 자동 포함
|
|
|
|
|
if (dataTransferConfig.includeConditionalValue !== false) {
|
|
|
|
|
const conditionalValue = formData.__conditionalContainerValue;
|
|
|
|
|
const conditionalLabel = formData.__conditionalContainerLabel;
|
|
|
|
|
const controlField = formData.__conditionalContainerControlField; // 🆕 제어 필드명 직접 사용
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
// 🆕 controlField가 있으면 그것을 필드명으로 사용 (자동 매핑!)
|
|
|
|
|
if (controlField) {
|
|
|
|
|
additionalData[controlField] = conditionalValue;
|
|
|
|
|
console.log("📦 조건부 컨테이너 값 자동 매핑:", {
|
|
|
|
|
controlField,
|
|
|
|
|
value: conditionalValue,
|
|
|
|
|
label: conditionalLabel,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// controlField가 없으면 기존 방식: formData에서 같은 값을 가진 키 찾기
|
|
|
|
|
for (const [key, value] of Object.entries(formData)) {
|
2025-12-12 14:37:24 +09:00
|
|
|
if (value === conditionalValue && !key.startsWith("__")) {
|
2025-11-28 14:56:11 +09:00
|
|
|
additionalData[key] = conditionalValue;
|
|
|
|
|
console.log("📦 조건부 컨테이너 값 자동 포함:", {
|
|
|
|
|
fieldName: key,
|
|
|
|
|
value: conditionalValue,
|
|
|
|
|
label: conditionalLabel,
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
// 못 찾았으면 기본 필드명 사용
|
2025-12-12 14:37:24 +09:00
|
|
|
if (!Object.keys(additionalData).some((k) => !k.startsWith("__"))) {
|
|
|
|
|
additionalData["condition_type"] = conditionalValue;
|
2025-11-28 14:56:11 +09:00
|
|
|
console.log("📦 조건부 컨테이너 값 (기본 필드명):", {
|
2025-12-12 14:37:24 +09:00
|
|
|
fieldName: "condition_type",
|
2025-11-28 14:56:11 +09:00
|
|
|
value: conditionalValue,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 12:54:57 +09:00
|
|
|
// 2. 검증
|
|
|
|
|
const validation = dataTransferConfig.validation;
|
|
|
|
|
if (validation) {
|
|
|
|
|
if (validation.minSelection && sourceData.length < validation.minSelection) {
|
|
|
|
|
toast.error(`최소 ${validation.minSelection}개 이상 선택해야 합니다.`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (validation.maxSelection && sourceData.length > validation.maxSelection) {
|
|
|
|
|
toast.error(`최대 ${validation.maxSelection}개까지 선택할 수 있습니다.`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. 확인 메시지
|
|
|
|
|
if (dataTransferConfig.confirmBeforeTransfer) {
|
|
|
|
|
const confirmMessage = dataTransferConfig.confirmMessage || `${sourceData.length}개 항목을 전달하시겠습니까?`;
|
|
|
|
|
if (!window.confirm(confirmMessage)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
// 4. 매핑 규칙 적용 + 추가 데이터 병합
|
2025-11-27 12:54:57 +09:00
|
|
|
const mappedData = sourceData.map((row) => {
|
2025-11-28 14:56:11 +09:00
|
|
|
const mappedRow = applyMappingRules(row, dataTransferConfig.mappingRules || []);
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
// 추가 데이터를 모든 행에 포함
|
|
|
|
|
return {
|
|
|
|
|
...mappedRow,
|
|
|
|
|
...additionalData,
|
|
|
|
|
};
|
2025-11-27 12:54:57 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log("📦 데이터 전달:", {
|
|
|
|
|
sourceData,
|
|
|
|
|
mappedData,
|
|
|
|
|
targetType: dataTransferConfig.targetType,
|
|
|
|
|
targetComponentId: dataTransferConfig.targetComponentId,
|
|
|
|
|
targetScreenId: dataTransferConfig.targetScreenId,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 5. 타겟으로 데이터 전달
|
|
|
|
|
if (dataTransferConfig.targetType === "component") {
|
|
|
|
|
// 같은 화면의 컴포넌트로 전달
|
|
|
|
|
const targetReceiver = screenContext.getDataReceiver(dataTransferConfig.targetComponentId);
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-27 12:54:57 +09:00
|
|
|
if (!targetReceiver) {
|
|
|
|
|
toast.error(`타겟 컴포넌트를 찾을 수 없습니다: ${dataTransferConfig.targetComponentId}`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await targetReceiver.receiveData(mappedData, {
|
|
|
|
|
targetComponentId: dataTransferConfig.targetComponentId,
|
|
|
|
|
targetComponentType: targetReceiver.componentType,
|
|
|
|
|
mode: dataTransferConfig.mode || "append",
|
|
|
|
|
mappingRules: dataTransferConfig.mappingRules || [],
|
|
|
|
|
});
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
toast.success(`${sourceData.length}개 항목이 전달되었습니다.`);
|
|
|
|
|
} else if (dataTransferConfig.targetType === "splitPanel") {
|
|
|
|
|
// 🆕 분할 패널의 반대편 화면으로 전달
|
|
|
|
|
if (!splitPanelContext) {
|
|
|
|
|
toast.error("분할 패널 컨텍스트를 찾을 수 없습니다. 이 버튼이 분할 패널 내부에 있는지 확인하세요.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
// 🆕 useSplitPanelPosition 훅으로 위치 가져오기 (중첩된 화면에서도 작동)
|
2025-12-12 14:37:24 +09:00
|
|
|
// screenId로 찾는 것은 직접 임베드된 화면에서만 작동하므로,
|
2025-11-28 14:56:11 +09:00
|
|
|
// SplitPanelPositionProvider로 전달된 위치를 우선 사용
|
2025-12-12 14:37:24 +09:00
|
|
|
const currentPosition =
|
|
|
|
|
splitPanelPosition || (screenId ? splitPanelContext.getPositionByScreenId(screenId) : null);
|
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
if (!currentPosition) {
|
|
|
|
|
toast.error("분할 패널 내 위치를 확인할 수 없습니다. screenId: " + screenId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
console.log("📦 분할 패널 데이터 전달:", {
|
|
|
|
|
currentPosition,
|
|
|
|
|
splitPanelPositionFromHook: splitPanelPosition,
|
|
|
|
|
screenId,
|
|
|
|
|
leftScreenId: splitPanelContext.leftScreenId,
|
|
|
|
|
rightScreenId: splitPanelContext.rightScreenId,
|
|
|
|
|
});
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
const result = await splitPanelContext.transferToOtherSide(
|
|
|
|
|
currentPosition,
|
|
|
|
|
mappedData,
|
|
|
|
|
dataTransferConfig.targetComponentId, // 특정 컴포넌트 지정 (선택사항)
|
2025-12-12 14:37:24 +09:00
|
|
|
dataTransferConfig.mode || "append",
|
2025-11-28 14:56:11 +09:00
|
|
|
);
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
if (result.success) {
|
|
|
|
|
toast.success(result.message);
|
|
|
|
|
} else {
|
|
|
|
|
toast.error(result.message);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-11-27 12:54:57 +09:00
|
|
|
} else if (dataTransferConfig.targetType === "screen") {
|
|
|
|
|
// 다른 화면으로 전달 (구현 예정)
|
|
|
|
|
toast.info("다른 화면으로의 데이터 전달은 추후 구현 예정입니다.");
|
2025-11-28 14:56:11 +09:00
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
toast.success(`${sourceData.length}개 항목이 전달되었습니다.`);
|
2025-11-27 12:54:57 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 6. 전달 후 정리
|
|
|
|
|
if (dataTransferConfig.clearAfterTransfer) {
|
|
|
|
|
sourceProvider.clearSelection();
|
|
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
console.error("❌ 데이터 전달 실패:", error);
|
|
|
|
|
toast.error(error.message || "데이터 전달 중 오류가 발생했습니다.");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
const handleClick = async (e: React.MouseEvent) => {
|
2025-09-11 18:38:28 +09:00
|
|
|
e.stopPropagation();
|
2025-09-12 14:24:25 +09:00
|
|
|
|
2025-10-28 15:39:22 +09:00
|
|
|
// 프리뷰 모드에서는 버튼 동작 차단
|
|
|
|
|
if (isPreviewMode) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
// 디자인 모드에서는 기본 onClick만 실행
|
|
|
|
|
if (isDesignMode) {
|
|
|
|
|
onClick?.();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 인터랙티브 모드에서 액션 실행
|
|
|
|
|
if (isInteractive && processedConfig.action) {
|
2025-11-27 12:54:57 +09:00
|
|
|
// transferData 액션 처리 (화면 컨텍스트 필요)
|
|
|
|
|
if (processedConfig.action.type === "transferData") {
|
|
|
|
|
await handleTransferDataAction(processedConfig.action);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 14:17:45 +09:00
|
|
|
// 🆕 선택된 데이터 우선순위:
|
|
|
|
|
// 1. selectedRowsData (테이블에서 직접 선택)
|
|
|
|
|
// 2. groupedData (부모창에서 모달로 전달된 데이터)
|
|
|
|
|
// 3. modalDataStore (분할 패널 등에서 선택한 데이터)
|
2025-12-04 18:26:35 +09:00
|
|
|
let effectiveSelectedRowsData = selectedRowsData;
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-12-09 14:17:45 +09:00
|
|
|
// groupedData가 있으면 우선 사용 (모달에서 부모 데이터 접근)
|
2025-12-12 14:37:24 +09:00
|
|
|
if (
|
|
|
|
|
(!effectiveSelectedRowsData || effectiveSelectedRowsData.length === 0) &&
|
|
|
|
|
groupedData &&
|
|
|
|
|
groupedData.length > 0
|
|
|
|
|
) {
|
2025-12-09 14:17:45 +09:00
|
|
|
effectiveSelectedRowsData = groupedData;
|
|
|
|
|
}
|
2025-12-12 14:37:24 +09:00
|
|
|
|
2025-12-09 14:17:45 +09:00
|
|
|
// modalDataStore에서 선택된 데이터 가져오기 (분할 패널 등에서 선택한 데이터)
|
2025-12-30 14:03:29 +09:00
|
|
|
// 단, 모달(modal) 액션은 신규 등록이므로 modalDataStore 데이터를 가져오지 않음
|
|
|
|
|
// (다른 화면에서 선택한 데이터가 남아있을 수 있으므로)
|
|
|
|
|
const shouldFetchFromModalDataStore =
|
|
|
|
|
processedConfig.action.type !== "modal" &&
|
|
|
|
|
(!effectiveSelectedRowsData || effectiveSelectedRowsData.length === 0) &&
|
|
|
|
|
effectiveTableName;
|
|
|
|
|
|
|
|
|
|
if (shouldFetchFromModalDataStore) {
|
2025-12-04 18:26:35 +09:00
|
|
|
try {
|
|
|
|
|
const { useModalDataStore } = await import("@/stores/modalDataStore");
|
|
|
|
|
const dataRegistry = useModalDataStore.getState().dataRegistry;
|
|
|
|
|
const modalData = dataRegistry[effectiveTableName];
|
|
|
|
|
if (modalData && modalData.length > 0) {
|
2025-12-12 14:37:24 +09:00
|
|
|
// modalDataStore는 {id, originalData, additionalData} 형태로 저장됨
|
|
|
|
|
// originalData를 추출하여 실제 행 데이터를 가져옴
|
|
|
|
|
effectiveSelectedRowsData = modalData.map((item: any) => {
|
|
|
|
|
// originalData가 있으면 그것을 사용, 없으면 item 자체 사용 (하위 호환성)
|
|
|
|
|
return item.originalData || item;
|
|
|
|
|
});
|
2025-12-04 18:26:35 +09:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn("modalDataStore 접근 실패:", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-23 13:15:52 +09:00
|
|
|
// 삭제 액션인데 선택된 데이터가 없으면 경고 메시지 표시하고 중단
|
2025-10-23 17:55:04 +09:00
|
|
|
const hasDataToDelete =
|
2025-12-12 14:37:24 +09:00
|
|
|
(effectiveSelectedRowsData && effectiveSelectedRowsData.length > 0) ||
|
|
|
|
|
(flowSelectedData && flowSelectedData.length > 0);
|
2025-10-23 17:55:04 +09:00
|
|
|
|
|
|
|
|
if (processedConfig.action.type === "delete" && !hasDataToDelete) {
|
2025-10-23 13:15:52 +09:00
|
|
|
toast.warning("삭제할 항목을 먼저 선택해주세요.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-30 14:03:29 +09:00
|
|
|
// 모달 액션인데 현재 화면(테이블)에서 직접 선택된 데이터가 있으면 경고 메시지 표시하고 중단
|
2025-12-16 09:13:42 +09:00
|
|
|
// (신규 등록 모달에서 선택된 데이터가 초기값으로 전달되는 것을 방지)
|
2025-12-30 14:03:29 +09:00
|
|
|
// 주의: selectedRowsData만 체크 (modalDataStore의 데이터는 이미 제외했으므로)
|
|
|
|
|
if (processedConfig.action.type === "modal" && selectedRowsData && selectedRowsData.length > 0) {
|
2025-12-16 09:13:42 +09:00
|
|
|
toast.warning("신규 등록 시에는 테이블에서 선택된 항목을 해제해주세요.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 수정(edit) 액션 검증
|
|
|
|
|
if (processedConfig.action.type === "edit") {
|
|
|
|
|
// 선택된 데이터가 없으면 경고
|
|
|
|
|
if (!effectiveSelectedRowsData || effectiveSelectedRowsData.length === 0) {
|
|
|
|
|
toast.warning("수정할 항목을 선택해주세요.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// groupByColumns 설정이 있으면 해당 컬럼 값이 유일한지 확인
|
|
|
|
|
const groupByColumns = processedConfig.action.groupByColumns;
|
|
|
|
|
if (groupByColumns && groupByColumns.length > 0 && effectiveSelectedRowsData.length > 1) {
|
|
|
|
|
// 첫 번째 그룹핑 컬럼 기준으로 중복 체크 (예: order_no)
|
|
|
|
|
const groupByColumn = groupByColumns[0];
|
|
|
|
|
const uniqueValues = new Set(
|
|
|
|
|
effectiveSelectedRowsData.map((row: any) => row[groupByColumn]).filter(Boolean)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (uniqueValues.size > 1) {
|
|
|
|
|
// 컬럼명을 한글로 변환 (order_no -> 수주번호)
|
|
|
|
|
const columnLabels: Record<string, string> = {
|
|
|
|
|
order_no: "수주번호",
|
|
|
|
|
shipment_no: "출하번호",
|
|
|
|
|
purchase_no: "구매번호",
|
|
|
|
|
};
|
|
|
|
|
const columnLabel = columnLabels[groupByColumn] || groupByColumn;
|
|
|
|
|
toast.warning(`${columnLabel} 하나만 선택해주세요. (현재 ${uniqueValues.size}개 선택됨)`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 13:48:44 +09:00
|
|
|
// 🆕 모든 컴포넌트의 설정 수집 (parentDataMapping 등)
|
|
|
|
|
const componentConfigs: Record<string, any> = {};
|
|
|
|
|
if (allComponents && Array.isArray(allComponents)) {
|
|
|
|
|
for (const comp of allComponents) {
|
|
|
|
|
if (comp.id && comp.componentConfig) {
|
|
|
|
|
componentConfigs[comp.id] = comp.componentConfig;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 11:49:10 +09:00
|
|
|
// 분할 패널 부모 데이터 가져오기 (우측 화면에서 저장 시 좌측 선택 데이터 포함)
|
2025-12-02 18:03:52 +09:00
|
|
|
// 조건 완화: splitPanelContext가 있고 selectedLeftData가 있으면 가져옴
|
|
|
|
|
// (탭 안에서도 분할 패널 컨텍스트에 접근 가능하도록)
|
2025-12-01 18:39:01 +09:00
|
|
|
let splitPanelParentData: Record<string, any> | undefined;
|
2025-12-02 18:03:52 +09:00
|
|
|
if (splitPanelContext) {
|
|
|
|
|
// 우측 화면이거나, 탭 안의 화면(splitPanelPosition이 undefined)인 경우 모두 처리
|
|
|
|
|
// 좌측 화면이 아닌 경우에만 부모 데이터 포함 (좌측에서 저장 시 자신의 데이터를 부모로 포함하면 안됨)
|
|
|
|
|
if (splitPanelPosition !== "left") {
|
|
|
|
|
splitPanelParentData = splitPanelContext.getMappedParentData();
|
2025-12-01 18:39:01 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-15 17:47:16 +09:00
|
|
|
// 🆕 분할 패널 우측이면 여러 소스에서 formData를 병합
|
|
|
|
|
// 우선순위: props.formData > screenContext.formData > splitPanelParentData
|
2025-12-15 15:40:29 +09:00
|
|
|
const screenContextFormData = screenContext?.formData || {};
|
|
|
|
|
const propsFormData = formData || {};
|
|
|
|
|
|
2025-12-15 17:47:16 +09:00
|
|
|
// 병합: splitPanelParentData를 기본으로, props.formData, screenContext.formData 순으로 오버라이드
|
|
|
|
|
// (일반 폼 필드는 props.formData, RepeaterFieldGroup은 screenContext.formData에 있음)
|
|
|
|
|
let effectiveFormData = { ...propsFormData, ...screenContextFormData };
|
|
|
|
|
|
2025-12-16 11:49:10 +09:00
|
|
|
// 분할 패널 우측이고 formData가 비어있으면 splitPanelParentData 사용
|
2025-12-15 17:47:16 +09:00
|
|
|
if (splitPanelPosition === "right" && Object.keys(effectiveFormData).length === 0 && splitPanelParentData) {
|
|
|
|
|
effectiveFormData = { ...splitPanelParentData };
|
|
|
|
|
}
|
2025-12-15 15:40:29 +09:00
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
const context: ButtonActionContext = {
|
2025-12-15 15:40:29 +09:00
|
|
|
formData: effectiveFormData,
|
2025-12-01 15:21:03 +09:00
|
|
|
originalData: originalData, // 🔧 빈 객체 대신 undefined 유지 (UPDATE 판단에 사용)
|
2025-11-28 18:35:34 +09:00
|
|
|
screenId: effectiveScreenId, // 🆕 ScreenContext에서 가져온 값 사용
|
|
|
|
|
tableName: effectiveTableName, // 🆕 ScreenContext에서 가져온 값 사용
|
2025-10-29 11:26:00 +09:00
|
|
|
userId, // 🆕 사용자 ID
|
|
|
|
|
userName, // 🆕 사용자 이름
|
|
|
|
|
companyCode, // 🆕 회사 코드
|
2025-09-12 14:24:25 +09:00
|
|
|
onFormDataChange,
|
|
|
|
|
onRefresh,
|
|
|
|
|
onClose,
|
2025-10-23 17:55:04 +09:00
|
|
|
onFlowRefresh, // 플로우 새로고침 콜백 추가
|
2025-11-25 12:07:14 +09:00
|
|
|
onSave: finalOnSave, // 🆕 EditModal의 handleSave 콜백 (props에서도 추출)
|
2025-12-04 18:26:35 +09:00
|
|
|
// 테이블 선택된 행 정보 추가 (modalDataStore에서 가져온 데이터 우선)
|
2025-09-18 18:49:30 +09:00
|
|
|
selectedRows,
|
2025-12-04 18:26:35 +09:00
|
|
|
selectedRowsData: effectiveSelectedRowsData,
|
2025-11-04 18:31:26 +09:00
|
|
|
// 테이블 정렬 정보 추가
|
|
|
|
|
sortBy, // 🆕 정렬 컬럼
|
|
|
|
|
sortOrder, // 🆕 정렬 방향
|
|
|
|
|
columnOrder, // 🆕 컬럼 순서
|
2025-11-05 10:23:00 +09:00
|
|
|
tableDisplayData, // 🆕 화면에 표시된 데이터
|
2025-11-17 15:25:08 +09:00
|
|
|
// 🆕 같은 화면의 모든 컴포넌트 (TableList 자동 감지용)
|
|
|
|
|
allComponents,
|
2025-10-23 17:26:14 +09:00
|
|
|
// 플로우 선택된 데이터 정보 추가
|
|
|
|
|
flowSelectedData,
|
|
|
|
|
flowSelectedStepId,
|
2025-11-19 13:48:44 +09:00
|
|
|
// 🆕 컴포넌트별 설정 (parentDataMapping 등)
|
|
|
|
|
componentConfigs,
|
2025-12-01 18:39:01 +09:00
|
|
|
// 🆕 분할 패널 부모 데이터 (좌측 화면에서 선택된 데이터)
|
|
|
|
|
splitPanelParentData,
|
2025-12-16 14:38:03 +09:00
|
|
|
// 🆕 분할 패널 컨텍스트 (quickInsert 등에서 좌측 패널 데이터 접근용)
|
|
|
|
|
splitPanelContext: splitPanelContext ? {
|
|
|
|
|
selectedLeftData: splitPanelContext.selectedLeftData,
|
|
|
|
|
refreshRightPanel: splitPanelContext.refreshRightPanel,
|
|
|
|
|
} : undefined,
|
2025-11-19 13:48:44 +09:00
|
|
|
} as ButtonActionContext;
|
2025-09-12 14:24:25 +09:00
|
|
|
|
|
|
|
|
// 확인이 필요한 액션인지 확인
|
|
|
|
|
if (confirmationRequiredActions.includes(processedConfig.action.type)) {
|
|
|
|
|
// 확인 다이얼로그 표시
|
|
|
|
|
setPendingAction({
|
|
|
|
|
type: processedConfig.action.type,
|
|
|
|
|
config: processedConfig.action,
|
|
|
|
|
context,
|
|
|
|
|
});
|
|
|
|
|
setShowConfirmDialog(true);
|
|
|
|
|
} else {
|
|
|
|
|
// 확인이 필요하지 않은 액션은 바로 실행
|
|
|
|
|
await executeAction(processedConfig.action, context);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 액션이 설정되지 않은 경우 기본 onClick 실행
|
|
|
|
|
onClick?.();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 확인 다이얼로그에서 확인 버튼 클릭 시
|
|
|
|
|
const handleConfirmAction = async () => {
|
|
|
|
|
if (pendingAction) {
|
|
|
|
|
await executeAction(pendingAction.config, pendingAction.context);
|
|
|
|
|
}
|
|
|
|
|
setShowConfirmDialog(false);
|
|
|
|
|
setPendingAction(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 확인 다이얼로그에서 취소 버튼 클릭 시
|
|
|
|
|
const handleCancelAction = () => {
|
|
|
|
|
setShowConfirmDialog(false);
|
|
|
|
|
setPendingAction(null);
|
2025-09-11 18:38:28 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// DOM에 전달하면 안 되는 React-specific props 필터링
|
|
|
|
|
const {
|
|
|
|
|
selectedScreen,
|
|
|
|
|
onZoneComponentDrop,
|
|
|
|
|
onZoneClick,
|
|
|
|
|
componentConfig: _componentConfig,
|
|
|
|
|
component: _component,
|
|
|
|
|
isSelected: _isSelected,
|
|
|
|
|
onClick: _onClick,
|
|
|
|
|
onDragStart: _onDragStart,
|
|
|
|
|
onDragEnd: _onDragEnd,
|
|
|
|
|
size: _size,
|
|
|
|
|
position: _position,
|
|
|
|
|
style: _style,
|
2025-09-12 14:24:25 +09:00
|
|
|
screenId: _screenId,
|
|
|
|
|
tableName: _tableName,
|
|
|
|
|
onRefresh: _onRefresh,
|
|
|
|
|
onClose: _onClose,
|
2025-09-18 18:49:30 +09:00
|
|
|
selectedRows: _selectedRows,
|
|
|
|
|
selectedRowsData: _selectedRowsData,
|
|
|
|
|
onSelectedRowsChange: _onSelectedRowsChange,
|
2025-10-23 17:26:14 +09:00
|
|
|
flowSelectedData: _flowSelectedData, // 플로우 선택 데이터 필터링
|
|
|
|
|
flowSelectedStepId: _flowSelectedStepId, // 플로우 선택 스텝 ID 필터링
|
2025-10-23 17:55:04 +09:00
|
|
|
onFlowRefresh: _onFlowRefresh, // 플로우 새로고침 콜백 필터링
|
2025-09-18 18:49:30 +09:00
|
|
|
originalData: _originalData, // 부분 업데이트용 원본 데이터 필터링
|
2025-12-11 16:09:58 +09:00
|
|
|
_originalData: __originalData, // DOM 필터링
|
|
|
|
|
_initialData: __initialData, // DOM 필터링
|
|
|
|
|
_groupedData: __groupedData, // DOM 필터링
|
2025-09-18 18:49:30 +09:00
|
|
|
refreshKey: _refreshKey, // 필터링 추가
|
|
|
|
|
isInModal: _isInModal, // 필터링 추가
|
|
|
|
|
mode: _mode, // 필터링 추가
|
2025-09-11 18:38:28 +09:00
|
|
|
...domProps
|
|
|
|
|
} = props;
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
// 다이얼로그 메시지 생성
|
|
|
|
|
const getConfirmMessage = () => {
|
|
|
|
|
if (!pendingAction) return "";
|
|
|
|
|
|
|
|
|
|
const customMessage = pendingAction.config.confirmMessage;
|
|
|
|
|
if (customMessage) return customMessage;
|
|
|
|
|
|
|
|
|
|
switch (pendingAction.type) {
|
|
|
|
|
case "save":
|
|
|
|
|
return "변경사항을 저장하시겠습니까?";
|
|
|
|
|
case "delete":
|
|
|
|
|
return "정말로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.";
|
|
|
|
|
case "submit":
|
|
|
|
|
return "제출하시겠습니까?";
|
|
|
|
|
default:
|
|
|
|
|
return "이 작업을 실행하시겠습니까?";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getConfirmTitle = () => {
|
|
|
|
|
if (!pendingAction) return "";
|
|
|
|
|
|
|
|
|
|
switch (pendingAction.type) {
|
|
|
|
|
case "save":
|
|
|
|
|
return "저장 확인";
|
|
|
|
|
case "delete":
|
|
|
|
|
return "삭제 확인";
|
|
|
|
|
case "submit":
|
|
|
|
|
return "제출 확인";
|
|
|
|
|
default:
|
|
|
|
|
return "작업 확인";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-19 02:15:21 +09:00
|
|
|
// DOM 안전한 props만 필터링
|
|
|
|
|
const safeDomProps = filterDOMProps(domProps);
|
|
|
|
|
|
2025-10-23 18:23:01 +09:00
|
|
|
// 🆕 플로우 단계별 표시 제어
|
|
|
|
|
if (!shouldShowButton) {
|
|
|
|
|
// 레이아웃 동작에 따라 다르게 처리
|
|
|
|
|
if (flowConfig?.layoutBehavior === "preserve-position") {
|
|
|
|
|
// 위치 유지 (빈 공간, display: none)
|
|
|
|
|
return <div style={{ display: "none" }} />;
|
|
|
|
|
} else {
|
|
|
|
|
// 완전히 렌더링하지 않음 (auto-compact, 빈 공간 제거)
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-05 11:03:15 +09:00
|
|
|
// 🆕 최종 비활성화 상태 (설정 + 조건부 비활성화)
|
|
|
|
|
const finalDisabled = componentConfig.disabled || isOperationButtonDisabled || statusLoading;
|
|
|
|
|
|
2025-11-10 15:36:18 +09:00
|
|
|
// 공통 버튼 스타일
|
|
|
|
|
const buttonElementStyle: React.CSSProperties = {
|
|
|
|
|
width: "100%",
|
|
|
|
|
height: "100%",
|
|
|
|
|
minHeight: "40px",
|
|
|
|
|
border: "none",
|
|
|
|
|
borderRadius: "0.5rem",
|
2025-12-05 11:03:15 +09:00
|
|
|
background: finalDisabled ? "#e5e7eb" : buttonColor,
|
|
|
|
|
color: finalDisabled ? "#9ca3af" : "white",
|
2025-11-10 15:36:18 +09:00
|
|
|
// 🔧 크기 설정 적용 (sm/md/lg)
|
|
|
|
|
fontSize: componentConfig.size === "sm" ? "0.75rem" : componentConfig.size === "lg" ? "1rem" : "0.875rem",
|
|
|
|
|
fontWeight: "600",
|
2025-12-05 11:03:15 +09:00
|
|
|
cursor: finalDisabled ? "not-allowed" : "pointer",
|
2025-11-10 15:36:18 +09:00
|
|
|
outline: "none",
|
|
|
|
|
boxSizing: "border-box",
|
|
|
|
|
display: "flex",
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
justifyContent: "center",
|
|
|
|
|
// 🔧 크기에 따른 패딩 조정
|
2025-12-12 14:37:24 +09:00
|
|
|
padding: componentConfig.size === "sm" ? "0 0.75rem" : componentConfig.size === "lg" ? "0 1.25rem" : "0 1rem",
|
2025-11-10 15:36:18 +09:00
|
|
|
margin: "0",
|
|
|
|
|
lineHeight: "1.25",
|
2025-12-05 11:03:15 +09:00
|
|
|
boxShadow: finalDisabled ? "none" : "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
2025-11-11 18:27:27 +09:00
|
|
|
// 디자인 모드와 인터랙티브 모드 모두에서 사용자 스타일 적용 (width/height 제외)
|
2025-12-12 14:37:24 +09:00
|
|
|
...(component.style
|
|
|
|
|
? Object.fromEntries(Object.entries(component.style).filter(([key]) => key !== "width" && key !== "height"))
|
|
|
|
|
: {}),
|
2025-11-10 15:36:18 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const buttonContent = processedConfig.text !== undefined ? processedConfig.text : component.label || "버튼";
|
|
|
|
|
|
2025-09-11 18:38:28 +09:00
|
|
|
return (
|
2025-09-12 14:24:25 +09:00
|
|
|
<>
|
2025-09-19 02:15:21 +09:00
|
|
|
<div style={componentStyle} className={className} {...safeDomProps}>
|
2025-11-10 15:36:18 +09:00
|
|
|
{isDesignMode ? (
|
|
|
|
|
// 디자인 모드: div로 렌더링하여 선택 가능하게 함
|
|
|
|
|
<div
|
|
|
|
|
className="transition-colors duration-150 hover:opacity-90"
|
|
|
|
|
style={buttonElementStyle}
|
|
|
|
|
onClick={handleClick}
|
|
|
|
|
>
|
|
|
|
|
{buttonContent}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
// 일반 모드: button으로 렌더링
|
|
|
|
|
<button
|
|
|
|
|
type={componentConfig.actionType || "button"}
|
2025-12-05 11:03:15 +09:00
|
|
|
disabled={finalDisabled}
|
2025-12-12 14:37:24 +09:00
|
|
|
className="transition-colors transition-transform duration-150 hover:opacity-90 active:scale-95"
|
2025-11-10 15:36:18 +09:00
|
|
|
style={buttonElementStyle}
|
|
|
|
|
onClick={handleClick}
|
|
|
|
|
onDragStart={onDragStart}
|
|
|
|
|
onDragEnd={onDragEnd}
|
|
|
|
|
>
|
|
|
|
|
{buttonContent}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2025-09-12 14:24:25 +09:00
|
|
|
</div>
|
|
|
|
|
|
2025-10-02 14:34:15 +09:00
|
|
|
{/* 확인 다이얼로그 - EditModal보다 위에 표시하도록 z-index 최상위로 설정 */}
|
2025-09-12 14:24:25 +09:00
|
|
|
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
2025-10-02 14:34:15 +09:00
|
|
|
<AlertDialogContent className="z-[99999]">
|
2025-09-12 14:24:25 +09:00
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>{getConfirmTitle()}</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>{getConfirmMessage()}</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel onClick={handleCancelAction}>취소</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction onClick={handleConfirmAction}>
|
|
|
|
|
{pendingAction?.type === "save"
|
|
|
|
|
? "저장"
|
|
|
|
|
: pendingAction?.type === "delete"
|
|
|
|
|
? "삭제"
|
|
|
|
|
: pendingAction?.type === "submit"
|
|
|
|
|
? "제출"
|
|
|
|
|
: "확인"}
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
</>
|
2025-09-11 18:38:28 +09:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ButtonPrimary 래퍼 컴포넌트
|
|
|
|
|
* 추가적인 로직이나 상태 관리가 필요한 경우 사용
|
|
|
|
|
*/
|
|
|
|
|
export const ButtonPrimaryWrapper: React.FC<ButtonPrimaryComponentProps> = (props) => {
|
|
|
|
|
return <ButtonPrimaryComponent {...props} />;
|
|
|
|
|
};
|