사용 예시:
diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx
index 5816940a..6f5c8739 100644
--- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx
+++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx
@@ -26,6 +26,7 @@ import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
import { useScreenContextOptional } from "@/contexts/ScreenContext";
import { useSplitPanelContext, SplitPanelPosition } from "@/contexts/SplitPanelContext";
import { applyMappingRules } from "@/lib/utils/dataMapping";
+import { apiClient } from "@/lib/api/client";
export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
config?: ButtonPrimaryConfig;
@@ -148,6 +149,149 @@ export const ButtonPrimaryComponent: React.FC = ({
return result;
}, [flowConfig, currentStep, component.id, component.label]);
+ // 🆕 운행알림 버튼 조건부 비활성화 (출발지/도착지 필수, 상태 체크)
+ // 상태는 API로 조회 (formData에 없는 경우)
+ const [vehicleStatus, setVehicleStatus] = useState(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;
+
+ const fetchStatus = async () => {
+ if (!isMounted) return;
+
+ try {
+ const response = await apiClient.post(`/table-management/tables/${statusTableName}/data`, {
+ page: 1,
+ size: 1,
+ search: { [statusKeyField]: userId },
+ autoFilter: true,
+ });
+
+ if (!isMounted) return;
+
+ const rows = response.data?.data?.data || response.data?.data?.rows || response.data?.rows || [];
+ const firstRow = Array.isArray(rows) ? rows[0] : null;
+
+ 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();
+
+ // 2초마다 갱신
+ const interval = setInterval(fetchStatus, 2000);
+
+ return () => {
+ isMounted = false;
+ clearInterval(interval);
+ };
+ }, [shouldFetchStatus, statusTableName, statusKeyField, statusFieldName, userId, component.label]);
+
+ // 버튼 비활성화 조건 계산
+ const isOperationButtonDisabled = useMemo(() => {
+ const actionConfig = component.componentConfig?.action;
+
+ if (actionConfig?.type !== "operation_control") return false;
+
+ // 1. 출발지/도착지 필수 체크
+ if (actionConfig?.requireLocationFields) {
+ const departureField = actionConfig.trackingDepartureField || "departure";
+ const destinationField = actionConfig.trackingArrivalField || "destination";
+
+ const departure = formData?.[departureField];
+ const destination = formData?.[destinationField];
+
+ // console.log("🔍 [ButtonPrimary] 출발지/도착지 체크:", {
+ // departureField, destinationField, departure, destination,
+ // buttonLabel: component.label
+ // });
+
+ 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];
+
+ const conditionType = actionConfig.statusConditionType || "enableOn";
+ const conditionValues = (actionConfig.statusConditionValues || "")
+ .split(",")
+ .map((v: string) => v.trim())
+ .filter((v: string) => v);
+
+ // console.log("🔍 [ButtonPrimary] 상태 조건 체크:", {
+ // statusField,
+ // formDataStatus: formData?.[statusField],
+ // apiStatus: vehicleStatus,
+ // currentStatus,
+ // conditionType,
+ // conditionValues,
+ // buttonLabel: component.label,
+ // });
+
+ // 상태 로딩 중이면 비활성화
+ if (statusLoading) {
+ // console.log("⏳ [ButtonPrimary] 상태 로딩 중 → 비활성화:", component.label);
+ return true;
+ }
+
+ // 상태값이 없으면 → 비활성화 (조건 확인 불가)
+ if (!currentStatus) {
+ // console.log("🚫 [ButtonPrimary] 상태값 없음 → 비활성화:", component.label);
+ return true;
+ }
+
+ 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]);
+
// 확인 다이얼로그 상태
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [pendingAction, setPendingAction] = useState<{
@@ -877,6 +1021,9 @@ export const ButtonPrimaryComponent: React.FC = ({
}
}
+ // 🆕 최종 비활성화 상태 (설정 + 조건부 비활성화)
+ const finalDisabled = componentConfig.disabled || isOperationButtonDisabled || statusLoading;
+
// 공통 버튼 스타일
const buttonElementStyle: React.CSSProperties = {
width: "100%",
@@ -884,12 +1031,12 @@ export const ButtonPrimaryComponent: React.FC = ({
minHeight: "40px",
border: "none",
borderRadius: "0.5rem",
- background: componentConfig.disabled ? "#e5e7eb" : buttonColor,
- color: componentConfig.disabled ? "#9ca3af" : "white",
+ background: finalDisabled ? "#e5e7eb" : buttonColor,
+ color: finalDisabled ? "#9ca3af" : "white",
// 🔧 크기 설정 적용 (sm/md/lg)
fontSize: componentConfig.size === "sm" ? "0.75rem" : componentConfig.size === "lg" ? "1rem" : "0.875rem",
fontWeight: "600",
- cursor: componentConfig.disabled ? "not-allowed" : "pointer",
+ cursor: finalDisabled ? "not-allowed" : "pointer",
outline: "none",
boxSizing: "border-box",
display: "flex",
@@ -900,7 +1047,7 @@ export const ButtonPrimaryComponent: React.FC = ({
componentConfig.size === "sm" ? "0 0.75rem" : componentConfig.size === "lg" ? "0 1.25rem" : "0 1rem",
margin: "0",
lineHeight: "1.25",
- boxShadow: componentConfig.disabled ? "none" : "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
+ boxShadow: finalDisabled ? "none" : "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
// 디자인 모드와 인터랙티브 모드 모두에서 사용자 스타일 적용 (width/height 제외)
...(component.style ? Object.fromEntries(
Object.entries(component.style).filter(([key]) => key !== 'width' && key !== 'height')
@@ -925,7 +1072,7 @@ export const ButtonPrimaryComponent: React.FC = ({
// 일반 모드: button으로 렌더링