현재 뷰 기준으로 마커 표시하기

MOON HEE·2023년 4월 13일
0

해결 순서

현재 뷰 지리좌표 구하기 ➡️ Polygon 구하기 ➡️ 이를 바탕으로 주소정보 필터링하기


0. 몽고DB의 지리공간 쿼리 사용 준비

카카오맵 API 중에 도로명 주소를 위경도 좌표로 변환하는 API가 있긴 하지만, 지도에 그리는 작업을 할 때 좌표로 변환하는 작업까지 하면 그만큼 시간이 소요됐다. 그래서 애초에 DB에 저장할 때 좌표정보까지 저장했다.

지리공간 쿼리를 사용하기 위해서는 정해진 Schema 형식이 있다.

  • Point는 location type 중에 하나로, 좌표(coordinates)가 단일 위치임을 의미한다(= 점 하나). Point 외에 MultiPoint, LineString, MultiLineString이 있지만 가장 기본이 되는 Point를 많이 사용한다.

  • 지리공간 쿼리의 연산자를 사용하려면 우선 해당 필드에 index를 추가해서 GeoJSON 객체(= 지리적 지점과 폴리곤을 저장하기 위한 형식)로 지정해준다. location이라는 필드의 index를 2dsphere로 지정한다.

import mongoose, { Schema } from 'mongoose';

const restaurantSchema = new Schema({
    ...,
    location: {
        type: {
            type: String, // { location: { type: String } } 아님!
            enum: ['Point'], // location type은 Point
            required: true,
        },
        coordinates: {
            type: [Number],
            required: true,
        },
    },
});

restaurantSchema.index({ location: '2dsphere' }); // 인덱스 지정

const Restaurant = mongoose.model('Restaurant', restaurantSchema);

1. 현재 뷰의 지리 좌표 구하기

카카오맵의 bounds_changed 이벤트를 통해 중심좌표를 이동하면 북동위도(neLat), 북동경도(neLng), 남서위도(swLat), 남서경도(swLng)를 구할 수 있다.

getLists라는 함수는 이 좌표로 만든 Polygon(다각형)을 매개변수로 보내서 지리공간 쿼리로 필터링한 식당 리스트를 반환하도록 만들었다.

kakao.maps.event.addListener(map, 'bounds_changed', function () {
        const neLat = map.getBounds().getNorthEast().getLat();
        const neLng = map.getBounds().getNorthEast().getLng();
        const swLat = map.getBounds().getSouthWest().getLat();
        const swLng = map.getBounds().getSouthWest().getLng();
  
        getLists([
            [neLng, neLat],
            [swLng, neLat],
            [swLng, swLat],
            [neLng, swLat],
            [neLng, neLat],
        ]).then((res) => {
          
            paintMarker(res);
          
            store.dispatch({
                type: 'CHANGED_CENTER',
                COUNT: res.length,
            });
        });
    });

...

export async function getLists(polygon: Polygon) {
    const data = await fetch(`http://localhost:5000/api/maps/inner`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(polygon),
    });

    const res = await data.json();
    return res;
}

2. Polygon 구하기

위 좌표로 다각형(Polygon)을 만들어 준다. 이때 올바른 다각형을 만들기 위해 시작점과 끝점이 만나도록 해줘야 한다.

Polygon은 몽고DB의 지리공간 쿼리 중 면적을 지정하기 위해 필요한 값이다. DB에 있는 많은 데이터를 프론트에서 순서대로 읽어서 필터링 하는 것보다 DB의 기능으로 필터링하는 것이 더 빠르기 때문에 몽고DB의 Geospatial Query를 사용한다.

카카오 좌표를 활용한 Polygon

[neLng, neLat], [swLng, neLat], [swLng, swLat], [neLng, swLat], [neLng, neLat]

3. 필터링

  • $geoWithin : 지정된 모양 내 완전히 존재하는 지리공간 데이터가 있는 document를 선택한다. 이때 지정된 모양은 GeoJSON Polygon, MultiPolygon, 레거시 좌표쌍이 가능하다.

  • $geometry : $geoWithin과 함께 사용할 GeoJSON geometry(도형)를 지정한다.

import { Router, Request, Response } from 'express';
import Restaurant from '../../models/Restaurant.js';

const route = Router();

export default (app: Router) => {
    app.use('/maps', route);

    route.post('/inner', async (req: Request, res: Response) => {
        try {
            const lists = await Restaurant.find({
                location: {
                    $geoWithin: {
                        $geometry: {
                            type: 'Polygon',
                            coordinates: [req.body],
                        },
                    },
                },
            });

            res.json(lists);
        } catch (err) {
            console.error(err);
        }
    });
};
profile
자고 일어나면 해결되는게 더 많은 듯 그럼 잘까

0개의 댓글