diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts
index 51f3bf7b..31287e1e 100644
--- a/frontend/components/admin/dashboard/types.ts
+++ b/frontend/components/admin/dashboard/types.ts
@@ -390,9 +390,11 @@ export interface RowDetailPopupConfig {
// 추가 데이터 조회 설정
additionalQuery?: {
enabled: boolean;
+ queryMode?: "table" | "custom"; // 조회 모드: table(테이블 조회), custom(커스텀 쿼리)
tableName: string; // 조회할 테이블명 (예: vehicles)
matchColumn: string; // 매칭할 컬럼 (예: id)
sourceColumn?: string; // 클릭한 행에서 가져올 컬럼 (기본: matchColumn과 동일)
+ customQuery?: string; // 커스텀 쿼리 ({id}, {vehicle_number} 등 파라미터 사용)
// 팝업에 표시할 컬럼 목록 (비어있으면 전체 표시)
displayColumns?: DisplayColumnConfig[];
};
diff --git a/frontend/components/admin/dashboard/widget-sections/ListWidgetSection.tsx b/frontend/components/admin/dashboard/widget-sections/ListWidgetSection.tsx
index b10057cf..a7186d50 100644
--- a/frontend/components/admin/dashboard/widget-sections/ListWidgetSection.tsx
+++ b/frontend/components/admin/dashboard/widget-sections/ListWidgetSection.tsx
@@ -158,7 +158,7 @@ export function ListWidgetSection({ queryResult, config, onConfigChange }: ListW
checked={popupConfig.additionalQuery?.enabled || false}
onCheckedChange={(enabled) =>
updatePopupConfig({
- additionalQuery: { ...popupConfig.additionalQuery, enabled, tableName: "", matchColumn: "" },
+ additionalQuery: { ...popupConfig.additionalQuery, enabled, queryMode: "table", tableName: "", matchColumn: "" },
})
}
aria-label="추가 데이터 조회 활성화"
@@ -167,116 +167,230 @@ export function ListWidgetSection({ queryResult, config, onConfigChange }: ListW
{popupConfig.additionalQuery?.enabled && (
+ {/* 조회 모드 선택 */}
-
-
+
+
-
-
-
- updatePopupConfig({
- additionalQuery: { ...popupConfig.additionalQuery!, matchColumn: e.target.value },
- })
- }
- placeholder="id"
- className="mt-1 h-8 text-xs"
- />
-
-
-
-
- updatePopupConfig({
- additionalQuery: { ...popupConfig.additionalQuery!, sourceColumn: e.target.value },
- })
- }
- placeholder="비워두면 매칭 컬럼과 동일"
- className="mt-1 h-8 text-xs"
- />
+ >
+
+
+
+
+ 테이블 조회
+ 커스텀 쿼리
+
+
- {/* 표시할 컬럼 선택 (다중 선택 + 라벨 편집) */}
+ {/* 테이블 조회 모드 */}
+ {(popupConfig.additionalQuery?.queryMode || "table") === "table" && (
+ <>
+
+
+
+ updatePopupConfig({
+ additionalQuery: { ...popupConfig.additionalQuery!, tableName: e.target.value },
+ })
+ }
+ placeholder="vehicles"
+ className="mt-1 h-8 text-xs"
+ />
+
+
+
+
+ updatePopupConfig({
+ additionalQuery: { ...popupConfig.additionalQuery!, matchColumn: e.target.value },
+ })
+ }
+ placeholder="id"
+ className="mt-1 h-8 text-xs"
+ />
+
+
+
+
+ updatePopupConfig({
+ additionalQuery: { ...popupConfig.additionalQuery!, sourceColumn: e.target.value },
+ })
+ }
+ placeholder="비워두면 매칭 컬럼과 동일"
+ className="mt-1 h-8 text-xs"
+ />
+
+ >
+ )}
+
+ {/* 커스텀 쿼리 모드 */}
+ {popupConfig.additionalQuery?.queryMode === "custom" && (
+ <>
+
+
+
+ updatePopupConfig({
+ additionalQuery: { ...popupConfig.additionalQuery!, sourceColumn: e.target.value },
+ })
+ }
+ placeholder="id"
+ className="mt-1 h-8 text-xs"
+ />
+
쿼리에서 사용할 파라미터 컬럼
+
+
+ >
+ )}
+
+ {/* 표시할 컬럼 선택 - 테이블 모드와 커스텀 쿼리 모드 분기 */}
-
-
-
-
-
-
- 컬럼 선택
-
-
-
- {/* 쿼리 결과 컬럼 목록 */}
- {queryResult?.columns.map((col) => {
- const currentColumns = popupConfig.additionalQuery?.displayColumns || [];
- const existingConfig = currentColumns.find((c) =>
- typeof c === 'object' ? c.column === col : c === col
- );
- const isSelected = !!existingConfig;
- return (
-
{
- const newColumns = isSelected
- ? currentColumns.filter((c) =>
- typeof c === 'object' ? c.column !== col : c !== col
- )
- : [...currentColumns, { column: col, label: col } as DisplayColumnConfig];
+
+ {/* 테이블 모드: 기존 쿼리 결과에서 선택 */}
+ {popupConfig.additionalQuery?.queryMode !== "custom" && (
+ <>
+
+
+
+
+
+
+ 컬럼 선택
+
- );
- })}
- {(!queryResult?.columns || queryResult.columns.length === 0) && (
-
- 쿼리를 먼저 실행해주세요
-
+ 초기화
+
+
+
+ {/* 쿼리 결과 컬럼 목록 */}
+ {queryResult?.columns.map((col) => {
+ const currentColumns = popupConfig.additionalQuery?.displayColumns || [];
+ const existingConfig = currentColumns.find((c) =>
+ typeof c === 'object' ? c.column === col : c === col
+ );
+ const isSelected = !!existingConfig;
+ return (
+
{
+ const newColumns = isSelected
+ ? currentColumns.filter((c) =>
+ typeof c === 'object' ? c.column !== col : c !== col
+ )
+ : [...currentColumns, { column: col, label: col } as DisplayColumnConfig];
+ updatePopupConfig({
+ additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: newColumns },
+ });
+ }}
+ >
+
+ {col}
+
+ );
+ })}
+ {(!queryResult?.columns || queryResult.columns.length === 0) && (
+
+ 쿼리를 먼저 실행해주세요
+
+ )}
+
+
+
+
비워두면 모든 컬럼이 표시됩니다
+ >
+ )}
+
+ {/* 커스텀 쿼리 모드: 직접 입력 방식 */}
+ {popupConfig.additionalQuery?.queryMode === "custom" && (
+ <>
+
+ 커스텀 쿼리의 결과 컬럼이 자동으로 표시됩니다.
+ 쿼리에서 AS "라벨명" 형태로 alias를 지정하면 해당 라벨로 표시됩니다.
+
+
+
+ {(popupConfig.additionalQuery?.displayColumns?.length || 0) > 0 && (
+
)}
-
-
-
비워두면 모든 컬럼이 표시됩니다
+ >
+ )}
- {/* 선택된 컬럼 라벨 편집 */}
- {(popupConfig.additionalQuery?.displayColumns?.length || 0) > 0 && (
+ {/* 선택된 컬럼 라벨 편집 (테이블 모드) */}
+ {popupConfig.additionalQuery?.queryMode !== "custom" && (popupConfig.additionalQuery?.displayColumns?.length || 0) > 0 && (
@@ -321,6 +435,63 @@ export function ListWidgetSection({ queryResult, config, onConfigChange }: ListW
)}
+
+ {/* 커스텀 쿼리 모드: 직접 입력 컬럼 편집 */}
+ {popupConfig.additionalQuery?.queryMode === "custom" && (popupConfig.additionalQuery?.displayColumns?.length || 0) > 0 && (
+
+
+
커스텀 쿼리 결과의 컬럼명을 직접 입력하세요
+
+ {popupConfig.additionalQuery?.displayColumns?.map((colConfig, index) => {
+ const column = typeof colConfig === 'object' ? colConfig.column : colConfig;
+ const label = typeof colConfig === 'object' ? colConfig.label : colConfig;
+ return (
+
+ {
+ const newColumns = [...(popupConfig.additionalQuery?.displayColumns || [])];
+ newColumns[index] = { column: e.target.value, label: label || e.target.value };
+ updatePopupConfig({
+ additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: newColumns },
+ });
+ }}
+ placeholder="컬럼명 (쿼리 결과)"
+ className="h-7 flex-1 text-xs"
+ />
+ {
+ const newColumns = [...(popupConfig.additionalQuery?.displayColumns || [])];
+ newColumns[index] = { column, label: e.target.value };
+ updatePopupConfig({
+ additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: newColumns },
+ });
+ }}
+ placeholder="표시 라벨"
+ className="h-7 flex-1 text-xs"
+ />
+
+
+ );
+ })}
+
+
+ )}
)}
diff --git a/frontend/components/admin/dashboard/widgets/ListWidget.tsx b/frontend/components/admin/dashboard/widgets/ListWidget.tsx
index 8a8541bc..fbf55750 100644
--- a/frontend/components/admin/dashboard/widgets/ListWidget.tsx
+++ b/frontend/components/admin/dashboard/widgets/ListWidget.tsx
@@ -64,22 +64,35 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
// 추가 데이터 조회 설정이 있으면 실행
const additionalQuery = config.rowDetailPopup?.additionalQuery;
- if (additionalQuery?.enabled && additionalQuery.tableName && additionalQuery.matchColumn) {
- const sourceColumn = additionalQuery.sourceColumn || additionalQuery.matchColumn;
- const matchValue = row[sourceColumn];
-
- if (matchValue !== undefined && matchValue !== null) {
+ if (additionalQuery?.enabled) {
+ const queryMode = additionalQuery.queryMode || "table";
+
+ // 커스텀 쿼리 모드
+ if (queryMode === "custom" && additionalQuery.customQuery) {
setDetailPopupLoading(true);
try {
- const query = `
- SELECT *
- FROM ${additionalQuery.tableName}
- WHERE ${additionalQuery.matchColumn} = '${matchValue}'
- LIMIT 1;
- `;
+ // 쿼리에서 {컬럼명} 형태의 파라미터를 실제 값으로 치환
+ let query = additionalQuery.customQuery;
+ // console.log("🔍 [ListWidget] 커스텀 쿼리 파라미터 치환 시작");
+ // console.log("🔍 [ListWidget] 클릭한 행 데이터:", row);
+ // console.log("🔍 [ListWidget] 행 컬럼 목록:", Object.keys(row));
+
+ Object.keys(row).forEach((key) => {
+ const value = row[key];
+ const placeholder = new RegExp(`\\{${key}\\}`, "g");
+ // SQL 인젝션 방지를 위해 값 이스케이프
+ const safeValue = typeof value === "string"
+ ? value.replace(/'/g, "''")
+ : value;
+ query = query.replace(placeholder, String(safeValue ?? ""));
+ // console.log(`🔍 [ListWidget] 치환: {${key}} → ${safeValue}`);
+ });
+ // console.log("🔍 [ListWidget] 최종 쿼리:", query);
+
const { dashboardApi } = await import("@/lib/api/dashboard");
const result = await dashboardApi.executeQuery(query);
+ // console.log("🔍 [ListWidget] 쿼리 결과:", result);
if (result.success && result.rows.length > 0) {
setAdditionalDetailData(result.rows[0]);
@@ -87,12 +100,43 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
setAdditionalDetailData({});
}
} catch (error) {
- console.error("추가 데이터 로드 실패:", error);
+ console.error("커스텀 쿼리 실행 실패:", error);
setAdditionalDetailData({});
} finally {
setDetailPopupLoading(false);
}
}
+ // 테이블 조회 모드
+ else if (queryMode === "table" && additionalQuery.tableName && additionalQuery.matchColumn) {
+ const sourceColumn = additionalQuery.sourceColumn || additionalQuery.matchColumn;
+ const matchValue = row[sourceColumn];
+
+ if (matchValue !== undefined && matchValue !== null) {
+ setDetailPopupLoading(true);
+ try {
+ const query = `
+ SELECT *
+ FROM ${additionalQuery.tableName}
+ WHERE ${additionalQuery.matchColumn} = '${matchValue}'
+ LIMIT 1;
+ `;
+
+ const { dashboardApi } = await import("@/lib/api/dashboard");
+ const result = await dashboardApi.executeQuery(query);
+
+ if (result.success && result.rows.length > 0) {
+ setAdditionalDetailData(result.rows[0]);
+ } else {
+ setAdditionalDetailData({});
+ }
+ } catch (error) {
+ console.error("추가 데이터 로드 실패:", error);
+ setAdditionalDetailData({});
+ } finally {
+ setDetailPopupLoading(false);
+ }
+ }
+ }
}
},
[config.rowDetailPopup],
@@ -190,22 +234,34 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
const getDefaultFieldGroups = (row: Record
, additional: Record | null): FieldGroup[] => {
const groups: FieldGroup[] = [];
const displayColumns = config.rowDetailPopup?.additionalQuery?.displayColumns;
+ const queryMode = config.rowDetailPopup?.additionalQuery?.queryMode || "table";
+
+ // 커스텀 쿼리 모드일 때는 additional 데이터를 우선 사용
+ // row와 additional을 병합하되, 커스텀 쿼리 결과(additional)가 우선
+ const mergedData = queryMode === "custom" && additional && Object.keys(additional).length > 0
+ ? { ...row, ...additional } // additional이 row를 덮어씀
+ : row;
// 기본 정보 그룹 - displayColumns가 있으면 해당 컬럼만, 없으면 전체
let basicFields: { column: string; label: string }[] = [];
if (displayColumns && displayColumns.length > 0) {
// DisplayColumnConfig 형식 지원
+ // 커스텀 쿼리 모드일 때는 mergedData에서 컬럼 확인
basicFields = displayColumns
.map((colConfig) => {
const column = typeof colConfig === 'object' ? colConfig.column : colConfig;
const label = typeof colConfig === 'object' ? colConfig.label : colConfig;
return { column, label };
})
- .filter((item) => item.column in row);
+ .filter((item) => item.column in mergedData);
} else {
- // 전체 컬럼
- basicFields = Object.keys(row).map((key) => ({ column: key, label: key }));
+ // 전체 컬럼 - 커스텀 쿼리 모드일 때는 additional 컬럼만 표시
+ if (queryMode === "custom" && additional && Object.keys(additional).length > 0) {
+ basicFields = Object.keys(additional).map((key) => ({ column: key, label: key }));
+ } else {
+ basicFields = Object.keys(row).map((key) => ({ column: key, label: key }));
+ }
}
groups.push({
@@ -220,8 +276,8 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
})),
});
- // 추가 데이터가 있고 vehicles 테이블인 경우 운행/공차 정보 추가
- if (additional && Object.keys(additional).length > 0) {
+ // 추가 데이터가 있고 vehicles 테이블인 경우 운행/공차 정보 추가 (테이블 모드일 때만)
+ if (queryMode === "table" && additional && Object.keys(additional).length > 0) {
// 운행 정보
if (additional.last_trip_start || additional.last_trip_end) {
groups.push({
diff --git a/frontend/components/dashboard/widgets/ListTestWidget.tsx b/frontend/components/dashboard/widgets/ListTestWidget.tsx
index c1f34e23..bc6b3299 100644
--- a/frontend/components/dashboard/widgets/ListTestWidget.tsx
+++ b/frontend/components/dashboard/widgets/ListTestWidget.tsx
@@ -96,22 +96,35 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
// 추가 데이터 조회 설정이 있으면 실행
const additionalQuery = config.rowDetailPopup?.additionalQuery;
- if (additionalQuery?.enabled && additionalQuery.tableName && additionalQuery.matchColumn) {
- const sourceColumn = additionalQuery.sourceColumn || additionalQuery.matchColumn;
- const matchValue = row[sourceColumn];
-
- if (matchValue !== undefined && matchValue !== null) {
+ if (additionalQuery?.enabled) {
+ const queryMode = additionalQuery.queryMode || "table";
+
+ // 커스텀 쿼리 모드
+ if (queryMode === "custom" && additionalQuery.customQuery) {
setDetailPopupLoading(true);
try {
- const query = `
- SELECT *
- FROM ${additionalQuery.tableName}
- WHERE ${additionalQuery.matchColumn} = '${matchValue}'
- LIMIT 1;
- `;
+ // 쿼리에서 {컬럼명} 형태의 파라미터를 실제 값으로 치환
+ let query = additionalQuery.customQuery;
+ // console.log("🔍 [ListTestWidget] 커스텀 쿼리 파라미터 치환 시작");
+ // console.log("🔍 [ListTestWidget] 클릭한 행 데이터:", row);
+ // console.log("🔍 [ListTestWidget] 행 컬럼 목록:", Object.keys(row));
+
+ Object.keys(row).forEach((key) => {
+ const value = row[key];
+ const placeholder = new RegExp(`\\{${key}\\}`, "g");
+ // SQL 인젝션 방지를 위해 값 이스케이프
+ const safeValue = typeof value === "string"
+ ? value.replace(/'/g, "''")
+ : value;
+ query = query.replace(placeholder, String(safeValue ?? ""));
+ // console.log(`🔍 [ListTestWidget] 치환: {${key}} → ${safeValue}`);
+ });
+ // console.log("🔍 [ListTestWidget] 최종 쿼리:", query);
+
const { dashboardApi } = await import("@/lib/api/dashboard");
const result = await dashboardApi.executeQuery(query);
+ // console.log("🔍 [ListTestWidget] 쿼리 결과:", result);
if (result.success && result.rows.length > 0) {
setAdditionalDetailData(result.rows[0]);
@@ -119,12 +132,43 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
setAdditionalDetailData({});
}
} catch (err) {
- console.error("추가 데이터 로드 실패:", err);
+ console.error("커스텀 쿼리 실행 실패:", err);
setAdditionalDetailData({});
} finally {
setDetailPopupLoading(false);
}
}
+ // 테이블 조회 모드
+ else if (queryMode === "table" && additionalQuery.tableName && additionalQuery.matchColumn) {
+ const sourceColumn = additionalQuery.sourceColumn || additionalQuery.matchColumn;
+ const matchValue = row[sourceColumn];
+
+ if (matchValue !== undefined && matchValue !== null) {
+ setDetailPopupLoading(true);
+ try {
+ const query = `
+ SELECT *
+ FROM ${additionalQuery.tableName}
+ WHERE ${additionalQuery.matchColumn} = '${matchValue}'
+ LIMIT 1;
+ `;
+
+ const { dashboardApi } = await import("@/lib/api/dashboard");
+ const result = await dashboardApi.executeQuery(query);
+
+ if (result.success && result.rows.length > 0) {
+ setAdditionalDetailData(result.rows[0]);
+ } else {
+ setAdditionalDetailData({});
+ }
+ } catch (err) {
+ console.error("추가 데이터 로드 실패:", err);
+ setAdditionalDetailData({});
+ } finally {
+ setDetailPopupLoading(false);
+ }
+ }
+ }
}
},
[config.rowDetailPopup],
@@ -222,13 +266,21 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
const getDefaultFieldGroups = (row: Record, additional: Record | null): FieldGroup[] => {
const groups: FieldGroup[] = [];
const displayColumns = config.rowDetailPopup?.additionalQuery?.displayColumns;
+ const queryMode = config.rowDetailPopup?.additionalQuery?.queryMode || "table";
+
+ // 커스텀 쿼리 모드일 때는 additional 데이터를 우선 사용
+ // row와 additional을 병합하되, 커스텀 쿼리 결과(additional)가 우선
+ const mergedData = queryMode === "custom" && additional && Object.keys(additional).length > 0
+ ? { ...row, ...additional } // additional이 row를 덮어씀
+ : row;
// 기본 정보 그룹 - displayColumns가 있으면 해당 컬럼만, 없으면 전체
- const allKeys = Object.keys(row).filter((key) => !key.startsWith("_")); // _source 등 내부 필드 제외
+ const allKeys = Object.keys(mergedData).filter((key) => !key.startsWith("_")); // _source 등 내부 필드 제외
let basicFields: { column: string; label: string }[] = [];
if (displayColumns && displayColumns.length > 0) {
// DisplayColumnConfig 형식 지원
+ // 커스텀 쿼리 모드일 때는 mergedData에서 컬럼 확인
basicFields = displayColumns
.map((colConfig) => {
const column = typeof colConfig === 'object' ? colConfig.column : colConfig;
@@ -237,8 +289,14 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
})
.filter((item) => allKeys.includes(item.column));
} else {
- // 전체 컬럼
- basicFields = allKeys.map((key) => ({ column: key, label: key }));
+ // 전체 컬럼 - 커스텀 쿼리 모드일 때는 additional 컬럼만 표시
+ if (queryMode === "custom" && additional && Object.keys(additional).length > 0) {
+ basicFields = Object.keys(additional)
+ .filter((key) => !key.startsWith("_"))
+ .map((key) => ({ column: key, label: key }));
+ } else {
+ basicFields = allKeys.map((key) => ({ column: key, label: key }));
+ }
}
groups.push({
@@ -253,8 +311,8 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
})),
});
- // 추가 데이터가 있고 vehicles 테이블인 경우 운행/공차 정보 추가
- if (additional && Object.keys(additional).length > 0) {
+ // 추가 데이터가 있고 vehicles 테이블인 경우 운행/공차 정보 추가 (테이블 모드일 때만)
+ if (queryMode === "table" && additional && Object.keys(additional).length > 0) {
// 운행 정보
if (additional.last_trip_start || additional.last_trip_end) {
groups.push({
diff --git a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx
index bc7b995d..b3c9e2fb 100644
--- a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx
+++ b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx
@@ -203,11 +203,15 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
setTripInfoLoading(identifier);
try {
- // user_id 또는 vehicle_number로 조회
+ // user_id 또는 vehicle_number로 조회 (시간은 KST로 변환)
const query = `SELECT
id, vehicle_number, user_id,
- last_trip_start, last_trip_end, last_trip_distance, last_trip_time,
- last_empty_start, last_empty_end, last_empty_distance, last_empty_time,
+ (last_trip_start AT TIME ZONE 'Asia/Seoul')::timestamp as last_trip_start,
+ (last_trip_end AT TIME ZONE 'Asia/Seoul')::timestamp as last_trip_end,
+ last_trip_distance, last_trip_time,
+ (last_empty_start AT TIME ZONE 'Asia/Seoul')::timestamp as last_empty_start,
+ (last_empty_end AT TIME ZONE 'Asia/Seoul')::timestamp as last_empty_end,
+ last_empty_distance, last_empty_time,
departure, arrival, status
FROM vehicles
WHERE user_id = '${identifier}'
@@ -277,12 +281,16 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
if (identifiers.length === 0) return;
try {
- // 모든 마커의 운행/공차 정보를 한 번에 조회
+ // 모든 마커의 운행/공차 정보를 한 번에 조회 (시간은 KST로 변환)
const placeholders = identifiers.map((_, i) => `$${i + 1}`).join(", ");
const query = `SELECT
id, vehicle_number, user_id,
- last_trip_start, last_trip_end, last_trip_distance, last_trip_time,
- last_empty_start, last_empty_end, last_empty_distance, last_empty_time,
+ (last_trip_start AT TIME ZONE 'Asia/Seoul')::timestamp as last_trip_start,
+ (last_trip_end AT TIME ZONE 'Asia/Seoul')::timestamp as last_trip_end,
+ last_trip_distance, last_trip_time,
+ (last_empty_start AT TIME ZONE 'Asia/Seoul')::timestamp as last_empty_start,
+ (last_empty_end AT TIME ZONE 'Asia/Seoul')::timestamp as last_empty_end,
+ last_empty_distance, last_empty_time,
departure, arrival, status
FROM vehicles
WHERE user_id IN (${identifiers.map(id => `'${id}'`).join(", ")})