mym0404 / react-native-naver-map

Naver Map for React Native - Bring Naver Map to Your React Fingertips
https://mym0404.github.io/react-native-naver-map/
MIT License
86 stars 6 forks source link

겹치는 마커 처리(클러스터)에 사용법에 대하여 질문 #103

Open ParkJongJoon7128 opened 6 days ago

ParkJongJoon7128 commented 6 days ago

크롤링한 데이터를 기반으로 지도맵에 마커를 뿌려 보여줄려고 하는데, 적은양의 데이터가 아니라서 마커가 많아 겹치는 부분이 많습니다. 때문에 이를 방지하고자 라이브러리에서 클러스터(cluster) 기능을 제공해주고 있어서 사용하고 있는데, 구현을 하고 앱을 실행해보니 클러스터 기능이 적용이 안되고, 마커만 적용되고 있어 질문합니다.

공식문서에서 사용된 예제에서는 클러스터가 사용되어 정상적으로 동작하는것으로 보이는데, 혹시 제 코드에서 미흡하게 구현한 부분이 있으면 조언을 받고 싶어 코드와 시연 영상과 함께 이슈 글을 남깁니다.

https://github.com/user-attachments/assets/55f5a668-24da-4fce-8faf-0c61c32b09b2

제가 구현중인 코드입니다.

import {
  Camera,
  ClusterMarkerProp,
  MapType,
  NaverMapMarkerOverlay,
  NaverMapView,
  NaverMapViewRef,
  Region,
} from '@mj-studio/react-native-naver-map';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { View } from 'react-native';
import { getRestaurants } from '../utils/API/LocationAPI';
import { Restaurant } from '../utils/data/type';

const Cameras = {
  KNU: {
    latitude: 37.271855,
    longitude: 127.127626,
    zoom: 14,
  },
} satisfies Record<string, Camera>;

const Regions = {
  KNU_Region: {
    latitude: 37.271855,
    longitude: 127.127626,
    latitudeDelta: 0.01,
    longitudeDelta: 0.01,
  },
} satisfies Record<string, Region>;

const MapTypes = [
  'Basic',
  'Navi',
  'Satellite',
  'Hybrid',
  'Terrain',
  'NaviHybrid',
  'None',
] satisfies MapType[];

const LocationMapScreen = () => {
  // Logic
  const ref = useRef<NaverMapViewRef>(null);
  const [restaurants, setRestaurants] = useState<Restaurant[]>([]);
  const [indoor, setIndoor] = useState(false);
  const [mapType, setMapType] = useState<MapType>(MapTypes[0]!);
  const [lightness, setLightness] = useState(0);
  const [compass, setCompass] = useState(true);
  const [scaleBar, setScaleBar] = useState(true);
  const [zoomControls, setZoomControls] = useState(true);
  const [indoorLevelPicker, setIndoorLevelPicker] = useState(true);
  const [myLocation, setMyLocation] = useState(true);
  const [hash, setHash] = useState(0);
  const [camera, setCamera] = useState(Cameras.KNU);

  const clusters = useMemo<
    {
      markers: ClusterMarkerProp[];
      screenDistance?: number;
      minZoom?: number;
      maxZoom?: number;
      animate?: boolean;
      width?: number;
      height?: number;
    }[]
  >(() => {
    return restaurants.map(i => {
      return {
        width: 200,
        height: 200,
        markers: restaurants.map<ClusterMarkerProp>(
          j =>
            ({
              image: {
                httpUri: `https://picsum.photos/seed/${hash}-${i}-${j}/32/32`,
              },
              width: 100,
              height: 100,
              latitude: Cameras.KNU.latitude + Math.random() + 1.5,
              longitude: Cameras.KNU.longitude + Math.random() + 1.5,
              identifier: `${hash}-${i}-${j}`,
            } satisfies ClusterMarkerProp),
        ),
      };
    });
  }, [hash]);

  useEffect(() => {
    getRestaurants(setRestaurants);
  }, []);

  // View
  return (
    <View
      style={{
        flex: 1,
        backgroundColor: 'white',
      }}>
      <NaverMapView
        style={{flex: 1}}
        ref={ref}
        initialCamera={{
          latitude: 37.271855,
          longitude: 127.127626,
          zoom: 15,
        }}
        locale="ko"
        layerGroups={{
          BUILDING: true,
          BICYCLE: false,
          CADASTRAL: false,
          MOUNTAIN: false,
          TRAFFIC: false,
          TRANSIT: false,
        }}
        mapType={mapType}
        initialRegion={Regions.KNU_Region}
        camera={camera}
        isIndoorEnabled={indoor}
        isShowCompass={compass}
        isShowIndoorLevelPicker={indoorLevelPicker}
        isShowScaleBar={scaleBar}
        isShowZoomControls={zoomControls}
        isShowLocationButton={myLocation}
        lightness={lightness}
        clusters={clusters}
        onInitialized={() => console.log('initialized!')}
        onTapClusterLeaf={({markerIdentifier}) => {
          console.log('onTapClusterLeaf', markerIdentifier);
        }}>
        {restaurants.map((restaurant, index) => (
          <NaverMapMarkerOverlay
            key={restaurant.id}
            latitude={parseFloat(restaurant.y)}
            longitude={parseFloat(restaurant.x)}
            onTap={() => console.log(`Tapped on: ${restaurant.name}`)}
            anchor={{x: 0.5, y: 1}}
            width={20}
            height={20}>
            <View style={{width: 50, height: 50, backgroundColor: 'red'}} />
          </NaverMapMarkerOverlay>
        ))}
      </NaverMapView>
    </View>
  );
};

export default LocationMapScreen;
mym0404 commented 6 days ago

예제의 App.tsx의 코드를 그대로 쓰신 것으로 보입니다. clusters 변수가 hash가 변할 때만 변경되도록 설정되어 있으니 useMemo 를 지워보시면 될것같습니다.

ParkJongJoon7128 commented 5 days ago

현재 저는 서버로 요청을 보내 받아온 결과값을 restaurants 라는 state 변수에 저장하여 marker로 뿌려주고 있습니다(빨간 사각형이 응답값으로 내려온 데이터 좌표값들).

이 state 변수와 연관지어 clusters 를 보여줄순 없나요? 지도를 축소했을때는 겹치는 마커만큼 갯수를 clusters로 표현하고, 확대하면 clusters가 벗겨져 마커를 띄우게 하고 싶습니다.

현재 구현한 코드입니다.

import { formatJson, generateArray } from '@mj-studio/js-util';
import {
  Camera,
  ClusterMarkerProp,
  MapType,
  NaverMapMarkerOverlay,
  NaverMapView,
  NaverMapViewRef,
  Region,
} from '@mj-studio/react-native-naver-map';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { View } from 'react-native';
import { getRestaurants } from '../utils/API/LocationAPI';
import { Restaurant } from '../utils/data/type';

const Cameras = {
  KNU: {
    latitude: 37.271855,
    longitude: 127.127626,
    zoom: 14,
  },
} satisfies Record<string, Camera>;

const Regions = {
  KNU_Region: {
    latitude: 37.271855,
    longitude: 127.127626,
    latitudeDelta: 0.01,
    longitudeDelta: 0.01,
  },
} satisfies Record<string, Region>;

const MapTypes = [
  'Basic',
  'Navi',
  'Satellite',
  'Hybrid',
  'Terrain',
  'NaviHybrid',
  'None',
] satisfies MapType[];

const LocationMapScreen = () => {
  // Logic
  const ref = useRef<NaverMapViewRef>(null);
  const [restaurants, setRestaurants] = useState<Restaurant[]>([]);
  const [indoor, setIndoor] = useState(false);
  const [mapType, setMapType] = useState<MapType>(MapTypes[0]!);
  const [lightness, setLightness] = useState(0);
  const [compass, setCompass] = useState(true);
  const [scaleBar, setScaleBar] = useState(true);
  const [zoomControls, setZoomControls] = useState(true);
  const [indoorLevelPicker, setIndoorLevelPicker] = useState(true);
  const [myLocation, setMyLocation] = useState(true);
  const [hash, setHash] = useState(0);
  const [camera, setCamera] = useState(Cameras.KNU);

  const clusters = useMemo<
    {
      markers: ClusterMarkerProp[];
      screenDistance?: number;
      minZoom?: number;
      maxZoom?: number;
      animate?: boolean;
      width?: number;
      height?: number;
    }[]
  >(() => {
    return generateArray(5).map(i => {
      return {
        width: 200,
        height: 200,
        markers: generateArray(restaurants.length).map<ClusterMarkerProp>(
          j =>
            ({
              image: {
                httpUri: `https://picsum.photos/seed/${hash}-${i}-${j}/32/32`,
              },
              width: 100,
              height: 100,
              latitude: Cameras.KNU.latitude,
              longitude: Cameras.KNU.longitude,
              identifier: `${hash}-${i}-${j}`,
            } satisfies ClusterMarkerProp),
        ),
      };
    });
  }, [hash]);

  useEffect(() => {
    getRestaurants(setRestaurants);
  }, []);

  // View
  return (
    <View
      style={{
        flex: 1,
        backgroundColor: 'white',
      }}>
      <NaverMapView
        style={{flex: 1}}
        ref={ref}
        initialCamera={{
          latitude: 37.271855,
          longitude: 127.127626,
          zoom: 15,
        }}
        locale="ko"
        layerGroups={{
          BUILDING: true,
          BICYCLE: false,
          CADASTRAL: false,
          MOUNTAIN: false,
          TRAFFIC: false,
          TRANSIT: false,
        }}
        mapType={mapType}
        initialRegion={Regions.KNU_Region}
        camera={camera}
        isIndoorEnabled={indoor}
        isShowCompass={compass}
        isShowIndoorLevelPicker={indoorLevelPicker}
        isShowScaleBar={scaleBar}
        isShowZoomControls={zoomControls}
        isShowLocationButton={myLocation}
        lightness={lightness}
        clusters={clusters}
        onInitialized={() => console.log('initialized!')}
        onTapClusterLeaf={({markerIdentifier}) => {
          console.log('onTapClusterLeaf', markerIdentifier);
        }}
        onTapMap={(args) => console.log(`Map Tapped: ${formatJson(args)}`)}
        >
        {restaurants.map((restaurant, index) => (
          <NaverMapMarkerOverlay
            key={restaurant.id}
            latitude={parseFloat(restaurant.y)}
            longitude={parseFloat(restaurant.x)}
            onTap={() => console.log(`Tapped on: ${restaurant.name}`)}
            anchor={{x: 0.5, y: 1}}
            width={20}
            height={20}>
            <View style={{width: 50, height: 50, backgroundColor: 'red'}} />
          </NaverMapMarkerOverlay>
        ))}
      </NaverMapView>
    </View>
  );
};

export default LocationMapScreen;
ParkJongJoon7128 commented 5 days ago

그리고 예제에서는 generateArray() 메소드를 사용하고 계시던데, 이 메소드의 사용법에 대해서도 궁금합니다.