버튼활성화비활성화

This commit is contained in:
leeheejin
2025-12-05 11:03:15 +09:00
parent 5c12b9fa83
commit ccf8bd3284
3 changed files with 429 additions and 5 deletions

View File

@@ -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<ButtonPrimaryComponentProps> = ({
return result;
}, [flowConfig, currentStep, component.id, component.label]);
// 🆕 운행알림 버튼 조건부 비활성화 (출발지/도착지 필수, 상태 체크)
// 상태는 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;
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<ButtonPrimaryComponentProps> = ({
}
}
// 🆕 최종 비활성화 상태 (설정 + 조건부 비활성화)
const finalDisabled = componentConfig.disabled || isOperationButtonDisabled || statusLoading;
// 공통 버튼 스타일
const buttonElementStyle: React.CSSProperties = {
width: "100%",
@@ -884,12 +1031,12 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
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<ButtonPrimaryComponentProps> = ({
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<ButtonPrimaryComponentProps> = ({
// 일반 모드: button으로 렌더링
<button
type={componentConfig.actionType || "button"}
disabled={componentConfig.disabled || false}
disabled={finalDisabled}
className="transition-colors duration-150 hover:opacity-90 active:scale-95 transition-transform"
style={buttonElementStyle}
onClick={handleClick}