[응용] NaverMap API 사용하기 (4)

쓰리원·2022년 6월 28일
0

안드로이드 지도API

목록 보기
6/12
post-thumbnail

1. 구현에 필요한 기능 확인하기

전체 코드 길이가 길고 다뤄야 하는 클래스도 많기 때문에 설명에 필요한 일부 코드만 다뤄보겠습니다.

1. MainActivity.kt (fun onCreate())

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        var mapFragment: MapFragment =
            supportFragmentManager.findFragmentById(R.id.map_fragment) as MapFragment
            
        mapFragment.getMapAsync(this)

        locationSource = FusedLocationSource(this, LOCATION_PERMISSION_REQUEST_CODE)
        uiScope = CoroutineScope(Dispatchers.Main)

        getApiShopList()

        binding.btnSearchAround.setOnClickListener {
            updateMarker()
        }
    }

이전글과 다르게 추가된 기능이 3개가 있습니다. 비동기로 통신을 위한 uiScope와 실제로 데이터를 가져올 fun인 getApiShopList() 그리고 데이터를 가져오고 그 데이터를 표시하기 위한 updateMarker() 입니다. 차근히 알아보록 하겠습니다.

2. 서버 통신으로 Shop 리스트 불러오기

1. Endpoint를 생성

위와 같이 Endpoint interface를 만들어서 서버에 접근해서 통신할 수 있게 해줍니다. Endpoint에서 URL에 대한 결과값을 반환 받을 메서드는 GET으로 하였습니다.

2. Retrofit과 OkHttpClient

object RetrofitUtil {

    private var shopinstance: Retrofit? = null

    val shopController: ShopController by lazy {
        provideShopRetrofit().create(ShopController::class.java)
    }

    private fun provideShopRetrofit(): Retrofit {
        if(shopinstance == null) {
            shopinstance = Retrofit.Builder()
                .baseUrl(Url.SHOP_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .client(buildOkHttpClient())
                .build()
        }
        return shopinstance!!
    }

    private fun buildOkHttpClient(): OkHttpClient {

        val interceptor = HttpLoggingInterceptor()

        interceptor.level =
            if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
            else HttpLoggingInterceptor.Level.NONE

        return OkHttpClient.Builder()
            .connectTimeout(5, TimeUnit.SECONDS)
            .addInterceptor(interceptor)
            .build()
    }
}

RetrofitUtil은 기존의 객체를 재활용 하기 위해서 싱글턴으로 구현하였습니다. 그리고 Response와 Request에 대한 정보를 보기 위해서 OkHttpClient를 사용하여 val interceptor = HttpLoggingInterceptor() 객체를 생성해서 Retrofit에 추가해 줍니다.

3. MainActivity.kt (fun getApiShopList())

    fun getApiShopList() {
        uiScope.launch {
            withContext(Dispatchers.IO) {
                val response = RetrofitUtil.shopController.getList()

                if (response.isSuccessful) {
                    val list = response.body()
                    list?.let {
                        it.shopList.forEach { ShopData ->
                            shopList.add(ShopData)
                        }
                    }
                }
                else {
                    null
                }
            }
        }
    }

레트로핏을 사용하여 http 통신을 진행합니다. 그 과정에서 비동기 처리를 하기 위해서 코루틴을 사용하게 됩니다. Dispatchers.IO로 하여서 IO 전용의 쓰레드를 사용하여 데이터를 가져오는 것을 확인 할 수 있습니다. 안드로이드 경우 UI 쓰레드를 사용하면 ANR로 앱이 멈추는 현상이 나타날 수 있기 때문에 UI 쓰레드에서는 비용이 큰 작업을 하면 안됩니다.

private var shopList: MutableList<ShopData> = mutableListOf()

이렇게 받아온 data를 위의 배열에 넣어줍니다.

3. 리스트에서 불러온 정보대로 마커를 표시하기

1. MainActivity.kt (fun updateMarker())

  private fun updateMarker() {

        deleteMarkers()

        var markets: List<ShopData> = mutableListOf()
        var temp = arrayListOf<Marker>()
        var i = 0

        markets = shopList

        markets?.let {
            repeat(markets.size) {
                temp += Marker().apply {
                    position = LatLng(markets[i].latitude, markets[i].longitude)
                    icon = MarkerIcons.BLACK
                    tag = markets[i].shop_name
                    zIndex = i
                }
                i++
            }
            markers = temp
            searchAround()
        }
    }

위에서 받아온 전역으로 저장된 shopList가 있습니다. 그것을 markets = shopList으로 넣어주고 markets의 갯수만큼 repeat하여 반복문을 시행해줍니다. 그리고, temp에 markets에 들어있는 정보중 필요한 정보를 마커에 넣어서 넣어준뒤 마커의 배열에 또 저장을 해주게 되면 마커는 shopList에서 받아온 데이터를 가지게 됩니다.

   private fun searchAround() {
       
       deleteMarkers()
 
       for (marker in markers) {
           marker.map = naverMap
           setMarkerIconAndColor(marker, getCategoryNum(shopList.get(marker.zIndex)!!.category))
       }
   }
   
   private fun deleteMarkers() {
       for (marker in markers) {
           marker.map = null
       }
   }

기존의 배열에 있는 마커를 지우고 난 뒤에 다시 마커를 새롭게 출력해 줍니다.

4. 원의 반지름 반경에 따라서 마커 출력하기

1. MainActivity.kt (fun calDist())

    fun calDist(lat1:Double, lon1:Double, lat2:Double, lon2:Double) : Long {

        val EARTH_R = 6371000.0
        val rad = Math.PI / 180
        val radLat1 = rad * lat1
        val radLat2 = rad * lat2
        val radDist = rad * (lon1 - lon2)

        var distance = Math.sin(radLat1) * Math.sin(radLat2)
        distance = distance + Math.cos(radLat1) * Math.cos(radLat2) * Math.cos(radDist)
        val ret = EARTH_R * Math.acos(distance)

        return Math.round(ret)
    }

두 점사이의 직선 거리를 구하는 함수 입니다. 이를 통해서 원안에 상점이 들어와 있는지 체크할 수 있습니다.

2. MainActivity.kt (fun updateMarker())

private fun updateMarker() {

        deleteMarkers()

        var markets: List<ShopData> = mutableListOf()
        var temp = arrayListOf<Marker>()
        var i = 0

        markets = shopList

        markets?.let {
            repeat(markets.size) {

                val dist = calDist(
                    curLocation.latitude,
                    curLocation.longitude,
                    markets[i].latitude,
                    markets[i].longitude)

                if (dist < DISTANCE ) {
                    temp += Marker().apply {
                        position = LatLng(markets[i].latitude, markets[i].longitude)
                        icon = MarkerIcons.BLACK
                        tag = markets[i].shop_name
                        zIndex = i
                    }
                }
                i++
            }
            markers = temp
            searchAround()
        }
    }

마커를 업데이트 할 때 DISTANCE 조건에 맞게 거리에 따라서 마커를 temp에 넣어줍니다. 그리고 난 뒤 출력을 해주게 되면 아래와 같은 그림의 원 범위로 존재하는 상점만 출력되는 것을 볼 수 있습니다.

5. 마커 정보창(InfoWindow) 클릭 이벤트

    private fun setMarkerListener() {
        for (marker in markers) {

            var tempinfoWindow = InfoWindow()
            tempinfoWindow?.adapter = object : InfoWindow.DefaultTextAdapter(this) {
                override fun getText(infoWindow: InfoWindow): CharSequence {
                    return infoWindow.marker?.tag as CharSequence
                }
            }

            infoWindow = tempinfoWindow
            
            marker.setOnClickListener {

                if(tempinfoWindow?.marker != null) {
                    tempinfoWindow?.close()
                } else {
                    tempinfoWindow?.open(marker)
                }
                true
            }
        }
    }

var tempinfoWindow = InfoWindow()에서 InfoWindow 객체를 각각 생성 한 뒤 tempinfoWindow?.marker로 InfoWindow를 각각 객체마다 껏다가 켯다가 할 수 있습니다.

profile
가장 아름다운 정답은 서로의 협업안에 있다.

0개의 댓글