지역 필터링 기능 추가

This commit is contained in:
leeheejin 2025-12-09 10:18:07 +09:00
parent a20712d48e
commit 469c8b2e57
2 changed files with 265 additions and 3 deletions

View File

@ -8,6 +8,8 @@ import { Button } from "@/components/ui/button";
import { Loader2, RefreshCw } from "lucide-react";
import { applyColumnMapping } from "@/lib/utils/columnMapping";
import { getApiUrl } from "@/lib/utils/apiUrl";
import { regionOptions, filterVehiclesByRegion } from "@/lib/constants/regionBounds";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import "leaflet/dist/leaflet.css";
// Popup 말풍선 꼬리 제거 스타일
@ -101,6 +103,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
const [routeLoading, setRouteLoading] = useState(false);
const [routeDate, setRouteDate] = useState<string>(new Date().toISOString().split("T")[0]); // YYYY-MM-DD 형식
// 지역 필터 상태
const [selectedRegion, setSelectedRegion] = useState<string>("all");
// dataSources를 useMemo로 추출 (circular reference 방지)
const dataSources = useMemo(() => {
return element?.dataSources || element?.chartConfig?.dataSources;
@ -1165,6 +1170,20 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
</p>
</div>
<div className="flex items-center gap-2">
{/* 지역 필터 */}
<Select value={selectedRegion} onValueChange={setSelectedRegion}>
<SelectTrigger className="h-8 w-[140px] text-xs">
<SelectValue placeholder="지역 선택" />
</SelectTrigger>
<SelectContent>
{regionOptions.map((option) => (
<SelectItem key={option.value} value={option.value} className="text-xs">
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 이동경로 날짜 선택 */}
{selectedUserId && (
<div className="flex items-center gap-1 rounded border bg-blue-50 px-2 py-1">
@ -1442,8 +1461,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
);
})}
{/* 마커 렌더링 */}
{markers.map((marker) => {
{/* 마커 렌더링 (지역 필터 적용) */}
{filterVehiclesByRegion(markers, selectedRegion).map((marker) => {
// 마커의 소스에 해당하는 데이터 소스 찾기
const sourceDataSource = dataSources?.find((ds) => ds.name === marker.source) || dataSources?.[0];
const markerType = sourceDataSource?.markerType || "circle";
@ -1771,7 +1790,12 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
{/* 하단 정보 */}
{(markers.length > 0 || polygons.length > 0) && (
<div className="text-muted-foreground border-t p-2 text-xs">
{markers.length > 0 && `마커 ${markers.length}`}
{markers.length > 0 && (
<>
{filterVehiclesByRegion(markers, selectedRegion).length}
{selectedRegion !== "all" && ` (전체 ${markers.length}개)`}
</>
)}
{markers.length > 0 && polygons.length > 0 && " · "}
{polygons.length > 0 && `영역 ${polygons.length}`}
</div>

View File

@ -0,0 +1,238 @@
/**
* / ( )
*
*/
export interface RegionBounds {
south: number; // 최남단 위도
north: number; // 최북단 위도
west: number; // 최서단 경도
east: number; // 최동단 경도
}
export interface RegionOption {
value: string;
label: string;
bounds?: RegionBounds;
}
// 전국 시/도별 좌표 범위
export const regionBounds: Record<string, RegionBounds> = {
// 서울특별시
seoul: {
south: 37.413,
north: 37.715,
west: 126.734,
east: 127.183,
},
// 부산광역시
busan: {
south: 34.879,
north: 35.389,
west: 128.758,
east: 129.314,
},
// 대구광역시
daegu: {
south: 35.601,
north: 36.059,
west: 128.349,
east: 128.761,
},
// 인천광역시
incheon: {
south: 37.166,
north: 37.592,
west: 126.349,
east: 126.775,
},
// 광주광역시
gwangju: {
south: 35.053,
north: 35.267,
west: 126.652,
east: 127.013,
},
// 대전광역시
daejeon: {
south: 36.197,
north: 36.488,
west: 127.246,
east: 127.538,
},
// 울산광역시
ulsan: {
south: 35.360,
north: 35.710,
west: 128.958,
east: 129.464,
},
// 세종특별자치시
sejong: {
south: 36.432,
north: 36.687,
west: 127.044,
east: 127.364,
},
// 경기도
gyeonggi: {
south: 36.893,
north: 38.284,
west: 126.387,
east: 127.839,
},
// 강원도 (강원특별자치도)
gangwon: {
south: 37.017,
north: 38.613,
west: 127.085,
east: 129.359,
},
// 충청북도
chungbuk: {
south: 36.012,
north: 37.261,
west: 127.282,
east: 128.657,
},
// 충청남도
chungnam: {
south: 35.972,
north: 37.029,
west: 125.927,
east: 127.380,
},
// 전라북도 (전북특별자치도)
jeonbuk: {
south: 35.287,
north: 36.133,
west: 126.392,
east: 127.923,
},
// 전라남도
jeonnam: {
south: 33.959,
north: 35.507,
west: 125.979,
east: 127.921,
},
// 경상북도
gyeongbuk: {
south: 35.571,
north: 37.144,
west: 128.113,
east: 130.922,
},
// 경상남도
gyeongnam: {
south: 34.599,
north: 35.906,
west: 127.555,
east: 129.224,
},
// 제주특별자치도
jeju: {
south: 33.106,
north: 33.959,
west: 126.117,
east: 126.978,
},
};
// 지역 선택 옵션 (드롭다운용)
export const regionOptions: RegionOption[] = [
{ value: "all", label: "전체" },
{ value: "seoul", label: "서울특별시", bounds: regionBounds.seoul },
{ value: "busan", label: "부산광역시", bounds: regionBounds.busan },
{ value: "daegu", label: "대구광역시", bounds: regionBounds.daegu },
{ value: "incheon", label: "인천광역시", bounds: regionBounds.incheon },
{ value: "gwangju", label: "광주광역시", bounds: regionBounds.gwangju },
{ value: "daejeon", label: "대전광역시", bounds: regionBounds.daejeon },
{ value: "ulsan", label: "울산광역시", bounds: regionBounds.ulsan },
{ value: "sejong", label: "세종특별자치시", bounds: regionBounds.sejong },
{ value: "gyeonggi", label: "경기도", bounds: regionBounds.gyeonggi },
{ value: "gangwon", label: "강원특별자치도", bounds: regionBounds.gangwon },
{ value: "chungbuk", label: "충청북도", bounds: regionBounds.chungbuk },
{ value: "chungnam", label: "충청남도", bounds: regionBounds.chungnam },
{ value: "jeonbuk", label: "전북특별자치도", bounds: regionBounds.jeonbuk },
{ value: "jeonnam", label: "전라남도", bounds: regionBounds.jeonnam },
{ value: "gyeongbuk", label: "경상북도", bounds: regionBounds.gyeongbuk },
{ value: "gyeongnam", label: "경상남도", bounds: regionBounds.gyeongnam },
{ value: "jeju", label: "제주특별자치도", bounds: regionBounds.jeju },
];
/**
*
*/
export function isInRegion(
latitude: number,
longitude: number,
region: string
): boolean {
if (region === "all") return true;
const bounds = regionBounds[region];
if (!bounds) return false;
return (
latitude >= bounds.south &&
latitude <= bounds.north &&
longitude >= bounds.west &&
longitude <= bounds.east
);
}
/**
* ( )
*/
export function findRegionByCoords(
latitude: number,
longitude: number
): string | null {
for (const [region, bounds] of Object.entries(regionBounds)) {
if (
latitude >= bounds.south &&
latitude <= bounds.north &&
longitude >= bounds.west &&
longitude <= bounds.east
) {
return region;
}
}
return null;
}
/**
*
*/
export function filterVehiclesByRegion<
T extends { latitude?: number; longitude?: number; lat?: number; lng?: number }
>(vehicles: T[], region: string): T[] {
if (region === "all") return vehicles;
const bounds = regionBounds[region];
if (!bounds) return vehicles;
return vehicles.filter((v) => {
const lat = v.latitude ?? v.lat;
const lng = v.longitude ?? v.lng;
if (lat === undefined || lng === undefined) return false;
return (
lat >= bounds.south &&
lat <= bounds.north &&
lng >= bounds.west &&
lng <= bounds.east
);
});
}
/**
* ()
*/
export function getRegionLabel(regionValue: string): string {
const option = regionOptions.find((opt) => opt.value === regionValue);
return option?.label ?? regionValue;
}