Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into common/feat/dashboard-map

This commit is contained in:
dohyeons 2025-12-15 18:01:42 +09:00
commit 8cc189da17
10 changed files with 475 additions and 36 deletions

View File

@ -26,12 +26,56 @@ interface EditModalState {
onSave?: () => void;
groupByColumns?: string[]; // 🆕 그룹핑 컬럼 (예: ["order_no"])
tableName?: string; // 🆕 테이블명 (그룹 조회용)
buttonConfig?: any; // 🆕 버튼 설정 (제어로직 실행용)
buttonContext?: any; // 🆕 버튼 컨텍스트 (screenId, userId 등)
saveButtonConfig?: {
enableDataflowControl?: boolean;
dataflowConfig?: any;
dataflowTiming?: string;
}; // 🆕 모달 내부 저장 버튼의 제어로직 설정
}
interface EditModalProps {
className?: string;
}
/**
* ( )
* action.type이 "save" button-primary
*/
const findSaveButtonInComponents = (components: any[]): any | null => {
if (!components || !Array.isArray(components)) return null;
for (const comp of components) {
// button-primary이고 action.type이 save인 경우
if (
comp.componentType === "button-primary" &&
comp.componentConfig?.action?.type === "save"
) {
return comp;
}
// conditional-container의 sections 내부 탐색
if (comp.componentType === "conditional-container" && comp.componentConfig?.sections) {
for (const section of comp.componentConfig.sections) {
if (section.screenId) {
// 조건부 컨테이너의 내부 화면은 별도로 로드해야 함
// 여기서는 null 반환하고, loadSaveButtonConfig에서 처리
continue;
}
}
}
// 자식 컴포넌트가 있으면 재귀 탐색
if (comp.children && Array.isArray(comp.children)) {
const found = findSaveButtonInComponents(comp.children);
if (found) return found;
}
}
return null;
};
export const EditModal: React.FC<EditModalProps> = ({ className }) => {
const { user } = useAuth();
const [modalState, setModalState] = useState<EditModalState>({
@ -44,6 +88,9 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
onSave: undefined,
groupByColumns: undefined,
tableName: undefined,
buttonConfig: undefined,
buttonContext: undefined,
saveButtonConfig: undefined,
});
const [screenData, setScreenData] = useState<{
@ -115,11 +162,88 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
};
};
// 🆕 모달 내부 저장 버튼의 제어로직 설정 조회
const loadSaveButtonConfig = async (targetScreenId: number): Promise<{
enableDataflowControl?: boolean;
dataflowConfig?: any;
dataflowTiming?: string;
} | null> => {
try {
// 1. 대상 화면의 레이아웃 조회
const layoutData = await screenApi.getLayout(targetScreenId);
if (!layoutData?.components) {
console.log("[EditModal] 레이아웃 컴포넌트 없음:", targetScreenId);
return null;
}
// 2. 저장 버튼 찾기
let saveButton = findSaveButtonInComponents(layoutData.components);
// 3. conditional-container가 있는 경우 내부 화면도 탐색
if (!saveButton) {
for (const comp of layoutData.components) {
if (comp.componentType === "conditional-container" && comp.componentConfig?.sections) {
for (const section of comp.componentConfig.sections) {
if (section.screenId) {
try {
const innerLayoutData = await screenApi.getLayout(section.screenId);
saveButton = findSaveButtonInComponents(innerLayoutData?.components || []);
if (saveButton) {
console.log("[EditModal] 조건부 컨테이너 내부에서 저장 버튼 발견:", {
sectionScreenId: section.screenId,
sectionLabel: section.label,
});
break;
}
} catch (innerError) {
console.warn("[EditModal] 내부 화면 레이아웃 조회 실패:", section.screenId);
}
}
}
if (saveButton) break;
}
}
}
if (!saveButton) {
console.log("[EditModal] 저장 버튼을 찾을 수 없음:", targetScreenId);
return null;
}
// 4. webTypeConfig에서 제어로직 설정 추출
const webTypeConfig = saveButton.webTypeConfig;
if (webTypeConfig?.enableDataflowControl) {
const config = {
enableDataflowControl: webTypeConfig.enableDataflowControl,
dataflowConfig: webTypeConfig.dataflowConfig,
dataflowTiming: webTypeConfig.dataflowConfig?.flowConfig?.executionTiming || "after",
};
console.log("[EditModal] 저장 버튼 제어로직 설정 발견:", config);
return config;
}
console.log("[EditModal] 저장 버튼에 제어로직 설정 없음");
return null;
} catch (error) {
console.warn("[EditModal] 저장 버튼 설정 조회 실패:", error);
return null;
}
};
// 전역 모달 이벤트 리스너
useEffect(() => {
const handleOpenEditModal = (event: CustomEvent) => {
const { screenId, title, description, modalSize, editData, onSave, groupByColumns, tableName, isCreateMode } =
event.detail;
const handleOpenEditModal = async (event: CustomEvent) => {
const { screenId, title, description, modalSize, editData, onSave, groupByColumns, tableName, isCreateMode, buttonConfig, buttonContext } = event.detail;
// 🆕 모달 내부 저장 버튼의 제어로직 설정 조회
let saveButtonConfig: EditModalState["saveButtonConfig"] = undefined;
if (screenId) {
const config = await loadSaveButtonConfig(screenId);
if (config) {
saveButtonConfig = config;
}
}
setModalState({
isOpen: true,
@ -131,6 +255,9 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
onSave,
groupByColumns, // 🆕 그룹핑 컬럼
tableName, // 🆕 테이블명
buttonConfig, // 🆕 버튼 설정
buttonContext, // 🆕 버튼 컨텍스트
saveButtonConfig, // 🆕 모달 내부 저장 버튼의 제어로직 설정
});
// 편집 데이터로 폼 데이터 초기화
@ -578,6 +705,46 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
}
}
// 🆕 저장 후 제어로직 실행 (버튼의 After 타이밍 제어)
// 우선순위: 모달 내부 저장 버튼 설정(saveButtonConfig) > 수정 버튼에서 전달받은 설정(buttonConfig)
try {
const controlConfig = modalState.saveButtonConfig || modalState.buttonConfig;
console.log("[EditModal] 그룹 저장 완료 후 제어로직 실행 시도", {
hasSaveButtonConfig: !!modalState.saveButtonConfig,
hasButtonConfig: !!modalState.buttonConfig,
controlConfig,
});
if (controlConfig?.enableDataflowControl && controlConfig?.dataflowTiming === "after") {
console.log("🎯 [EditModal] 저장 후 제어로직 발견:", controlConfig.dataflowConfig);
// buttonActions의 executeAfterSaveControl 동적 import
const { ButtonActionExecutor } = await import("@/lib/utils/buttonActions");
// 제어로직 실행
await ButtonActionExecutor.executeAfterSaveControl(
controlConfig,
{
formData: modalState.editData,
screenId: modalState.buttonContext?.screenId || modalState.screenId,
tableName: modalState.buttonContext?.tableName || screenData?.screenInfo?.tableName,
userId: user?.userId,
companyCode: user?.companyCode,
onRefresh: modalState.onSave,
}
);
console.log("✅ [EditModal] 제어로직 실행 완료");
} else {
console.log(" [EditModal] 저장 후 실행할 제어로직 없음");
}
} catch (controlError) {
console.error("❌ [EditModal] 제어로직 실행 오류:", controlError);
// 제어로직 오류는 저장 성공을 방해하지 않음 (경고만 표시)
toast.warning("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다.");
}
handleClose();
} else {
toast.info("변경된 내용이 없습니다.");
@ -612,6 +779,37 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
}
}
// 🆕 저장 후 제어로직 실행 (버튼의 After 타이밍 제어)
// 우선순위: 모달 내부 저장 버튼 설정(saveButtonConfig) > 수정 버튼에서 전달받은 설정(buttonConfig)
try {
const controlConfig = modalState.saveButtonConfig || modalState.buttonConfig;
console.log("[EditModal] INSERT 완료 후 제어로직 실행 시도", { controlConfig });
if (controlConfig?.enableDataflowControl && controlConfig?.dataflowTiming === "after") {
console.log("🎯 [EditModal] 저장 후 제어로직 발견:", controlConfig.dataflowConfig);
const { ButtonActionExecutor } = await import("@/lib/utils/buttonActions");
await ButtonActionExecutor.executeAfterSaveControl(
controlConfig,
{
formData,
screenId: modalState.buttonContext?.screenId || modalState.screenId,
tableName: modalState.buttonContext?.tableName || screenData?.screenInfo?.tableName,
userId: user?.userId,
companyCode: user?.companyCode,
onRefresh: modalState.onSave,
}
);
console.log("✅ [EditModal] 제어로직 실행 완료");
}
} catch (controlError) {
console.error("❌ [EditModal] 제어로직 실행 오류:", controlError);
toast.warning("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다.");
}
handleClose();
} else {
throw new Error(response.message || "생성에 실패했습니다.");
@ -654,6 +852,37 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
}
}
// 🆕 저장 후 제어로직 실행 (버튼의 After 타이밍 제어)
// 우선순위: 모달 내부 저장 버튼 설정(saveButtonConfig) > 수정 버튼에서 전달받은 설정(buttonConfig)
try {
const controlConfig = modalState.saveButtonConfig || modalState.buttonConfig;
console.log("[EditModal] UPDATE 완료 후 제어로직 실행 시도", { controlConfig });
if (controlConfig?.enableDataflowControl && controlConfig?.dataflowTiming === "after") {
console.log("🎯 [EditModal] 저장 후 제어로직 발견:", controlConfig.dataflowConfig);
const { ButtonActionExecutor } = await import("@/lib/utils/buttonActions");
await ButtonActionExecutor.executeAfterSaveControl(
controlConfig,
{
formData,
screenId: modalState.buttonContext?.screenId || modalState.screenId,
tableName: modalState.buttonContext?.tableName || screenData?.screenInfo?.tableName,
userId: user?.userId,
companyCode: user?.companyCode,
onRefresh: modalState.onSave,
}
);
console.log("✅ [EditModal] 제어로직 실행 완료");
}
} catch (controlError) {
console.error("❌ [EditModal] 제어로직 실행 오류:", controlError);
toast.warning("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다.");
}
handleClose();
} else {
throw new Error(response.message || "수정에 실패했습니다.");

View File

@ -376,6 +376,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
// 🔥 제어관리 설정 추가 (webTypeConfig에서 가져옴)
enableDataflowControl: component.webTypeConfig?.enableDataflowControl,
dataflowConfig: component.webTypeConfig?.dataflowConfig,
dataflowTiming: component.webTypeConfig?.dataflowTiming,
};
} else if (componentConfig.action && typeof componentConfig.action === "object") {
// 🔥 이미 객체인 경우에도 제어관리 설정 추가
@ -383,8 +384,19 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
...componentConfig.action,
enableDataflowControl: component.webTypeConfig?.enableDataflowControl,
dataflowConfig: component.webTypeConfig?.dataflowConfig,
dataflowTiming: component.webTypeConfig?.dataflowTiming,
};
}
// 🔍 디버깅: processedConfig.action 확인
console.log("[ButtonPrimaryComponent] processedConfig.action 생성 완료", {
actionType: processedConfig.action?.type,
enableDataflowControl: processedConfig.action?.enableDataflowControl,
dataflowTiming: processedConfig.action?.dataflowTiming,
dataflowConfig: processedConfig.action?.dataflowConfig,
webTypeConfigRaw: component.webTypeConfig,
componentText: component.text,
});
// 스타일 계산
// height: 100%로 부모(RealtimePreviewDynamic의 내부 div)의 높이를 따라감

View File

@ -53,6 +53,9 @@ export interface LocationSwapSelectorProps {
formData?: Record<string, any>;
onFormDataChange?: (field: string, value: any) => void;
// 🆕 사용자 정보 (DB에서 초기값 로드용)
userId?: string;
// componentConfig (화면 디자이너에서 전달)
componentConfig?: {
dataSource?: DataSourceConfig;
@ -65,6 +68,10 @@ export interface LocationSwapSelectorProps {
showSwapButton?: boolean;
swapButtonPosition?: "center" | "right";
variant?: "card" | "inline" | "minimal";
// 🆕 DB 초기값 로드 설정
loadFromDb?: boolean; // DB에서 초기값 로드 여부
dbTableName?: string; // 조회할 테이블명 (기본: vehicles)
dbKeyField?: string; // 키 필드 (기본: user_id)
};
}
@ -80,6 +87,7 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
formData = {},
onFormDataChange,
componentConfig,
userId,
} = props;
// componentConfig에서 설정 가져오기 (우선순위: componentConfig > props)
@ -93,6 +101,11 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
const destinationLabel = config.destinationLabel || props.destinationLabel || "도착지";
const showSwapButton = config.showSwapButton !== false && props.showSwapButton !== false;
const variant = config.variant || props.variant || "card";
// 🆕 DB 초기값 로드 설정
const loadFromDb = config.loadFromDb !== false; // 기본값 true
const dbTableName = config.dbTableName || "vehicles";
const dbKeyField = config.dbKeyField || "user_id";
// 기본 옵션 (포항/광양)
const DEFAULT_OPTIONS: LocationOption[] = [
@ -104,6 +117,7 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
const [options, setOptions] = useState<LocationOption[]>(DEFAULT_OPTIONS);
const [loading, setLoading] = useState(false);
const [isSwapping, setIsSwapping] = useState(false);
const [dbLoaded, setDbLoaded] = useState(false); // DB 로드 완료 여부
// 로컬 선택 상태 (Select 컴포넌트용)
const [localDeparture, setLocalDeparture] = useState<string>("");
@ -193,8 +207,89 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
loadOptions();
}, [dataSource, isDesignMode]);
// formData에서 초기값 동기화
// 🆕 DB에서 초기값 로드 (새로고침 시에도 출발지/목적지 유지)
useEffect(() => {
const loadFromDatabase = async () => {
// 디자인 모드이거나, DB 로드 비활성화이거나, userId가 없으면 스킵
if (isDesignMode || !loadFromDb || !userId) {
console.log("[LocationSwapSelector] DB 로드 스킵:", { isDesignMode, loadFromDb, userId });
return;
}
// 이미 로드했으면 스킵
if (dbLoaded) {
return;
}
try {
console.log("[LocationSwapSelector] DB에서 출발지/목적지 로드 시작:", { dbTableName, dbKeyField, userId });
const response = await apiClient.post(
`/table-management/tables/${dbTableName}/data`,
{
page: 1,
size: 1,
search: { [dbKeyField]: userId },
autoFilter: true,
}
);
const vehicleData = response.data?.data?.data?.[0] || response.data?.data?.rows?.[0];
if (vehicleData) {
const dbDeparture = vehicleData[departureField] || vehicleData.departure;
const dbDestination = vehicleData[destinationField] || vehicleData.arrival || vehicleData.destination;
console.log("[LocationSwapSelector] DB에서 로드된 값:", { dbDeparture, dbDestination });
// DB에 값이 있으면 로컬 상태 및 formData 업데이트
if (dbDeparture && options.some(o => o.value === dbDeparture)) {
setLocalDeparture(dbDeparture);
onFormDataChange?.(departureField, dbDeparture);
// 라벨도 업데이트
if (departureLabelField) {
const opt = options.find(o => o.value === dbDeparture);
if (opt) {
onFormDataChange?.(departureLabelField, opt.label);
}
}
}
if (dbDestination && options.some(o => o.value === dbDestination)) {
setLocalDestination(dbDestination);
onFormDataChange?.(destinationField, dbDestination);
// 라벨도 업데이트
if (destinationLabelField) {
const opt = options.find(o => o.value === dbDestination);
if (opt) {
onFormDataChange?.(destinationLabelField, opt.label);
}
}
}
}
setDbLoaded(true);
} catch (error) {
console.error("[LocationSwapSelector] DB 로드 실패:", error);
setDbLoaded(true); // 실패해도 다시 시도하지 않음
}
};
// 옵션이 로드된 후에 DB 로드 실행
if (options.length > 0) {
loadFromDatabase();
}
}, [userId, loadFromDb, dbTableName, dbKeyField, departureField, destinationField, options, isDesignMode, dbLoaded, onFormDataChange, departureLabelField, destinationLabelField]);
// formData에서 초기값 동기화 (DB 로드 후에도 formData 변경 시 반영)
useEffect(() => {
// DB 로드가 완료되지 않았으면 스킵 (DB 값 우선)
if (loadFromDb && userId && !dbLoaded) {
return;
}
const depVal = formData[departureField];
const destVal = formData[destinationField];
@ -204,7 +299,7 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps)
if (destVal && options.some(o => o.value === destVal)) {
setLocalDestination(destVal);
}
}, [formData, departureField, destinationField, options]);
}, [formData, departureField, destinationField, options, loadFromDb, userId, dbLoaded]);
// 출발지 변경
const handleDepartureChange = (selectedValue: string) => {

View File

@ -470,6 +470,58 @@ export function LocationSwapSelectorConfigPanel({
</div>
</div>
{/* DB 초기값 로드 설정 */}
<div className="space-y-2 border-t pt-4">
<h4 className="text-sm font-medium">DB </h4>
<p className="text-xs text-muted-foreground">
DB에 /
</p>
<div className="flex items-center justify-between">
<Label>DB에서 </Label>
<Switch
checked={config?.loadFromDb !== false}
onCheckedChange={(checked) => handleChange("loadFromDb", checked)}
/>
</div>
{config?.loadFromDb !== false && (
<>
<div className="space-y-2">
<Label> </Label>
<Select
value={config?.dbTableName || "vehicles"}
onValueChange={(value) => handleChange("dbTableName", value)}
>
<SelectTrigger>
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="vehicles">vehicles ()</SelectItem>
{tables.map((table) => (
<SelectItem key={table.name} value={table.name}>
{table.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label> </Label>
<Input
value={config?.dbKeyField || "user_id"}
onChange={(e) => handleChange("dbKeyField", e.target.value)}
placeholder="user_id"
/>
<p className="text-xs text-muted-foreground">
ID로 (기본: user_id)
</p>
</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">
@ -480,6 +532,8 @@ export function LocationSwapSelectorConfigPanel({
2. /
<br />
3.
<br />
4. DB
</p>
</div>
</div>

View File

@ -100,3 +100,4 @@
- [컴포넌트 시스템 가이드](../../docs/컴포넌트_시스템_가이드.md)
- [split-panel-layout (v1)](../split-panel-layout/README.md)

View File

@ -40,3 +40,4 @@ export class SplitPanelLayout2Renderer extends AutoRegisteringComponentRenderer
// 자동 등록 실행
SplitPanelLayout2Renderer.registerSelf();

View File

@ -840,3 +840,4 @@ export function FieldDetailSettingsModal({
);
}

View File

@ -794,3 +794,4 @@ export function SaveSettingsModal({
);
}

View File

@ -514,3 +514,4 @@ export function SectionLayoutModal({
);
}

View File

@ -2292,6 +2292,8 @@ export class ButtonActionExecutor {
editData: rowData,
groupByColumns: groupByColumns.length > 0 ? groupByColumns : undefined, // 🆕 그룹핑 컬럼 전달
tableName: context.tableName, // 🆕 테이블명 전달
buttonConfig: config, // 🆕 버튼 설정 전달 (제어로직 실행용)
buttonContext: context, // 🆕 버튼 컨텍스트 전달 (screenId, userId 등)
onSave: () => {
context.onRefresh?.();
},
@ -2827,9 +2829,10 @@ export class ButtonActionExecutor {
/**
* (After Timing)
* EditModal public으로
*
*/
private static async executeAfterSaveControl(
public static async executeAfterSaveControl(
config: ButtonActionConfig,
context: ButtonActionContext,
): Promise<void> {
@ -4273,39 +4276,80 @@ export class ButtonActionExecutor {
try {
console.log("🛑 [handleTrackingStop] 위치 추적 종료:", { config, context });
// 추적 중인지 확인
if (!this.trackingIntervalId) {
toast.warning("진행 중인 위치 추적이 없습니다.");
return false;
// 추적 중인지 확인 (새로고침 후에도 DB 상태 기반 종료 가능하도록 수정)
const isTrackingActive = !!this.trackingIntervalId;
if (!isTrackingActive) {
// 추적 중이 아니어도 DB 상태 변경은 진행 (새로고침 후 종료 지원)
console.log("⚠️ [handleTrackingStop] trackingIntervalId 없음 - DB 상태 기반 종료 진행");
} else {
// 타이머 정리 (추적 중인 경우에만)
clearInterval(this.trackingIntervalId);
this.trackingIntervalId = null;
}
// 타이머 정리
clearInterval(this.trackingIntervalId);
this.trackingIntervalId = null;
const tripId = this.currentTripId;
// 마지막 위치 저장 (trip_status를 completed로)
const departure =
this.trackingContext?.formData?.[this.trackingConfig?.trackingDepartureField || "departure"] || null;
const arrival = 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 =
this.trackingContext?.formData?.[this.trackingConfig?.trackingVehicleIdField || "vehicle_id"] || null;
// 🆕 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);
}
}
await this.saveLocationToHistory(
tripId,
departure,
arrival,
departureName,
destinationName,
vehicleId,
"completed",
);
// 마지막 위치 저장 (추적 중이었던 경우에만)
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;
// 🆕 거리/시간 계산 및 저장
if (tripId) {
await this.saveLocationToHistory(
tripId,
departure,
arrival,
departureName,
destinationName,
vehicleId,
"completed",
);
}
// 🆕 거리/시간 계산 및 저장 (추적 중이었던 경우에만)
if (isTrackingActive && tripId) {
try {
const tripStats = await this.calculateTripStats(tripId);
console.log("📊 운행 통계:", tripStats);
@ -4417,9 +4461,9 @@ export class ButtonActionExecutor {
}
}
// 상태 변경 (vehicles 테이블 등)
const effectiveConfig = config.trackingStatusOnStop ? config : this.trackingConfig;
const effectiveContext = context.userId ? context : this.trackingContext;
// 상태 변경 (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 {