Merge pull request 'lhj' (#233) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/233
This commit is contained in:
commit
18521339bb
|
|
@ -482,3 +482,125 @@ export const updateFieldValue = async (
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 위치 이력 저장 (연속 위치 추적용)
|
||||
* POST /api/dynamic-form/location-history
|
||||
*/
|
||||
export const saveLocationHistory = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<Response | void> => {
|
||||
try {
|
||||
const { companyCode, userId } = req.user as any;
|
||||
const {
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
altitude,
|
||||
speed,
|
||||
heading,
|
||||
tripId,
|
||||
tripStatus,
|
||||
departure,
|
||||
arrival,
|
||||
departureName,
|
||||
destinationName,
|
||||
recordedAt,
|
||||
vehicleId,
|
||||
} = req.body;
|
||||
|
||||
console.log("📍 [saveLocationHistory] 요청:", {
|
||||
userId,
|
||||
companyCode,
|
||||
latitude,
|
||||
longitude,
|
||||
tripId,
|
||||
});
|
||||
|
||||
// 필수 필드 검증
|
||||
if (latitude === undefined || longitude === undefined) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "필수 필드가 누락되었습니다. (latitude, longitude)",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await dynamicFormService.saveLocationHistory({
|
||||
userId,
|
||||
companyCode,
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
altitude,
|
||||
speed,
|
||||
heading,
|
||||
tripId,
|
||||
tripStatus: tripStatus || "active",
|
||||
departure,
|
||||
arrival,
|
||||
departureName,
|
||||
destinationName,
|
||||
recordedAt: recordedAt || new Date().toISOString(),
|
||||
vehicleId,
|
||||
});
|
||||
|
||||
console.log("✅ [saveLocationHistory] 성공:", result);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: "위치 이력이 저장되었습니다.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("❌ [saveLocationHistory] 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "위치 이력 저장에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 위치 이력 조회 (경로 조회용)
|
||||
* GET /api/dynamic-form/location-history/:tripId
|
||||
*/
|
||||
export const getLocationHistory = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<Response | void> => {
|
||||
try {
|
||||
const { companyCode } = req.user as any;
|
||||
const { tripId } = req.params;
|
||||
const { userId, startDate, endDate, limit } = req.query;
|
||||
|
||||
console.log("📍 [getLocationHistory] 요청:", {
|
||||
tripId,
|
||||
userId,
|
||||
startDate,
|
||||
endDate,
|
||||
limit,
|
||||
});
|
||||
|
||||
const result = await dynamicFormService.getLocationHistory({
|
||||
companyCode,
|
||||
tripId,
|
||||
userId: userId as string,
|
||||
startDate: startDate as string,
|
||||
endDate: endDate as string,
|
||||
limit: limit ? parseInt(limit as string) : 1000,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
count: result.length,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("❌ [getLocationHistory] 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "위치 이력 조회에 실패했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import {
|
|||
validateFormData,
|
||||
getTableColumns,
|
||||
getTablePrimaryKeys,
|
||||
saveLocationHistory,
|
||||
getLocationHistory,
|
||||
} from "../controllers/dynamicFormController";
|
||||
|
||||
const router = express.Router();
|
||||
|
|
@ -40,4 +42,8 @@ router.get("/table/:tableName/columns", getTableColumns);
|
|||
// 테이블 기본키 조회
|
||||
router.get("/table/:tableName/primary-keys", getTablePrimaryKeys);
|
||||
|
||||
// 위치 이력 (연속 위치 추적)
|
||||
router.post("/location-history", saveLocationHistory);
|
||||
router.get("/location-history/:tripId", getLocationHistory);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1731,6 +1731,191 @@ export class DynamicFormService {
|
|||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 위치 이력 저장 (연속 위치 추적용)
|
||||
*/
|
||||
async saveLocationHistory(data: {
|
||||
userId: string;
|
||||
companyCode: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
accuracy?: number;
|
||||
altitude?: number;
|
||||
speed?: number;
|
||||
heading?: number;
|
||||
tripId?: string;
|
||||
tripStatus?: string;
|
||||
departure?: string;
|
||||
arrival?: string;
|
||||
departureName?: string;
|
||||
destinationName?: string;
|
||||
recordedAt?: string;
|
||||
vehicleId?: number;
|
||||
}): Promise<{ id: number }> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
console.log("📍 [saveLocationHistory] 저장 시작:", data);
|
||||
|
||||
const sqlQuery = `
|
||||
INSERT INTO vehicle_location_history (
|
||||
user_id,
|
||||
company_code,
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
altitude,
|
||||
speed,
|
||||
heading,
|
||||
trip_id,
|
||||
trip_status,
|
||||
departure,
|
||||
arrival,
|
||||
departure_name,
|
||||
destination_name,
|
||||
recorded_at,
|
||||
vehicle_id
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
const params = [
|
||||
data.userId,
|
||||
data.companyCode,
|
||||
data.latitude,
|
||||
data.longitude,
|
||||
data.accuracy || null,
|
||||
data.altitude || null,
|
||||
data.speed || null,
|
||||
data.heading || null,
|
||||
data.tripId || null,
|
||||
data.tripStatus || "active",
|
||||
data.departure || null,
|
||||
data.arrival || null,
|
||||
data.departureName || null,
|
||||
data.destinationName || null,
|
||||
data.recordedAt ? new Date(data.recordedAt) : new Date(),
|
||||
data.vehicleId || null,
|
||||
];
|
||||
|
||||
const result = await client.query(sqlQuery, params);
|
||||
|
||||
console.log("✅ [saveLocationHistory] 저장 완료:", {
|
||||
id: result.rows[0]?.id,
|
||||
});
|
||||
|
||||
return { id: result.rows[0]?.id };
|
||||
} catch (error) {
|
||||
console.error("❌ [saveLocationHistory] 오류:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 위치 이력 조회 (경로 조회용)
|
||||
*/
|
||||
async getLocationHistory(params: {
|
||||
companyCode: string;
|
||||
tripId?: string;
|
||||
userId?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
limit?: number;
|
||||
}): Promise<any[]> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
console.log("📍 [getLocationHistory] 조회 시작:", params);
|
||||
|
||||
const conditions: string[] = [];
|
||||
const queryParams: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 멀티테넌시: company_code 필터
|
||||
if (params.companyCode && params.companyCode !== "*") {
|
||||
conditions.push(`company_code = $${paramIndex}`);
|
||||
queryParams.push(params.companyCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// trip_id 필터
|
||||
if (params.tripId) {
|
||||
conditions.push(`trip_id = $${paramIndex}`);
|
||||
queryParams.push(params.tripId);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// user_id 필터
|
||||
if (params.userId) {
|
||||
conditions.push(`user_id = $${paramIndex}`);
|
||||
queryParams.push(params.userId);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 날짜 범위 필터
|
||||
if (params.startDate) {
|
||||
conditions.push(`recorded_at >= $${paramIndex}`);
|
||||
queryParams.push(new Date(params.startDate));
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (params.endDate) {
|
||||
conditions.push(`recorded_at <= $${paramIndex}`);
|
||||
queryParams.push(new Date(params.endDate));
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
||||
const limitClause = params.limit ? `LIMIT ${params.limit}` : "LIMIT 1000";
|
||||
|
||||
const sqlQuery = `
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
vehicle_id,
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
altitude,
|
||||
speed,
|
||||
heading,
|
||||
trip_id,
|
||||
trip_status,
|
||||
departure,
|
||||
arrival,
|
||||
departure_name,
|
||||
destination_name,
|
||||
recorded_at,
|
||||
created_at,
|
||||
company_code
|
||||
FROM vehicle_location_history
|
||||
${whereClause}
|
||||
ORDER BY recorded_at ASC
|
||||
${limitClause}
|
||||
`;
|
||||
|
||||
console.log("🔍 [getLocationHistory] 쿼리:", sqlQuery);
|
||||
console.log("🔍 [getLocationHistory] 파라미터:", queryParams);
|
||||
|
||||
const result = await client.query(sqlQuery, queryParams);
|
||||
|
||||
console.log("✅ [getLocationHistory] 조회 완료:", {
|
||||
count: result.rowCount,
|
||||
});
|
||||
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
console.error("❌ [getLocationHistory] 오류:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 싱글톤 인스턴스 생성 및 export
|
||||
|
|
|
|||
|
|
@ -503,8 +503,8 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
<SelectItem value="excel_upload">엑셀 업로드</SelectItem>
|
||||
<SelectItem value="barcode_scan">바코드 스캔</SelectItem>
|
||||
<SelectItem value="code_merge">코드 병합</SelectItem>
|
||||
<SelectItem value="geolocation">위치정보 가져오기</SelectItem>
|
||||
<SelectItem value="update_field">필드 값 변경</SelectItem>
|
||||
<SelectItem value="empty_vehicle">공차등록</SelectItem>
|
||||
<SelectItem value="operation_control">운행알림 및 종료</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
@ -1665,24 +1665,16 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
)}
|
||||
|
||||
{/* 위치정보 가져오기 설정 */}
|
||||
{(component.componentConfig?.action?.type || "save") === "geolocation" && (
|
||||
{(component.componentConfig?.action?.type || "save") === "empty_vehicle" && (
|
||||
<div className="mt-4 space-y-4 rounded-lg border bg-muted/50 p-4">
|
||||
<h4 className="text-sm font-medium text-foreground">📍 위치정보 설정</h4>
|
||||
<h4 className="text-sm font-medium text-foreground">🚛 공차등록 설정</h4>
|
||||
|
||||
{/* 테이블 선택 */}
|
||||
<div>
|
||||
<Label htmlFor="geolocation-table">
|
||||
저장할 테이블 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Label htmlFor="geolocation-table">저장할 테이블</Label>
|
||||
<Select
|
||||
value={config.action?.geolocationTableName || currentTableName || ""}
|
||||
onValueChange={(value) => {
|
||||
onUpdateProperty("componentConfig.action.geolocationTableName", value);
|
||||
onUpdateProperty("componentConfig.action.geolocationLatField", "");
|
||||
onUpdateProperty("componentConfig.action.geolocationLngField", "");
|
||||
onUpdateProperty("componentConfig.action.geolocationAccuracyField", "");
|
||||
onUpdateProperty("componentConfig.action.geolocationTimestampField", "");
|
||||
}}
|
||||
onValueChange={(value) => onUpdateProperty("componentConfig.action.geolocationTableName", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="테이블 선택" />
|
||||
|
|
@ -1695,32 +1687,29 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
위치 정보를 저장할 테이블 (기본: 현재 화면 테이블)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="geolocation-lat-field">
|
||||
위도 저장 필드 <span className="text-destructive">*</span>
|
||||
위도 필드 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="geolocation-lat-field"
|
||||
placeholder="예: latitude"
|
||||
value={config.action?.geolocationLatField || ""}
|
||||
placeholder="latitude"
|
||||
value={config.action?.geolocationLatField || "latitude"}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.geolocationLatField", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="geolocation-lng-field">
|
||||
경도 저장 필드 <span className="text-destructive">*</span>
|
||||
경도 필드 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="geolocation-lng-field"
|
||||
placeholder="예: longitude"
|
||||
value={config.action?.geolocationLngField || ""}
|
||||
placeholder="longitude"
|
||||
value={config.action?.geolocationLngField || "longitude"}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.geolocationLngField", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
|
|
@ -1729,20 +1718,20 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="geolocation-accuracy-field">정확도 저장 필드 (선택)</Label>
|
||||
<Label htmlFor="geolocation-accuracy-field">정확도 필드 (선택)</Label>
|
||||
<Input
|
||||
id="geolocation-accuracy-field"
|
||||
placeholder="예: accuracy"
|
||||
placeholder="accuracy"
|
||||
value={config.action?.geolocationAccuracyField || ""}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.geolocationAccuracyField", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="geolocation-timestamp-field">타임스탬프 저장 필드 (선택)</Label>
|
||||
<Label htmlFor="geolocation-timestamp-field">타임스탬프 필드 (선택)</Label>
|
||||
<Input
|
||||
id="geolocation-timestamp-field"
|
||||
placeholder="예: location_time"
|
||||
placeholder="location_time"
|
||||
value={config.action?.geolocationTimestampField || ""}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.geolocationTimestampField", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
|
|
@ -1753,7 +1742,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="geolocation-high-accuracy">고정밀 모드</Label>
|
||||
<p className="text-xs text-muted-foreground">GPS를 사용하여 더 정확한 위치 (배터리 소모 증가)</p>
|
||||
<p className="text-xs text-muted-foreground">GPS 사용 (배터리 소모 증가)</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="geolocation-high-accuracy"
|
||||
|
|
@ -1762,57 +1751,74 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="geolocation-auto-save">위치 가져온 후 자동 저장</Label>
|
||||
<p className="text-xs text-muted-foreground">위치 정보를 가져온 후 자동으로 폼을 저장합니다</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="geolocation-auto-save"
|
||||
checked={config.action?.geolocationAutoSave === true}
|
||||
onCheckedChange={(checked) => onUpdateProperty("componentConfig.action.geolocationAutoSave", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 첫 번째 추가 테이블 설정 (위치정보와 함께 상태 변경) */}
|
||||
<div className="mt-4 border-t pt-4">
|
||||
{/* 자동 저장 옵션 */}
|
||||
<div className="border-t pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="geolocation-update-field">추가 필드 변경 (테이블 1)</Label>
|
||||
<p className="text-xs text-muted-foreground">위치정보와 함께 다른 테이블의 필드 값을 변경합니다</p>
|
||||
<Label htmlFor="geolocation-auto-save">DB 자동 저장</Label>
|
||||
<p className="text-xs text-muted-foreground">위치 정보를 바로 DB에 저장</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="geolocation-update-field"
|
||||
checked={config.action?.geolocationUpdateField === true}
|
||||
onCheckedChange={(checked) => onUpdateProperty("componentConfig.action.geolocationUpdateField", checked)}
|
||||
id="geolocation-auto-save"
|
||||
checked={config.action?.geolocationAutoSave === true}
|
||||
onCheckedChange={(checked) => onUpdateProperty("componentConfig.action.geolocationAutoSave", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{config.action?.geolocationUpdateField && (
|
||||
|
||||
{config.action?.geolocationAutoSave && (
|
||||
<div className="mt-3 space-y-3 rounded-md bg-amber-50 p-3 dark:bg-amber-950">
|
||||
<div>
|
||||
<Label>대상 테이블</Label>
|
||||
<Select
|
||||
value={config.action?.geolocationExtraTableName || ""}
|
||||
onValueChange={(value) => onUpdateProperty("componentConfig.action.geolocationExtraTableName", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="테이블 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableTables.map((table) => (
|
||||
<SelectItem key={table.name} value={table.name} className="text-xs">
|
||||
{table.label || table.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label>변경할 필드</Label>
|
||||
<Label>키 필드 (WHERE 조건)</Label>
|
||||
<Input
|
||||
placeholder="예: status"
|
||||
placeholder="user_id"
|
||||
value={config.action?.geolocationKeyField || "user_id"}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.geolocationKeyField", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>키 값 소스</Label>
|
||||
<Select
|
||||
value={config.action?.geolocationKeySourceField || "__userId__"}
|
||||
onValueChange={(value) => onUpdateProperty("componentConfig.action.geolocationKeySourceField", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="소스 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__userId__" className="text-xs font-medium text-blue-600">
|
||||
🔑 로그인 사용자 ID
|
||||
</SelectItem>
|
||||
<SelectItem value="__companyCode__" className="text-xs font-medium text-blue-600">
|
||||
🏢 회사 코드
|
||||
</SelectItem>
|
||||
<SelectItem value="__userName__" className="text-xs font-medium text-blue-600">
|
||||
👤 사용자 이름
|
||||
</SelectItem>
|
||||
{tableColumns.length > 0 && (
|
||||
<>
|
||||
<SelectItem value="__divider__" disabled className="text-xs text-muted-foreground">
|
||||
── 폼 필드 ──
|
||||
</SelectItem>
|
||||
{tableColumns.map((col) => (
|
||||
<SelectItem key={col} value={col} className="text-xs">
|
||||
{col}
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 추가 필드 변경 (status 등) */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label>추가 변경 필드 (선택)</Label>
|
||||
<Input
|
||||
placeholder="status"
|
||||
value={config.action?.geolocationExtraField || ""}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.geolocationExtraField", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
|
|
@ -1821,203 +1827,15 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
<div>
|
||||
<Label>변경할 값</Label>
|
||||
<Input
|
||||
placeholder="예: active"
|
||||
placeholder="inactive"
|
||||
value={config.action?.geolocationExtraValue || ""}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.geolocationExtraValue", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label>키 필드 (대상 테이블)</Label>
|
||||
<Input
|
||||
placeholder="예: id"
|
||||
value={config.action?.geolocationExtraKeyField || ""}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.geolocationExtraKeyField", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>키 값 소스</Label>
|
||||
<Select
|
||||
value={config.action?.geolocationExtraKeySourceField || ""}
|
||||
onValueChange={(value) => onUpdateProperty("componentConfig.action.geolocationExtraKeySourceField", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="소스 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__userId__" className="text-xs font-medium text-blue-600">
|
||||
🔑 로그인 사용자 ID
|
||||
</SelectItem>
|
||||
<SelectItem value="__companyCode__" className="text-xs font-medium text-blue-600">
|
||||
🏢 회사 코드
|
||||
</SelectItem>
|
||||
<SelectItem value="__userName__" className="text-xs font-medium text-blue-600">
|
||||
👤 사용자 이름
|
||||
</SelectItem>
|
||||
{tableColumns.length > 0 && (
|
||||
<>
|
||||
<SelectItem value="__divider__" disabled className="text-xs text-muted-foreground">
|
||||
── 폼 필드 ──
|
||||
</SelectItem>
|
||||
{tableColumns.map((col) => (
|
||||
<SelectItem key={col} value={col} className="text-xs">
|
||||
{col}
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 두 번째 추가 테이블 설정 */}
|
||||
<div className="mt-4 border-t pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="geolocation-second-table">추가 필드 변경 (테이블 2)</Label>
|
||||
<p className="text-xs text-muted-foreground">두 번째 테이블의 필드 값도 함께 변경합니다</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="geolocation-second-table"
|
||||
checked={config.action?.geolocationSecondTableEnabled === true}
|
||||
onCheckedChange={(checked) => onUpdateProperty("componentConfig.action.geolocationSecondTableEnabled", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{config.action?.geolocationSecondTableEnabled && (
|
||||
<div className="mt-3 space-y-3 rounded-md bg-green-50 p-3 dark:bg-green-950">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label>대상 테이블</Label>
|
||||
<Select
|
||||
value={config.action?.geolocationSecondTableName || ""}
|
||||
onValueChange={(value) => onUpdateProperty("componentConfig.action.geolocationSecondTableName", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="테이블 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableTables.map((table) => (
|
||||
<SelectItem key={table.name} value={table.name} className="text-xs">
|
||||
{table.label || table.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>작업 모드</Label>
|
||||
<Select
|
||||
value={config.action?.geolocationSecondMode || "update"}
|
||||
onValueChange={(value) => onUpdateProperty("componentConfig.action.geolocationSecondMode", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="update" className="text-xs">UPDATE (기존 레코드 수정)</SelectItem>
|
||||
<SelectItem value="insert" className="text-xs">INSERT (새 레코드 생성)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label>{config.action?.geolocationSecondMode === "insert" ? "저장할 필드" : "변경할 필드"}</Label>
|
||||
<Input
|
||||
placeholder="예: status"
|
||||
value={config.action?.geolocationSecondField || ""}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.geolocationSecondField", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>{config.action?.geolocationSecondMode === "insert" ? "저장할 값" : "변경할 값"}</Label>
|
||||
<Input
|
||||
placeholder="예: inactive"
|
||||
value={config.action?.geolocationSecondValue || ""}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.geolocationSecondValue", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label>
|
||||
{config.action?.geolocationSecondMode === "insert"
|
||||
? "연결 필드 (대상 테이블)"
|
||||
: "키 필드 (대상 테이블)"}
|
||||
</Label>
|
||||
<Input
|
||||
placeholder={config.action?.geolocationSecondMode === "insert" ? "예: vehicle_id" : "예: id"}
|
||||
value={config.action?.geolocationSecondKeyField || ""}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.geolocationSecondKeyField", e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>키 값 소스</Label>
|
||||
<Select
|
||||
value={config.action?.geolocationSecondKeySourceField || ""}
|
||||
onValueChange={(value) => onUpdateProperty("componentConfig.action.geolocationSecondKeySourceField", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="소스 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__userId__" className="text-xs font-medium text-blue-600">
|
||||
🔑 로그인 사용자 ID
|
||||
</SelectItem>
|
||||
<SelectItem value="__companyCode__" className="text-xs font-medium text-blue-600">
|
||||
🏢 회사 코드
|
||||
</SelectItem>
|
||||
<SelectItem value="__userName__" className="text-xs font-medium text-blue-600">
|
||||
👤 사용자 이름
|
||||
</SelectItem>
|
||||
{tableColumns.length > 0 && (
|
||||
<>
|
||||
<SelectItem value="__divider__" disabled className="text-xs text-muted-foreground">
|
||||
── 폼 필드 ──
|
||||
</SelectItem>
|
||||
{tableColumns.map((col) => (
|
||||
<SelectItem key={col} value={col} className="text-xs">
|
||||
{col}
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config.action?.geolocationSecondMode === "insert" && (
|
||||
<div className="flex items-center justify-between rounded bg-green-100 p-2 dark:bg-green-900">
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-xs">위치정보도 함께 저장</Label>
|
||||
<p className="text-[10px] text-muted-foreground">위도/경도를 이 테이블에도 저장</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.action?.geolocationSecondInsertFields?.includeLocation === true}
|
||||
onCheckedChange={(checked) => onUpdateProperty("componentConfig.action.geolocationSecondInsertFields", {
|
||||
...config.action?.geolocationSecondInsertFields,
|
||||
includeLocation: checked
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-[10px] text-green-700 dark:text-green-300">
|
||||
{config.action?.geolocationSecondMode === "insert"
|
||||
? "새 레코드를 생성합니다. 연결 필드로 현재 폼 데이터와 연결됩니다."
|
||||
: "기존 레코드를 수정합니다. 키 필드로 레코드를 찾아 값을 변경합니다."}
|
||||
<p className="text-[10px] text-amber-700 dark:text-amber-300">
|
||||
위치 정보와 함께 status 같은 필드도 변경할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -2025,30 +1843,16 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
|
||||
<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>
|
||||
<br />
|
||||
1. 버튼을 클릭하면 브라우저가 위치 권한을 요청합니다
|
||||
<br />
|
||||
2. 사용자가 허용하면 현재 GPS 좌표를 가져옵니다
|
||||
<br />
|
||||
3. 위도/경도가 지정된 필드에 자동으로 입력됩니다
|
||||
<br />
|
||||
4. 추가 테이블 설정이 있으면 해당 테이블의 필드도 함께 변경됩니다
|
||||
<br />
|
||||
<br />
|
||||
<strong>예시:</strong> 위치정보 저장 + vehicles.status를 inactive로 변경
|
||||
<br />
|
||||
<br />
|
||||
<strong>참고:</strong> HTTPS 환경에서만 위치정보가 작동합니다.
|
||||
<strong>참고:</strong> HTTPS 환경에서만 작동합니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 필드 값 변경 설정 */}
|
||||
{(component.componentConfig?.action?.type || "save") === "update_field" && (
|
||||
{/* 운행알림 및 종료 설정 */}
|
||||
{(component.componentConfig?.action?.type || "save") === "operation_control" && (
|
||||
<div className="mt-4 space-y-4 rounded-lg border bg-muted/50 p-4">
|
||||
<h4 className="text-sm font-medium text-foreground">📝 필드 값 변경 설정</h4>
|
||||
<h4 className="text-sm font-medium text-foreground">🚗 운행알림 및 종료 설정</h4>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="update-table">
|
||||
|
|
@ -2272,15 +2076,70 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* 🆕 연속 위치 추적 설정 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label htmlFor="update-with-tracking">연속 위치 추적</Label>
|
||||
<p className="text-xs text-muted-foreground">10초마다 위치를 경로 테이블에 저장합니다</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="update-with-tracking"
|
||||
checked={config.action?.updateWithTracking === true}
|
||||
onCheckedChange={(checked) => onUpdateProperty("componentConfig.action.updateWithTracking", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{config.action?.updateWithTracking && (
|
||||
<div className="mt-3 space-y-3 rounded-md bg-green-50 p-3 dark:bg-green-950">
|
||||
<div>
|
||||
<Label>추적 모드 <span className="text-destructive">*</span></Label>
|
||||
<Select
|
||||
value={config.action?.updateTrackingMode || "start"}
|
||||
onValueChange={(value) => onUpdateProperty("componentConfig.action.updateTrackingMode", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="모드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="start">추적 시작 (운행 시작)</SelectItem>
|
||||
<SelectItem value="stop">추적 종료 (운행 종료)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{config.action?.updateTrackingMode === "start" && (
|
||||
<div>
|
||||
<Label>위치 저장 주기 (초)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="10"
|
||||
value={(config.action?.updateTrackingInterval || 10000) / 1000}
|
||||
onChange={(e) => onUpdateProperty("componentConfig.action.updateTrackingInterval", parseInt(e.target.value) * 1000 || 10000)}
|
||||
className="h-8 text-xs"
|
||||
min={5}
|
||||
max={300}
|
||||
/>
|
||||
<p className="mt-1 text-[10px] text-muted-foreground">5초 ~ 300초 사이로 설정 (기본: 10초)</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-[10px] text-green-700 dark:text-green-300">
|
||||
{config.action?.updateTrackingMode === "start"
|
||||
? "버튼 클릭 시 연속 위치 추적이 시작되고, vehicle_location_history 테이블에 경로가 저장됩니다."
|
||||
: "버튼 클릭 시 진행 중인 위치 추적이 종료됩니다."}
|
||||
</p>
|
||||
</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>
|
||||
<br />
|
||||
- 운행알림 버튼: status를 "active"로 + 위치정보 수집
|
||||
- 운행 시작: status를 "active"로 + 연속 추적 시작
|
||||
<br />
|
||||
- 출발 버튼: status를 "inactive"로 + 위치정보 수집
|
||||
- 운행 종료: status를 "completed"로 + 연속 추적 종료
|
||||
<br />
|
||||
- 완료 버튼: is_completed를 "Y"로 변경
|
||||
- 공차등록: status를 "inactive"로 + 1회성 위치정보 수집
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export type ButtonActionType =
|
|||
| "edit" // 편집
|
||||
| "copy" // 복사 (품목코드 초기화)
|
||||
| "navigate" // 페이지 이동
|
||||
| "openModalWithData" // 🆕 데이터를 전달하면서 모달 열기
|
||||
| "openModalWithData" // 데이터를 전달하면서 모달 열기
|
||||
| "modal" // 모달 열기
|
||||
| "control" // 제어 흐름
|
||||
| "view_table_history" // 테이블 이력 보기
|
||||
|
|
@ -24,10 +24,10 @@ export type ButtonActionType =
|
|||
| "excel_upload" // 엑셀 업로드
|
||||
| "barcode_scan" // 바코드 스캔
|
||||
| "code_merge" // 코드 병합
|
||||
| "geolocation" // 위치정보 가져오기
|
||||
| "empty_vehicle" // 공차등록 (위치 수집 + 상태 변경)
|
||||
| "operation_control" // 운행알림 및 종료 (위치 수집 + 상태 변경 + 연속 추적)
|
||||
| "swap_fields" // 필드 값 교환 (출발지 ↔ 목적지)
|
||||
| "update_field" // 특정 필드 값 변경 (예: status를 active로)
|
||||
| "transferData"; // 🆕 데이터 전달 (컴포넌트 간 or 화면 간)
|
||||
| "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; // 타임스탬프 저장 필드 (선택)
|
||||
|
||||
|
|
@ -326,15 +350,15 @@ export class ButtonActionExecutor {
|
|||
case "transferData":
|
||||
return await this.handleTransferData(config, context);
|
||||
|
||||
case "geolocation":
|
||||
return await this.handleGeolocation(config, context);
|
||||
case "empty_vehicle":
|
||||
return await this.handleEmptyVehicle(config, context);
|
||||
|
||||
case "operation_control":
|
||||
return await this.handleOperationControl(config, context);
|
||||
|
||||
case "swap_fields":
|
||||
return await this.handleSwapFields(config, context);
|
||||
|
||||
case "update_field":
|
||||
return await this.handleUpdateField(config, context);
|
||||
|
||||
default:
|
||||
console.warn(`지원되지 않는 액션 타입: ${config.type}`);
|
||||
return false;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 전달 액션 처리 (분할 패널에서 좌측 → 우측 데이터 전달)
|
||||
*/
|
||||
|
|
@ -3398,19 +3674,12 @@ export class ButtonActionExecutor {
|
|||
}
|
||||
|
||||
/**
|
||||
* 위치정보 가져오기 액션 처리
|
||||
* 공차등록 액션 처리
|
||||
* - 위치 수집 + 상태 변경 (예: status → inactive)
|
||||
*/
|
||||
private static async handleGeolocation(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||
private static async handleEmptyVehicle(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 +3688,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 +3719,6 @@ export class ButtonActionExecutor {
|
|||
[lngField]: longitude,
|
||||
};
|
||||
|
||||
// 선택적 필드들
|
||||
if (config.geolocationAccuracyField && accuracy !== null) {
|
||||
updates[config.geolocationAccuracyField] = accuracy;
|
||||
}
|
||||
|
|
@ -3469,289 +3726,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 +3877,36 @@ export class ButtonActionExecutor {
|
|||
/**
|
||||
* 필드 값 변경 액션 처리 (예: status를 active로 변경)
|
||||
* 🆕 위치정보 수집 기능 추가
|
||||
* 🆕 연속 위치 추적 기능 추가
|
||||
*/
|
||||
private static async handleUpdateField(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||
/**
|
||||
* 운행알림 및 종료 액션 처리
|
||||
* - 위치 수집 + 상태 변경 + 연속 추적 (시작/종료)
|
||||
*/
|
||||
private static async handleOperationControl(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 +4217,12 @@ export const DEFAULT_BUTTON_ACTIONS: Record<ButtonActionType, Partial<ButtonActi
|
|||
type: "edit",
|
||||
successMessage: "편집되었습니다.",
|
||||
},
|
||||
copy: {
|
||||
type: "copy",
|
||||
confirmMessage: "복사하시겠습니까?",
|
||||
successMessage: "복사되었습니다.",
|
||||
errorMessage: "복사 중 오류가 발생했습니다.",
|
||||
},
|
||||
control: {
|
||||
type: "control",
|
||||
},
|
||||
|
|
@ -4191,21 +4261,36 @@ export const DEFAULT_BUTTON_ACTIONS: Record<ButtonActionType, Partial<ButtonActi
|
|||
successMessage: "데이터가 전달되었습니다.",
|
||||
errorMessage: "데이터 전달 중 오류가 발생했습니다.",
|
||||
},
|
||||
geolocation: {
|
||||
type: "geolocation",
|
||||
empty_vehicle: {
|
||||
type: "empty_vehicle",
|
||||
geolocationHighAccuracy: true,
|
||||
geolocationTimeout: 10000,
|
||||
geolocationMaxAge: 0,
|
||||
geolocationAutoSave: false,
|
||||
confirmMessage: "현재 위치 정보를 가져오시겠습니까?",
|
||||
successMessage: "위치 정보를 가져왔습니다.",
|
||||
errorMessage: "위치 정보를 가져오는 중 오류가 발생했습니다.",
|
||||
geolocationLatField: "latitude",
|
||||
geolocationLngField: "longitude",
|
||||
geolocationAutoSave: true,
|
||||
geolocationKeyField: "user_id",
|
||||
geolocationKeySourceField: "__userId__",
|
||||
geolocationExtraField: "status",
|
||||
geolocationExtraValue: "inactive",
|
||||
successMessage: "공차등록이 완료되었습니다.",
|
||||
errorMessage: "공차등록 중 오류가 발생했습니다.",
|
||||
},
|
||||
update_field: {
|
||||
type: "update_field",
|
||||
operation_control: {
|
||||
type: "operation_control",
|
||||
updateAutoSave: true,
|
||||
confirmMessage: "상태를 변경하시겠습니까?",
|
||||
successMessage: "상태가 변경되었습니다.",
|
||||
errorMessage: "상태 변경 중 오류가 발생했습니다.",
|
||||
updateWithGeolocation: true,
|
||||
updateGeolocationLatField: "latitude",
|
||||
updateGeolocationLngField: "longitude",
|
||||
updateKeyField: "user_id",
|
||||
updateKeySourceField: "__userId__",
|
||||
confirmMessage: "운행 상태를 변경하시겠습니까?",
|
||||
successMessage: "운행 상태가 변경되었습니다.",
|
||||
errorMessage: "운행 상태 변경 중 오류가 발생했습니다.",
|
||||
},
|
||||
swap_fields: {
|
||||
type: "swap_fields",
|
||||
successMessage: "필드 값이 교환되었습니다.",
|
||||
errorMessage: "필드 값 교환 중 오류가 발생했습니다.",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue