지도 수정 및 경로확인 가능하게
This commit is contained in:
parent
4d9f010ac5
commit
dbf6cfc995
|
|
@ -43,6 +43,7 @@ const Marker = dynamic(() => import("react-leaflet").then((mod) => mod.Marker),
|
||||||
const Popup = dynamic(() => import("react-leaflet").then((mod) => mod.Popup), { ssr: false });
|
const Popup = dynamic(() => import("react-leaflet").then((mod) => mod.Popup), { ssr: false });
|
||||||
const Polygon = dynamic(() => import("react-leaflet").then((mod) => mod.Polygon), { ssr: false });
|
const Polygon = dynamic(() => import("react-leaflet").then((mod) => mod.Polygon), { ssr: false });
|
||||||
const GeoJSON = dynamic(() => import("react-leaflet").then((mod) => mod.GeoJSON), { ssr: false });
|
const GeoJSON = dynamic(() => import("react-leaflet").then((mod) => mod.GeoJSON), { ssr: false });
|
||||||
|
const Polyline = dynamic(() => import("react-leaflet").then((mod) => mod.Polyline), { ssr: false });
|
||||||
|
|
||||||
// 브이월드 API 키
|
// 브이월드 API 키
|
||||||
const VWORLD_API_KEY = "97AD30D5-FDC4-3481-99C3-158E36422033";
|
const VWORLD_API_KEY = "97AD30D5-FDC4-3481-99C3-158E36422033";
|
||||||
|
|
@ -78,6 +79,13 @@ interface PolygonData {
|
||||||
opacity?: number; // 투명도 (0.0 ~ 1.0)
|
opacity?: number; // 투명도 (0.0 ~ 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 이동경로 타입
|
||||||
|
interface RoutePoint {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
recordedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
const [markers, setMarkers] = useState<MarkerData[]>([]);
|
const [markers, setMarkers] = useState<MarkerData[]>([]);
|
||||||
const prevMarkersRef = useRef<MarkerData[]>([]); // 이전 마커 위치 저장 (useRef 사용)
|
const prevMarkersRef = useRef<MarkerData[]>([]); // 이전 마커 위치 저장 (useRef 사용)
|
||||||
|
|
@ -86,6 +94,12 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [geoJsonData, setGeoJsonData] = useState<any>(null);
|
const [geoJsonData, setGeoJsonData] = useState<any>(null);
|
||||||
const [lastRefreshTime, setLastRefreshTime] = useState<Date | null>(null);
|
const [lastRefreshTime, setLastRefreshTime] = useState<Date | null>(null);
|
||||||
|
|
||||||
|
// 이동경로 상태
|
||||||
|
const [routePoints, setRoutePoints] = useState<RoutePoint[]>([]);
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
|
||||||
|
const [routeLoading, setRouteLoading] = useState(false);
|
||||||
|
const [routeDate, setRouteDate] = useState<string>(new Date().toISOString().split('T')[0]); // YYYY-MM-DD 형식
|
||||||
|
|
||||||
// dataSources를 useMemo로 추출 (circular reference 방지)
|
// dataSources를 useMemo로 추출 (circular reference 방지)
|
||||||
const dataSources = useMemo(() => {
|
const dataSources = useMemo(() => {
|
||||||
|
|
@ -107,6 +121,70 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
return heading;
|
return heading;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 이동경로 로드 함수
|
||||||
|
const loadRoute = useCallback(async (userId: string, date?: string) => {
|
||||||
|
if (!userId) {
|
||||||
|
console.log("🛣️ 이동경로 조회 불가: userId 없음");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRouteLoading(true);
|
||||||
|
setSelectedUserId(userId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 선택한 날짜 기준으로 이동경로 조회
|
||||||
|
const targetDate = date || routeDate;
|
||||||
|
const startOfDay = `${targetDate}T00:00:00.000Z`;
|
||||||
|
const endOfDay = `${targetDate}T23:59:59.999Z`;
|
||||||
|
|
||||||
|
const query = `SELECT latitude, longitude, recorded_at
|
||||||
|
FROM vehicle_location_history
|
||||||
|
WHERE user_id = '${userId}'
|
||||||
|
AND recorded_at >= '${startOfDay}'
|
||||||
|
AND recorded_at <= '${endOfDay}'
|
||||||
|
ORDER BY recorded_at ASC`;
|
||||||
|
|
||||||
|
console.log("🛣️ 이동경로 쿼리:", query);
|
||||||
|
|
||||||
|
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${typeof window !== "undefined" ? localStorage.getItem("authToken") || "" : ""}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ query }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success && result.data.rows.length > 0) {
|
||||||
|
const points: RoutePoint[] = result.data.rows.map((row: any) => ({
|
||||||
|
lat: parseFloat(row.latitude),
|
||||||
|
lng: parseFloat(row.longitude),
|
||||||
|
recordedAt: row.recorded_at,
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(`🛣️ 이동경로 ${points.length}개 포인트 로드 완료`);
|
||||||
|
setRoutePoints(points);
|
||||||
|
} else {
|
||||||
|
console.log("🛣️ 이동경로 데이터 없음");
|
||||||
|
setRoutePoints([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("이동경로 로드 실패:", error);
|
||||||
|
setRoutePoints([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRouteLoading(false);
|
||||||
|
}, [routeDate]);
|
||||||
|
|
||||||
|
// 이동경로 숨기기
|
||||||
|
const clearRoute = useCallback(() => {
|
||||||
|
setSelectedUserId(null);
|
||||||
|
setRoutePoints([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 다중 데이터 소스 로딩
|
// 다중 데이터 소스 로딩
|
||||||
const loadMultipleDataSources = useCallback(async () => {
|
const loadMultipleDataSources = useCallback(async () => {
|
||||||
if (!dataSources || dataSources.length === 0) {
|
if (!dataSources || dataSources.length === 0) {
|
||||||
|
|
@ -509,7 +587,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
status: row.status || row.level,
|
status: row.status || row.level,
|
||||||
description: JSON.stringify(row, null, 2), // 항상 전체 row 객체를 JSON으로 저장
|
description: JSON.stringify(row, null, 2), // 항상 전체 row 객체를 JSON으로 저장
|
||||||
source: sourceName,
|
source: sourceName,
|
||||||
color: dataSource?.markerColor || "#3b82f6", // 사용자 지정 색상 또는 기본 파랑
|
color: row.marker_color || row.color || dataSource?.markerColor || "#3b82f6", // 쿼리 색상 > 설정 색상 > 기본 파랑
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 위도/경도가 없는 육지 지역 → 폴리곤으로 추가 (GeoJSON 매칭용)
|
// 위도/경도가 없는 육지 지역 → 폴리곤으로 추가 (GeoJSON 매칭용)
|
||||||
|
|
@ -1005,6 +1083,32 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
{/* 이동경로 날짜 선택 */}
|
||||||
|
{selectedUserId && (
|
||||||
|
<div className="flex items-center gap-1 rounded border bg-blue-50 px-2 py-1">
|
||||||
|
<span className="text-xs text-blue-600">🛣️</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={routeDate}
|
||||||
|
onChange={(e) => {
|
||||||
|
setRouteDate(e.target.value);
|
||||||
|
if (selectedUserId) {
|
||||||
|
loadRoute(selectedUserId, e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="h-6 rounded border-none bg-transparent px-1 text-xs text-blue-600 focus:outline-none"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-blue-600">
|
||||||
|
({routePoints.length}개)
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={clearRoute}
|
||||||
|
className="ml-1 text-xs text-blue-400 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -1528,12 +1632,51 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
<div className="text-muted-foreground border-t pt-2 text-[10px]">
|
<div className="text-muted-foreground border-t pt-2 text-[10px]">
|
||||||
{marker.lat.toFixed(6)}, {marker.lng.toFixed(6)}
|
{marker.lat.toFixed(6)}, {marker.lng.toFixed(6)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 이동경로 버튼 */}
|
||||||
|
{(() => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(marker.description || "{}");
|
||||||
|
const userId = parsed.user_id;
|
||||||
|
if (userId) {
|
||||||
|
return (
|
||||||
|
<div className="mt-2 border-t pt-2">
|
||||||
|
<button
|
||||||
|
onClick={() => loadRoute(userId)}
|
||||||
|
disabled={routeLoading}
|
||||||
|
className="w-full rounded bg-blue-500 px-2 py-1 text-xs text-white hover:bg-blue-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{routeLoading && selectedUserId === userId
|
||||||
|
? "로딩 중..."
|
||||||
|
: "🛣️ 이동경로 보기"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
</Marker>
|
</Marker>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{/* 이동경로 Polyline */}
|
||||||
|
{routePoints.length > 1 && (
|
||||||
|
<Polyline
|
||||||
|
positions={routePoints.map((p) => [p.lat, p.lng] as [number, number])}
|
||||||
|
pathOptions={{
|
||||||
|
color: "#3b82f6",
|
||||||
|
weight: 4,
|
||||||
|
opacity: 0.8,
|
||||||
|
dashArray: "10, 5",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ const TileLayer = dynamic(() => import("react-leaflet").then((mod) => mod.TileLa
|
||||||
const Marker = dynamic(() => import("react-leaflet").then((mod) => mod.Marker), { ssr: false });
|
const Marker = dynamic(() => import("react-leaflet").then((mod) => mod.Marker), { ssr: false });
|
||||||
const Popup = dynamic(() => import("react-leaflet").then((mod) => mod.Popup), { ssr: false });
|
const Popup = dynamic(() => import("react-leaflet").then((mod) => mod.Popup), { ssr: false });
|
||||||
const Circle = dynamic(() => import("react-leaflet").then((mod) => mod.Circle), { ssr: false });
|
const Circle = dynamic(() => import("react-leaflet").then((mod) => mod.Circle), { ssr: false });
|
||||||
|
const Polyline = dynamic(() => import("react-leaflet").then((mod) => mod.Polyline), { ssr: false });
|
||||||
|
|
||||||
// 브이월드 API 키
|
// 브이월드 API 키
|
||||||
const VWORLD_API_KEY = "97AD30D5-FDC4-3481-99C3-158E36422033";
|
const VWORLD_API_KEY = "97AD30D5-FDC4-3481-99C3-158E36422033";
|
||||||
|
|
@ -37,6 +38,16 @@ interface Vehicle {
|
||||||
status: "active" | "inactive" | "maintenance" | "warning" | "off";
|
status: "active" | "inactive" | "maintenance" | "warning" | "off";
|
||||||
speed: number;
|
speed: number;
|
||||||
destination: string;
|
destination: string;
|
||||||
|
userId?: string; // 이동경로 조회용
|
||||||
|
tripId?: string; // 현재 운행 ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이동경로 좌표
|
||||||
|
interface RoutePoint {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
recordedAt: string;
|
||||||
|
speed?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VehicleMapOnlyWidgetProps {
|
interface VehicleMapOnlyWidgetProps {
|
||||||
|
|
@ -48,6 +59,11 @@ export default function VehicleMapOnlyWidget({ element, refreshInterval = 30000
|
||||||
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
|
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
|
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
|
||||||
|
|
||||||
|
// 이동경로 상태
|
||||||
|
const [selectedVehicle, setSelectedVehicle] = useState<Vehicle | null>(null);
|
||||||
|
const [routePoints, setRoutePoints] = useState<RoutePoint[]>([]);
|
||||||
|
const [isRouteLoading, setIsRouteLoading] = useState(false);
|
||||||
|
|
||||||
const loadVehicles = async () => {
|
const loadVehicles = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
@ -121,6 +137,8 @@ export default function VehicleMapOnlyWidget({ element, refreshInterval = 30000
|
||||||
: "inactive",
|
: "inactive",
|
||||||
speed: parseFloat(row.speed) || 0,
|
speed: parseFloat(row.speed) || 0,
|
||||||
destination: row.destination || "대기 중",
|
destination: row.destination || "대기 중",
|
||||||
|
userId: row.user_id || row.userId || undefined,
|
||||||
|
tripId: row.trip_id || row.tripId || undefined,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
// 유효한 위도/경도가 있는 차량만 필터링
|
// 유효한 위도/경도가 있는 차량만 필터링
|
||||||
|
|
@ -140,6 +158,78 @@ export default function VehicleMapOnlyWidget({ element, refreshInterval = 30000
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 이동경로 로드 함수
|
||||||
|
const loadRoute = async (vehicle: Vehicle) => {
|
||||||
|
if (!vehicle.userId && !vehicle.tripId) {
|
||||||
|
console.log("🛣️ 이동경로 조회 불가: userId 또는 tripId 없음");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRouteLoading(true);
|
||||||
|
setSelectedVehicle(vehicle);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 오늘 날짜 기준으로 최근 이동경로 조회
|
||||||
|
const today = new Date();
|
||||||
|
const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate()).toISOString();
|
||||||
|
|
||||||
|
// trip_id가 있으면 해당 운행만, 없으면 user_id로 오늘 전체 조회
|
||||||
|
let query = "";
|
||||||
|
if (vehicle.tripId) {
|
||||||
|
query = `SELECT latitude, longitude, speed, recorded_at
|
||||||
|
FROM vehicle_location_history
|
||||||
|
WHERE trip_id = '${vehicle.tripId}'
|
||||||
|
ORDER BY recorded_at ASC`;
|
||||||
|
} else if (vehicle.userId) {
|
||||||
|
query = `SELECT latitude, longitude, speed, recorded_at
|
||||||
|
FROM vehicle_location_history
|
||||||
|
WHERE user_id = '${vehicle.userId}'
|
||||||
|
AND recorded_at >= '${startOfDay}'
|
||||||
|
ORDER BY recorded_at ASC`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🛣️ 이동경로 쿼리:", query);
|
||||||
|
|
||||||
|
const response = await fetch(getApiUrl("/api/dashboards/execute-query"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${typeof window !== "undefined" ? localStorage.getItem("authToken") || "" : ""}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ query }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success && result.data.rows.length > 0) {
|
||||||
|
const points: RoutePoint[] = result.data.rows.map((row: any) => ({
|
||||||
|
lat: parseFloat(row.latitude),
|
||||||
|
lng: parseFloat(row.longitude),
|
||||||
|
recordedAt: row.recorded_at,
|
||||||
|
speed: row.speed ? parseFloat(row.speed) : undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(`🛣️ 이동경로 ${points.length}개 포인트 로드 완료`);
|
||||||
|
setRoutePoints(points);
|
||||||
|
} else {
|
||||||
|
console.log("🛣️ 이동경로 데이터 없음");
|
||||||
|
setRoutePoints([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("이동경로 로드 실패:", error);
|
||||||
|
setRoutePoints([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRouteLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 이동경로 숨기기
|
||||||
|
const clearRoute = () => {
|
||||||
|
setSelectedVehicle(null);
|
||||||
|
setRoutePoints([]);
|
||||||
|
};
|
||||||
|
|
||||||
// useEffect는 항상 같은 순서로 호출되어야 함 (early return 전에 배치)
|
// useEffect는 항상 같은 순서로 호출되어야 함 (early return 전에 배치)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadVehicles();
|
loadVehicles();
|
||||||
|
|
@ -220,6 +310,19 @@ export default function VehicleMapOnlyWidget({ element, refreshInterval = 30000
|
||||||
keepBuffer={2}
|
keepBuffer={2}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 이동경로 Polyline */}
|
||||||
|
{routePoints.length > 1 && (
|
||||||
|
<Polyline
|
||||||
|
positions={routePoints.map((p) => [p.lat, p.lng] as [number, number])}
|
||||||
|
pathOptions={{
|
||||||
|
color: "#3b82f6",
|
||||||
|
weight: 4,
|
||||||
|
opacity: 0.8,
|
||||||
|
dashArray: "10, 5",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 차량 마커 */}
|
{/* 차량 마커 */}
|
||||||
{vehicles.map((vehicle) => (
|
{vehicles.map((vehicle) => (
|
||||||
<React.Fragment key={vehicle.id}>
|
<React.Fragment key={vehicle.id}>
|
||||||
|
|
@ -248,6 +351,20 @@ export default function VehicleMapOnlyWidget({ element, refreshInterval = 30000
|
||||||
<div>
|
<div>
|
||||||
<strong>목적지:</strong> {vehicle.destination}
|
<strong>목적지:</strong> {vehicle.destination}
|
||||||
</div>
|
</div>
|
||||||
|
{/* 이동경로 버튼 */}
|
||||||
|
{(vehicle.userId || vehicle.tripId) && (
|
||||||
|
<div className="mt-2 border-t pt-2">
|
||||||
|
<button
|
||||||
|
onClick={() => loadRoute(vehicle)}
|
||||||
|
disabled={isRouteLoading}
|
||||||
|
className="w-full rounded bg-blue-500 px-2 py-1 text-xs text-white hover:bg-blue-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isRouteLoading && selectedVehicle?.id === vehicle.id
|
||||||
|
? "로딩 중..."
|
||||||
|
: "🛣️ 이동경로 보기"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
</Marker>
|
</Marker>
|
||||||
|
|
@ -271,6 +388,24 @@ export default function VehicleMapOnlyWidget({ element, refreshInterval = 30000
|
||||||
<div className="text-xs text-foreground">데이터를 연결하세요</div>
|
<div className="text-xs text-foreground">데이터를 연결하세요</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 이동경로 정보 표시 - 상단으로 이동하여 주석 처리 */}
|
||||||
|
{/* {selectedVehicle && routePoints.length > 0 && (
|
||||||
|
<div className="absolute bottom-2 right-2 z-[1000] rounded-lg bg-blue-500/90 p-2 shadow-lg backdrop-blur-sm">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="text-xs text-white">
|
||||||
|
<div className="font-semibold">🛣️ {selectedVehicle.name} 이동경로</div>
|
||||||
|
<div>{routePoints.length}개 포인트</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={clearRoute}
|
||||||
|
className="rounded bg-white/20 px-2 py-1 text-xs text-white hover:bg-white/30"
|
||||||
|
>
|
||||||
|
숨기기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue