버튼 과정이 조금 복잡하지만 위도경도 연속추적기능도 넣음

This commit is contained in:
leeheejin
2025-12-01 16:49:02 +09:00
parent 7263c9c3ff
commit fbeb3ec2c9
5 changed files with 823 additions and 578 deletions

View File

@@ -24,10 +24,10 @@ export type ButtonActionType =
| "excel_upload" // 엑셀 업로드
| "barcode_scan" // 바코드 스캔
| "code_merge" // 코드 병합
| "geolocation" // 위치정보 가져오기
| "geolocation" // 위치정보 가져오기 (1회성, 폼 업데이트만)
| "swap_fields" // 필드 값 교환 (출발지 ↔ 목적지)
| "update_field" // 특정 필드 값 변경 (예: status를 active로)
| "transferData"; // 🆕 데이터 전달 (컴포넌트 간 or 화면 간)
| "update_field" // 필드 값 변경 + 위치 수집 + 연속 추적 (통합)
| "transferData"; // 데이터 전달 (컴포넌트 간 or 화면 간)
/**
* 버튼 액션 설정
@@ -104,6 +104,8 @@ export interface ButtonActionConfig {
geolocationTimeout?: number; // 타임아웃 (ms, 기본: 10000)
geolocationMaxAge?: number; // 캐시된 위치 최대 수명 (ms, 기본: 0)
geolocationAutoSave?: boolean; // 위치 가져온 후 자동 저장 여부 (기본: false)
geolocationKeyField?: string; // DB UPDATE 시 WHERE 조건에 사용할 키 필드 (예: "user_id")
geolocationKeySourceField?: string; // 키 값 소스 (예: "__userId__" 또는 폼 필드명)
geolocationUpdateField?: boolean; // 위치정보와 함께 추가 필드 변경 여부
geolocationExtraTableName?: string; // 추가 필드 변경 대상 테이블 (다른 테이블 가능)
geolocationExtraField?: string; // 추가로 변경할 필드명 (예: "status")
@@ -121,6 +123,20 @@ export interface ButtonActionConfig {
geolocationSecondKeySourceField?: string; // 현재 폼에서 키 값을 가져올 필드 (예: "vehicle_id") - UPDATE 모드에서만 사용
geolocationSecondInsertFields?: Record<string, any>; // INSERT 모드에서 추가로 넣을 필드들
// 🆕 연속 위치 추적 설정 (update_field 액션의 updateWithTracking 옵션용)
trackingInterval?: number; // 위치 저장 주기 (ms, 기본: 10000 = 10초)
trackingTripIdField?: string; // 운행 ID를 저장할 필드명 (예: "trip_id")
trackingAutoGenerateTripId?: boolean; // 운행 ID 자동 생성 여부 (기본: true)
trackingDepartureField?: string; // 출발지 필드명 (formData에서 가져옴)
trackingArrivalField?: string; // 도착지 필드명 (formData에서 가져옴)
trackingVehicleIdField?: string; // 차량 ID 필드명 (formData에서 가져옴)
trackingStatusOnStart?: string; // 추적 시작 시 상태값 (예: "active")
trackingStatusOnStop?: string; // 추적 종료 시 상태값 (예: "completed")
trackingStatusField?: string; // 상태 필드명 (vehicles 테이블 등)
trackingStatusTableName?: string; // 상태 변경 대상 테이블명
trackingStatusKeyField?: string; // 상태 변경 키 필드 (예: "user_id")
trackingStatusKeySourceField?: string; // 키 값 소스 (예: "__userId__")
// 필드 값 교환 관련 (출발지 ↔ 목적지)
swapFieldA?: string; // 교환할 첫 번째 필드명 (예: "departure")
swapFieldB?: string; // 교환할 두 번째 필드명 (예: "destination")
@@ -131,11 +147,19 @@ export interface ButtonActionConfig {
updateTargetValue?: string | number | boolean; // 변경할 값 (예: "active")
updateAutoSave?: boolean; // 변경 후 자동 저장 여부 (기본: true)
updateMultipleFields?: Array<{ field: string; value: string | number | boolean }>; // 여러 필드 동시 변경
updateTableName?: string; // 대상 테이블명 (다른 테이블 UPDATE 시)
updateKeyField?: string; // 키 필드명 (WHERE 조건에 사용)
updateKeySourceField?: string; // 키 값 소스 (폼 필드명 또는 __userId__ 등 특수 키워드)
// 🆕 필드 값 변경 + 위치정보 수집 (update_field 액션에서 사용)
updateWithGeolocation?: boolean; // 위치정보도 함께 수집할지 여부
updateGeolocationLatField?: string; // 위도 저장 필드
updateGeolocationLngField?: string; // 경도 저장 필드
// 🆕 필드 값 변경 + 연속 위치 추적 (update_field 액션에서 사용)
updateWithTracking?: boolean; // 연속 위치 추적 사용 여부
updateTrackingMode?: "start" | "stop"; // 추적 모드 (시작/종료)
updateTrackingInterval?: number; // 위치 저장 주기 (ms, 기본: 10000)
updateGeolocationAccuracyField?: string; // 정확도 저장 필드 (선택)
updateGeolocationTimestampField?: string; // 타임스탬프 저장 필드 (선택)
@@ -3301,6 +3325,258 @@ export class ButtonActionExecutor {
}
}
// 🆕 연속 위치 추적 상태 저장 (전역)
private static trackingIntervalId: NodeJS.Timeout | null = null;
private static currentTripId: string | null = null;
private static trackingContext: ButtonActionContext | null = null;
private static trackingConfig: ButtonActionConfig | null = null;
/**
* 연속 위치 추적 시작
*/
private static async handleTrackingStart(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
try {
console.log("🚀 [handleTrackingStart] 위치 추적 시작:", { config, context });
// 이미 추적 중인지 확인
if (this.trackingIntervalId) {
toast.warning("이미 위치 추적이 진행 중입니다.");
return false;
}
// 위치 권한 확인
if (!navigator.geolocation) {
toast.error("이 브라우저는 위치 정보를 지원하지 않습니다.");
return false;
}
// Trip ID 생성
const tripId = config.trackingAutoGenerateTripId !== false
? `TRIP_${Date.now()}_${context.userId || "unknown"}`
: context.formData?.[config.trackingTripIdField || "trip_id"] || `TRIP_${Date.now()}`;
this.currentTripId = tripId;
this.trackingContext = context;
this.trackingConfig = config;
// 출발지/도착지 정보
const departure = context.formData?.[config.trackingDepartureField || "departure"] || null;
const arrival = context.formData?.[config.trackingArrivalField || "arrival"] || null;
const departureName = context.formData?.["departure_name"] || null;
const destinationName = context.formData?.["destination_name"] || null;
const vehicleId = context.formData?.[config.trackingVehicleIdField || "vehicle_id"] || null;
console.log("📍 [handleTrackingStart] 운행 정보:", {
tripId,
departure,
arrival,
departureName,
destinationName,
vehicleId,
});
// 상태 변경 (vehicles 테이블 등)
if (config.trackingStatusOnStart && config.trackingStatusField) {
try {
const { apiClient } = await import("@/lib/api/client");
const statusTableName = config.trackingStatusTableName || context.tableName;
const keyField = config.trackingStatusKeyField || "user_id";
const keyValue = resolveSpecialKeyword(config.trackingStatusKeySourceField || "__userId__", context);
if (keyValue) {
await apiClient.put(`/dynamic-form/update-field`, {
tableName: statusTableName,
keyField: keyField,
keyValue: keyValue,
updateField: config.trackingStatusField,
updateValue: config.trackingStatusOnStart,
});
console.log("✅ 상태 변경 완료:", config.trackingStatusOnStart);
}
} catch (statusError) {
console.warn("⚠️ 상태 변경 실패:", statusError);
}
}
// 첫 번째 위치 저장
await this.saveLocationToHistory(tripId, departure, arrival, departureName, destinationName, vehicleId);
// 주기적 위치 저장 시작
const interval = config.trackingInterval || 10000; // 기본 10초
this.trackingIntervalId = setInterval(async () => {
await this.saveLocationToHistory(tripId, departure, arrival, departureName, destinationName, vehicleId);
}, interval);
toast.success(config.successMessage || `위치 추적이 시작되었습니다. (${interval / 1000}초 간격)`);
// 추적 시작 이벤트 발생 (UI 업데이트용)
window.dispatchEvent(new CustomEvent("trackingStarted", {
detail: { tripId, interval }
}));
return true;
} catch (error: any) {
console.error("❌ 위치 추적 시작 실패:", error);
toast.error(config.errorMessage || "위치 추적 시작 중 오류가 발생했습니다.");
return false;
}
}
/**
* 연속 위치 추적 종료
*/
private static async handleTrackingStop(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
try {
console.log("🛑 [handleTrackingStop] 위치 추적 종료:", { config, context });
// 추적 중인지 확인
if (!this.trackingIntervalId) {
toast.warning("진행 중인 위치 추적이 없습니다.");
return false;
}
// 타이머 정리
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;
await this.saveLocationToHistory(tripId, departure, arrival, departureName, destinationName, vehicleId, "completed");
// 상태 변경 (vehicles 테이블 등)
const effectiveConfig = config.trackingStatusOnStop ? config : this.trackingConfig;
const effectiveContext = context.userId ? context : this.trackingContext;
if (effectiveConfig?.trackingStatusOnStop && effectiveConfig?.trackingStatusField && effectiveContext) {
try {
const { apiClient } = await import("@/lib/api/client");
const statusTableName = effectiveConfig.trackingStatusTableName || effectiveContext.tableName;
const keyField = effectiveConfig.trackingStatusKeyField || "user_id";
const keyValue = resolveSpecialKeyword(effectiveConfig.trackingStatusKeySourceField || "__userId__", effectiveContext);
if (keyValue) {
await apiClient.put(`/dynamic-form/update-field`, {
tableName: statusTableName,
keyField: keyField,
keyValue: keyValue,
updateField: effectiveConfig.trackingStatusField,
updateValue: effectiveConfig.trackingStatusOnStop,
});
console.log("✅ 상태 변경 완료:", effectiveConfig.trackingStatusOnStop);
}
} catch (statusError) {
console.warn("⚠️ 상태 변경 실패:", statusError);
}
}
// 컨텍스트 정리
this.currentTripId = null;
this.trackingContext = null;
this.trackingConfig = null;
toast.success(config.successMessage || "위치 추적이 종료되었습니다.");
// 추적 종료 이벤트 발생 (UI 업데이트용)
window.dispatchEvent(new CustomEvent("trackingStopped", {
detail: { tripId }
}));
// 화면 새로고침
context.onRefresh?.();
return true;
} catch (error: any) {
console.error("❌ 위치 추적 종료 실패:", error);
toast.error(config.errorMessage || "위치 추적 종료 중 오류가 발생했습니다.");
return false;
}
}
/**
* 위치 이력 테이블에 저장 (내부 헬퍼)
*/
private static async saveLocationToHistory(
tripId: string | null,
departure: string | null,
arrival: string | null,
departureName: string | null,
destinationName: string | null,
vehicleId: number | null,
tripStatus: string = "active"
): Promise<void> {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
async (position) => {
try {
const { apiClient } = await import("@/lib/api/client");
const locationData = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
altitude: position.coords.altitude,
speed: position.coords.speed,
heading: position.coords.heading,
tripId,
tripStatus,
departure,
arrival,
departureName,
destinationName,
recordedAt: new Date(position.timestamp).toISOString(),
vehicleId,
};
console.log("📍 [saveLocationToHistory] 위치 저장:", locationData);
const response = await apiClient.post(`/dynamic-form/location-history`, locationData);
if (response.data?.success) {
console.log("✅ 위치 이력 저장 성공:", response.data.data);
} else {
console.warn("⚠️ 위치 이력 저장 실패:", response.data);
}
resolve();
} catch (error) {
console.error("❌ 위치 이력 저장 오류:", error);
reject(error);
}
},
(error) => {
console.error("❌ 위치 획득 실패:", error.message);
reject(error);
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0,
}
);
});
}
/**
* 현재 추적 상태 확인 (외부에서 호출 가능)
*/
static isTracking(): boolean {
return this.trackingIntervalId !== null;
}
/**
* 현재 Trip ID 가져오기 (외부에서 호출 가능)
*/
static getCurrentTripId(): string | null {
return this.currentTripId;
}
/**
* 데이터 전달 액션 처리 (분할 패널에서 좌측 → 우측 데이터 전달)
*/
@@ -3399,18 +3675,13 @@ export class ButtonActionExecutor {
/**
* 위치정보 가져오기 액션 처리
* - 1회성 위치 수집
* - 폼 필드 업데이트
* - 자동 저장 옵션 시 DB UPDATE
*/
private static async handleGeolocation(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
try {
console.log("📍 위치정보 가져오기 액션 실행:", { config, context });
console.log("📍 [디버그] 추가 필드 설정값:", {
geolocationUpdateField: config.geolocationUpdateField,
geolocationExtraField: config.geolocationExtraField,
geolocationExtraValue: config.geolocationExtraValue,
geolocationExtraTableName: config.geolocationExtraTableName,
geolocationExtraKeyField: config.geolocationExtraKeyField,
geolocationExtraKeySourceField: config.geolocationExtraKeySourceField,
});
// 브라우저 Geolocation API 지원 확인
if (!navigator.geolocation) {
@@ -3419,41 +3690,30 @@ export class ButtonActionExecutor {
}
// 위도/경도 저장 필드 확인
const latField = config.geolocationLatField;
const lngField = config.geolocationLngField;
if (!latField || !lngField) {
toast.error("위도/경도 저장 필드가 설정되지 않았습니다.");
return false;
}
const latField = config.geolocationLatField || "latitude";
const lngField = config.geolocationLngField || "longitude";
// 로딩 토스트 표시
const loadingToastId = toast.loading("위치 정보를 가져오는 중...");
// Geolocation 옵션 설정
const options: PositionOptions = {
enableHighAccuracy: config.geolocationHighAccuracy !== false, // 기본 true
timeout: config.geolocationTimeout || 10000, // 기본 10초
maximumAge: config.geolocationMaxAge || 0, // 기본 0 (항상 새로운 위치)
enableHighAccuracy: config.geolocationHighAccuracy !== false,
timeout: config.geolocationTimeout || 10000,
maximumAge: config.geolocationMaxAge || 0,
};
// 위치 정보 가져오기 (Promise로 래핑)
// 위치 정보 가져오기
const position = await new Promise<GeolocationPosition>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, options);
});
// 로딩 토스트 제거
toast.dismiss(loadingToastId);
const { latitude, longitude, accuracy, altitude, heading, speed } = position.coords;
const { latitude, longitude, accuracy } = position.coords;
const timestamp = new Date(position.timestamp);
console.log("📍 위치정보 획득 성공:", {
latitude,
longitude,
accuracy,
timestamp: timestamp.toISOString(),
});
console.log("📍 위치정보 획득 성공:", { latitude, longitude, accuracy });
// 폼 데이터 업데이트
const updates: Record<string, any> = {
@@ -3461,7 +3721,6 @@ export class ButtonActionExecutor {
[lngField]: longitude,
};
// 선택적 필드들
if (config.geolocationAccuracyField && accuracy !== null) {
updates[config.geolocationAccuracyField] = accuracy;
}
@@ -3469,289 +3728,71 @@ export class ButtonActionExecutor {
updates[config.geolocationTimestampField] = timestamp.toISOString();
}
// 🆕 추가 필드 변경 (위치정보 + 상태변경)
let extraTableUpdated = false;
let secondTableUpdated = false;
if (config.geolocationUpdateField && config.geolocationExtraField && config.geolocationExtraValue !== undefined) {
const extraTableName = config.geolocationExtraTableName || context.tableName;
const currentTableName = config.geolocationTableName || context.tableName;
const keySourceField = config.geolocationExtraKeySourceField;
// 🆕 특수 키워드가 설정되어 있으면 바로 DB UPDATE (같은 테이블이어도)
const hasSpecialKeyword = keySourceField?.startsWith("__") && keySourceField?.endsWith("__");
const isDifferentTable = extraTableName && extraTableName !== currentTableName;
// 다른 테이블이거나 특수 키워드가 설정된 경우 → 바로 DB UPDATE
if (isDifferentTable || hasSpecialKeyword) {
console.log("📍 DB 직접 UPDATE:", {
targetTable: extraTableName,
field: config.geolocationExtraField,
value: config.geolocationExtraValue,
keyField: config.geolocationExtraKeyField,
keySourceField: keySourceField,
hasSpecialKeyword,
isDifferentTable,
});
// 키 값 가져오기 (특수 키워드 지원)
const keyValue = resolveSpecialKeyword(keySourceField, context);
if (keyValue && config.geolocationExtraKeyField) {
try {
// DB UPDATE API 호출
const { apiClient } = await import("@/lib/api/client");
const response = await apiClient.put(`/dynamic-form/update-field`, {
tableName: extraTableName,
keyField: config.geolocationExtraKeyField,
keyValue: keyValue,
updateField: config.geolocationExtraField,
updateValue: config.geolocationExtraValue,
});
if (response.data?.success) {
extraTableUpdated = true;
console.log("✅ DB UPDATE 성공:", response.data);
} else {
console.error("❌ DB UPDATE 실패:", response.data);
toast.error(`${extraTableName} 테이블 업데이트에 실패했습니다.`);
}
} catch (apiError) {
console.error("❌ DB UPDATE API 오류:", apiError);
toast.error(`${extraTableName} 테이블 업데이트 중 오류가 발생했습니다.`);
}
} else {
console.warn("⚠️ 키 값이 없어서 DB UPDATE를 건너뜁니다:", {
keySourceField: keySourceField,
keyValue,
});
}
} else {
// 같은 테이블이고 특수 키워드가 없는 경우 (현재 폼 데이터에 추가)
updates[config.geolocationExtraField] = config.geolocationExtraValue;
console.log("📍 같은 테이블 추가 필드 변경 (폼 데이터):", {
field: config.geolocationExtraField,
value: config.geolocationExtraValue,
});
}
}
// 🆕 두 번째 테이블 INSERT 또는 UPDATE
if (config.geolocationSecondTableEnabled &&
config.geolocationSecondTableName) {
const secondMode = config.geolocationSecondMode || "update";
console.log("📍 두 번째 테이블 작업:", {
mode: secondMode,
targetTable: config.geolocationSecondTableName,
field: config.geolocationSecondField,
value: config.geolocationSecondValue,
keyField: config.geolocationSecondKeyField,
keySourceField: config.geolocationSecondKeySourceField,
});
try {
const { apiClient } = await import("@/lib/api/client");
if (secondMode === "insert") {
// INSERT 모드: 새 레코드 생성
const insertData: Record<string, any> = {
// 위치정보 포함 (선택적)
...(config.geolocationSecondInsertFields || {}),
};
// 기본 필드 추가
if (config.geolocationSecondField && config.geolocationSecondValue !== undefined) {
insertData[config.geolocationSecondField] = config.geolocationSecondValue;
}
// 위치정보도 두 번째 테이블에 저장하려면 추가
// (선택적으로 위도/경도도 저장)
if (config.geolocationSecondInsertFields?.includeLocation) {
insertData[latField] = latitude;
insertData[lngField] = longitude;
if (config.geolocationAccuracyField) {
insertData[config.geolocationAccuracyField] = accuracy;
}
if (config.geolocationTimestampField) {
insertData[config.geolocationTimestampField] = timestamp.toISOString();
}
}
// 현재 폼에서 키 값 가져와서 연결 (외래키) - 특수 키워드 지원
if (config.geolocationSecondKeySourceField && config.geolocationSecondKeyField) {
const keyValue = resolveSpecialKeyword(config.geolocationSecondKeySourceField, context);
if (keyValue) {
insertData[config.geolocationSecondKeyField] = keyValue;
}
}
console.log("📍 두 번째 테이블 INSERT 데이터:", insertData);
const response = await apiClient.post(`/dynamic-form/save`, {
tableName: config.geolocationSecondTableName,
data: insertData,
});
if (response.data?.success) {
secondTableUpdated = true;
console.log("✅ 두 번째 테이블 INSERT 성공:", response.data);
} else {
console.error("❌ 두 번째 테이블 INSERT 실패:", response.data);
toast.error(`${config.geolocationSecondTableName} 테이블 저장에 실패했습니다.`);
}
} else {
// UPDATE 모드: 기존 레코드 수정
if (config.geolocationSecondField && config.geolocationSecondValue !== undefined) {
// 특수 키워드 지원
const secondKeyValue = resolveSpecialKeyword(config.geolocationSecondKeySourceField, context);
if (secondKeyValue && config.geolocationSecondKeyField) {
const response = await apiClient.put(`/dynamic-form/update-field`, {
tableName: config.geolocationSecondTableName,
keyField: config.geolocationSecondKeyField,
keyValue: secondKeyValue,
updateField: config.geolocationSecondField,
updateValue: config.geolocationSecondValue,
});
if (response.data?.success) {
secondTableUpdated = true;
console.log("✅ 두 번째 테이블 UPDATE 성공:", response.data);
} else {
console.error("❌ 두 번째 테이블 UPDATE 실패:", response.data);
toast.error(`${config.geolocationSecondTableName} 테이블 업데이트에 실패했습니다.`);
}
} else {
console.warn("⚠️ 두 번째 테이블 키 값이 없어서 UPDATE를 건너뜁니다:", {
keySourceField: config.geolocationSecondKeySourceField,
keyValue: secondKeyValue,
});
}
}
}
} catch (apiError) {
console.error("❌ 두 번째 테이블 API 오류:", apiError);
toast.error(`${config.geolocationSecondTableName} 테이블 작업 중 오류가 발생했습니다.`);
}
}
// formData 업데이트
// onFormDataChange로 폼 업데이트
if (context.onFormDataChange) {
Object.entries(updates).forEach(([field, value]) => {
context.onFormDataChange?.(field, value);
context.onFormDataChange!(field, value);
});
}
// 성공 메시지 생성
let successMsg =
config.successMessage ||
`위치 정보를 가져왔습니다.\n위도: ${latitude.toFixed(6)}, 경도: ${longitude.toFixed(6)}`;
// 추가 필드 변경이 있으면 메시지에 포함
if (config.geolocationUpdateField && config.geolocationExtraField) {
if (extraTableUpdated) {
successMsg += `\n[${config.geolocationExtraTableName}] ${config.geolocationExtraField}: ${config.geolocationExtraValue}`;
} else if (
!config.geolocationExtraTableName ||
config.geolocationExtraTableName === (config.geolocationTableName || context.tableName)
) {
successMsg += `\n${config.geolocationExtraField}: ${config.geolocationExtraValue}`;
}
}
// 두 번째 테이블 변경이 있으면 메시지에 포함
if (secondTableUpdated && config.geolocationSecondTableName) {
successMsg += `\n[${config.geolocationSecondTableName}] ${config.geolocationSecondField}: ${config.geolocationSecondValue}`;
}
// 성공 메시지 표시
toast.success(successMsg);
// 자동 저장 옵션이 활성화된 경우
// 🆕 자동 저장 옵션이 활성화된 경우 DB UPDATE
if (config.geolocationAutoSave) {
console.log("📍 위치정보 자동 저장 실행");
// onSave 콜백이 있으면 사용
if (context.onSave) {
const keyField = config.geolocationKeyField || "user_id";
const keySourceField = config.geolocationKeySourceField || "__userId__";
const keyValue = resolveSpecialKeyword(keySourceField, context);
const targetTableName = config.geolocationTableName || context.tableName;
if (keyValue && targetTableName) {
try {
await context.onSave();
toast.success("위치 정보가 저장되었습니다.");
const { apiClient } = await import("@/lib/api/client");
// 위치 정보 필드들 업데이트 (위도, 경도, 정확도, 타임스탬프)
const fieldsToUpdate = { ...updates };
// formData에서 departure, arrival만 포함 (테이블에 있을 가능성 높은 필드만)
if (context.formData?.departure) fieldsToUpdate.departure = context.formData.departure;
if (context.formData?.arrival) fieldsToUpdate.arrival = context.formData.arrival;
// 추가 필드 변경 (status 등)
if (config.geolocationExtraField && config.geolocationExtraValue !== undefined) {
fieldsToUpdate[config.geolocationExtraField] = config.geolocationExtraValue;
}
console.log("📍 DB UPDATE 시작:", { targetTableName, keyField, keyValue, fieldsToUpdate });
// 각 필드를 개별적으로 UPDATE (에러 무시)
let successCount = 0;
for (const [field, value] of Object.entries(fieldsToUpdate)) {
try {
const response = await apiClient.put(`/dynamic-form/update-field`, {
tableName: targetTableName,
keyField,
keyValue,
updateField: field,
updateValue: value,
});
if (response.data?.success) {
successCount++;
}
} catch {
// 컬럼이 없으면 조용히 무시 (에러 로그 안 찍음)
}
}
console.log(`📍 DB UPDATE 완료: ${successCount}/${Object.keys(fieldsToUpdate).length} 필드 저장됨`);
toast.success(config.successMessage || "위치 정보가 저장되었습니다.");
} catch (saveError) {
console.error("❌ 위치정보 자동 저장 실패:", saveError);
toast.error("위치 정보 저장에 실패했습니다.");
return false;
}
} else if (context.tableName && context.formData) {
// onSave가 없으면 직접 API 호출
// 키 필드 설정이 있으면 update-field API 사용 (더 안전)
const keyField = config.geolocationExtraKeyField;
const keySourceField = config.geolocationExtraKeySourceField;
if (keyField && keySourceField) {
try {
const { apiClient } = await import("@/lib/api/client");
const keyValue = resolveSpecialKeyword(keySourceField, context);
if (keyValue) {
// formData에서 저장할 필드들 추출 (위치정보 + 출발지/도착지 등)
const fieldsToSave = { ...updates };
// formData에서 추가로 저장할 필드들 (테이블에 존재할 가능성이 높은 필드만)
// departure, arrival은 location-swap-selector에서 설정한 필드명 사용
const additionalFields = ['departure', 'arrival'];
additionalFields.forEach(field => {
if (context.formData?.[field] !== undefined && context.formData[field] !== '') {
fieldsToSave[field] = context.formData[field];
}
});
console.log("📍 개별 필드 UPDATE:", {
tableName: context.tableName,
keyField,
keyValue,
fieldsToSave,
});
// 각 필드를 개별적으로 UPDATE (에러가 나도 다른 필드 계속 저장)
let successCount = 0;
let failCount = 0;
for (const [field, value] of Object.entries(fieldsToSave)) {
try {
console.log(`🔄 UPDATE: ${context.tableName}.${field} = ${value}`);
const response = await apiClient.put(`/dynamic-form/update-field`, {
tableName: context.tableName,
keyField: keyField,
keyValue: keyValue,
updateField: field,
updateValue: value,
});
if (response.data?.success) {
successCount++;
console.log(`${field} 업데이트 성공`);
} else {
failCount++;
console.warn(`⚠️ ${field} 업데이트 실패:`, response.data);
}
} catch (fieldError) {
failCount++;
console.warn(`⚠️ ${field} 업데이트 오류 (컬럼이 없을 수 있음):`, fieldError);
}
}
console.log(`✅ 필드 저장 완료: 성공 ${successCount}개, 실패 ${failCount}`);
}
} catch (saveError) {
console.error("❌ 위치정보 자동 저장 실패:", saveError);
toast.error("위치 정보 저장에 실패했습니다.");
}
} else {
console.warn("⚠️ 키 필드가 설정되지 않아 자동 저장을 건너뜁니다.");
}
} else {
console.warn("⚠️ 키 값 또는 테이블명이 없어서 자동 저장을 건너뜁니다:", { keyValue, targetTableName });
toast.success(config.successMessage || `위치: ${latitude.toFixed(6)}, ${longitude.toFixed(6)}`);
}
} else {
// 자동 저장 없이 성공 메시지만
toast.success(config.successMessage || `위치: ${latitude.toFixed(6)}, ${longitude.toFixed(6)}`);
}
return true;
@@ -3838,11 +3879,32 @@ export class ButtonActionExecutor {
/**
* 필드 값 변경 액션 처리 (예: status를 active로 변경)
* 🆕 위치정보 수집 기능 추가
* 🆕 연속 위치 추적 기능 추가
*/
private static async handleUpdateField(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
try {
console.log("🔄 필드 값 변경 액션 실행:", { config, context });
// 🆕 연속 위치 추적 모드 처리
if (config.updateWithTracking) {
const trackingConfig: ButtonActionConfig = {
...config,
trackingInterval: config.updateTrackingInterval || config.trackingInterval || 10000,
trackingStatusField: config.updateTargetField,
trackingStatusTableName: config.updateTableName || context.tableName,
trackingStatusKeyField: config.updateKeyField,
trackingStatusKeySourceField: config.updateKeySourceField,
};
if (config.updateTrackingMode === "start") {
trackingConfig.trackingStatusOnStart = config.updateTargetValue as string;
return await this.handleTrackingStart(trackingConfig, context);
} else if (config.updateTrackingMode === "stop") {
trackingConfig.trackingStatusOnStop = config.updateTargetValue as string;
return await this.handleTrackingStop(trackingConfig, context);
}
}
const { formData, tableName, onFormDataChange, onSave } = context;
// 변경할 필드 확인
@@ -4153,6 +4215,12 @@ export const DEFAULT_BUTTON_ACTIONS: Record<ButtonActionType, Partial<ButtonActi
type: "edit",
successMessage: "편집되었습니다.",
},
copy: {
type: "copy",
confirmMessage: "복사하시겠습니까?",
successMessage: "복사되었습니다.",
errorMessage: "복사 중 오류가 발생했습니다.",
},
control: {
type: "control",
},
@@ -4196,11 +4264,16 @@ export const DEFAULT_BUTTON_ACTIONS: Record<ButtonActionType, Partial<ButtonActi
geolocationHighAccuracy: true,
geolocationTimeout: 10000,
geolocationMaxAge: 0,
geolocationAutoSave: false,
confirmMessage: "현재 위치 정보를 가져오시겠습니까?",
geolocationLatField: "latitude",
geolocationLngField: "longitude",
successMessage: "위치 정보를 가져왔습니다.",
errorMessage: "위치 정보를 가져오는 중 오류가 발생했습니다.",
},
swap_fields: {
type: "swap_fields",
successMessage: "필드 값이 교환되었습니다.",
errorMessage: "필드 값 교환 중 오류가 발생했습니다.",
},
update_field: {
type: "update_field",
updateAutoSave: true,