버튼활성화비활성화
This commit is contained in:
parent
5c12b9fa83
commit
ccf8bd3284
|
|
@ -1953,6 +1953,139 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 🆕 버튼 활성화 조건 설정 */}
|
||||
<div className="mt-4 border-t pt-4">
|
||||
<h5 className="mb-3 text-xs font-medium text-muted-foreground">버튼 활성화 조건</h5>
|
||||
|
||||
{/* 출발지/도착지 필수 체크 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label htmlFor="require-location">출발지/도착지 필수</Label>
|
||||
<p className="text-xs text-muted-foreground">선택하지 않으면 버튼 비활성화</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="require-location"
|
||||
checked={config.action?.requireLocationFields === true}
|
||||
onCheckedChange={(checked) => onUpdateProperty("componentConfig.action.requireLocationFields", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{config.action?.requireLocationFields && (
|
||||
<div className="mt-3 space-y-2 rounded-md bg-orange-50 p-3 dark:bg-orange-950">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label>출발지 필드명</Label>
|
||||
<Input
|
||||
placeholder="departure"
|
||||
value={config.action?.trackingDepartureField || "departure"}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.trackingDepartureField", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>도착지 필드명</Label>
|
||||
<Input
|
||||
placeholder="destination"
|
||||
value={config.action?.trackingArrivalField || "destination"}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.trackingArrivalField", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 상태 기반 활성화 조건 */}
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<Label htmlFor="enable-on-status">상태 기반 활성화</Label>
|
||||
<p className="text-xs text-muted-foreground">특정 상태일 때만 버튼 활성화</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="enable-on-status"
|
||||
checked={config.action?.enableOnStatusCheck === true}
|
||||
onCheckedChange={(checked) => onUpdateProperty("componentConfig.action.enableOnStatusCheck", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{config.action?.enableOnStatusCheck && (
|
||||
<div className="mt-3 space-y-2 rounded-md bg-purple-50 p-3 dark:bg-purple-950">
|
||||
<div>
|
||||
<Label>상태 조회 테이블</Label>
|
||||
<Select
|
||||
value={config.action?.statusCheckTableName || "vehicles"}
|
||||
onValueChange={(value) => onUpdateProperty("componentConfig.action.statusCheckTableName", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableTables.map((table) => (
|
||||
<SelectItem key={table.name} value={table.name} className="text-xs">
|
||||
{table.label || table.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="mt-1 text-[10px] text-muted-foreground">
|
||||
상태를 조회할 테이블 (기본: vehicles)
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>조회 키 필드</Label>
|
||||
<Input
|
||||
placeholder="user_id"
|
||||
value={config.action?.statusCheckKeyField || "user_id"}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.statusCheckKeyField", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<p className="mt-1 text-[10px] text-muted-foreground">
|
||||
현재 로그인 사용자 ID로 조회할 필드 (기본: user_id)
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>상태 컬럼명</Label>
|
||||
<Input
|
||||
placeholder="status"
|
||||
value={config.action?.statusCheckField || "status"}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.statusCheckField", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<p className="mt-1 text-[10px] text-muted-foreground">
|
||||
상태 값이 저장된 컬럼명 (기본: status)
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>상태 조건</Label>
|
||||
<Select
|
||||
value={config.action?.statusConditionType || "enableOn"}
|
||||
onValueChange={(value) => onUpdateProperty("componentConfig.action.statusConditionType", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="enableOn">이 상태일 때만 활성화</SelectItem>
|
||||
<SelectItem value="disableOn">이 상태일 때 비활성화</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>상태값 (쉼표로 구분)</Label>
|
||||
<Input
|
||||
placeholder="예: active, inactive"
|
||||
value={config.action?.statusConditionValues || ""}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.statusConditionValues", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<p className="mt-1 text-[10px] text-muted-foreground">
|
||||
여러 상태값은 쉼표(,)로 구분
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-md bg-blue-50 p-3 dark:bg-blue-950">
|
||||
<p className="text-xs text-blue-900 dark:text-blue-100">
|
||||
<strong>사용 예시:</strong>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -3613,6 +3613,33 @@ export class ButtonActionExecutor {
|
|||
|
||||
await this.saveLocationToHistory(tripId, departure, arrival, departureName, destinationName, vehicleId, "completed");
|
||||
|
||||
// 🆕 거리/시간 계산 및 저장
|
||||
if (tripId) {
|
||||
try {
|
||||
const tripStats = await this.calculateTripStats(tripId);
|
||||
console.log("📊 운행 통계:", tripStats);
|
||||
|
||||
// 운행 통계를 vehicle_location_history의 마지막 레코드에 업데이트하거나
|
||||
// 별도 테이블에 저장할 수 있음
|
||||
if (tripStats) {
|
||||
// 이벤트로 통계 전달 (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 테이블 등)
|
||||
const effectiveConfig = config.trackingStatusOnStop ? config : this.trackingConfig;
|
||||
const effectiveContext = context.userId ? context : this.trackingContext;
|
||||
|
|
@ -3662,6 +3689,101 @@ export class ButtonActionExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 운행 통계 계산 (거리, 시간)
|
||||
*/
|
||||
private static async calculateTripStats(tripId: string): Promise<{
|
||||
totalDistanceKm: number;
|
||||
totalTimeMinutes: number;
|
||||
startTime: string | null;
|
||||
endTime: string | null;
|
||||
} | null> {
|
||||
try {
|
||||
// vehicle_location_history에서 해당 trip의 모든 위치 조회 (직접 fetch 사용)
|
||||
const token = typeof window !== "undefined" ? localStorage.getItem("authToken") || "" : "";
|
||||
|
||||
const response = await fetch(`/api/dynamic-form/list/vehicle_location_history?trip_id=${encodeURIComponent(tripId)}&pageSize=10000&sortBy=recorded_at&sortOrder=asc`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.log("📊 통계 계산: API 응답 실패", response.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data?.rows?.length) {
|
||||
console.log("📊 통계 계산: 데이터 없음");
|
||||
return null;
|
||||
}
|
||||
|
||||
const locations = result.data.rows;
|
||||
console.log(`📊 통계 계산: ${locations.length}개 위치 데이터`);
|
||||
|
||||
// 시간 계산
|
||||
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;
|
||||
|
||||
console.log("📊 운행 통계 결과:", {
|
||||
tripId,
|
||||
totalDistanceKm,
|
||||
totalTimeMinutes,
|
||||
startTime,
|
||||
endTime,
|
||||
pointCount: locations.length,
|
||||
});
|
||||
|
||||
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도 함께 업데이트
|
||||
|
|
@ -4217,6 +4339,28 @@ export class ButtonActionExecutor {
|
|||
try {
|
||||
console.log("🔄 운행알림/종료 액션 실행:", { config, context });
|
||||
|
||||
// 🆕 출발지/도착지 필수 체크 (운행 시작 모드일 때만)
|
||||
// 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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue