작업 일지

[Android] 내비게이션의 다음 경로 정보 구하기#1

O_Gyong 2024. 3. 5.

프로젝트에서 내비게이션을 사용하여 다음 경로까지의 거리 값과 회전 정보를 구해야하는 일이 생겼다.

개발 과정과 사용하게 된 API에 대해서 정리를 해본다.


사용 API

◾ 카카오내비 길찾기 SDK

우선 내비게이션을 사용하기 위해서 네이버, 카카오, 티맵과 같은 플랫폼에서 제공하는 API 또는 SDK를 사용해야 했다. 이 중에서 '카카오내비 길찾기 SDK'(링크)는 KNRGCode(링크)라는 회전 구간 경로 분류 코드를 내려주는데 별도의 구현없이 필요한 정보를 얻을 수 있어서 선택하게 됐다.

 

SK Open API 지오코딩-좌표변환 API / 경로안내-직선 거리 계산 API

기기에서 GPS를 통해 얻는 좌표는 WGS84 좌표계다. 하지만 카카오내비에서는 KATECH 좌표계를 사용하기 때문에 좌표변환이 필요했다. 카카오에서 제공하는 좌표변환 API는 KATECH을 지원하지 않았기에 다른 API를 사용해야 했다. 'SK open API의 지오코딩-좌표변환 API'(링크)는 KATECH 좌표계를 지원하여 사용하게 됐다.

 

다음 경로까지의 거리는 SK Open API의 '직선 거리 계산 API' (링크)를 사용하려고 한다. 해당 API는 KATECH 좌표를 지원하기 때문에 좌표 변환 없이 거리를 구할 수 있다.

 

 Naver Moblie Dynamic Map API

출발지와 도착지 설정을 위해 네이버의 'Moblie Dynamic Map API'(링크)를 사용했다.

 

카카오 키워드 장소 검색 API

출발지는 현재 위치를 지정할 수 있지만 도착지의 경우 사용자가 지정할 필요가 있다. 사용자가 원하는 장소를 검색하고 지정할 수 있도록 카카오의 '키워드 장소 검색 API'(링크)를 사용했다.

 


API 키 관리

local.properties

프로젝트를 GitHub에 올리기 때문에 코드에 Key 값을 그대로 사용하지 않고 local.properties에 등록하여 사용했다.

 

android {
    // 생략
    defaultConfig {
    	// 생략
        fun key(pKey:String): String = gradleLocalProperties(rootDir).getProperty(pKey) ?: ""
        buildConfigField("String", "KAKAO_NATIVE_APP_KEY", key("KAKAO_NATIVE_APP_KEY"))
        buildConfigField("String", "KAKAO_REST_API_KEY", key("KAKAO_REST_API_KEY"))
        buildConfigField("String", "SK_APP_KEY", key("SK_APP_KEY"))
        buildConfigField("String", "USER_KEY", key("USER_KEY"))
        buildConfigField("String", "SK_BASE_URL", key("SK_BASE_URL"))
        buildConfigField("String", "KAKAO_BASE_URL", key("KAKAO_BASE_URL"))
        buildConfigField("String", "NAVER_CLIENT_ID", key("NAVER_CLIENT_ID"))

        manifestPlaceholders["NAVER_CLIENT_ID"] = key("NAVER_CLIENT_ID")
    }
}

local.properties에 등록한 Key 값을 코드에 사용하기 위해서 gradle에 위와 같은 작업을 했다.

위 작업 이후부터는 아래처럼 Manifest와 코드에서 Key 값에 접근하여 사용할 수 있다.

 

// Manifest
<meta-data
    android:name="com.naver.maps.map.CLIENT_ID"
    android:value="${NAVER_CLIENT_ID}"/>
    
    

// MapRepository
fun getSearchPlaceData(query: String, x: String, y: String) : SearchPlaceResponse? {
    return mapService.searchPlaceRequest(
        Authorization = "KakaoAK ${com.navirotation.BuildConfig.KAKAO_REST_API_KEY}",
	...
    ).execute().body()
}

출발지 설정하기

지도 화면 진입 시 플로우
1. 위치 권한 체크
2. 현재 위치 좌표 값 업데이트
3. Naver Map 객체 요청
4. 현재 위치로 카메라 이동 및 마커 표시

 

◾ 위치 권한 체크

    /**
     * MapActivity
     */
    
    // 위치 권한 Array
    private val permissionArray = arrayOf(
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_FINE_LOCATION
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        // 생략

        /**
         * 권한 체크
         * - 권한 허용 O : 현재 위치 좌표 값 업데이트
         * - 권한 허용 X : 권한 요청
         */
        if(permissionArray.all{
                ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
        }) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                viewModel.getInitMyLocationData(fusedLocationClient)
            } else {
                myLatitude = 37.55453
                myLongitude = 126.97071
                binding.mapView.getMapAsync(this)
            }
        } else {
            requestPermissions(permissionArray, 1)
        }
    }

지도 화면 진입 시 가장 먼저 위치 권한을 체크한다. 위치 권한을 체크하여 허용되어 있을 경우 ViewModel에서 현재 위치 정보를 얻는다. 단, FusedLocationProviderClient는 안드로이드 S(31버전)부터 사용이 가능하기 때문에 그 아래 버전은 임시로 서울역 좌표로 지정했다.

 

 현재 위치 좌표 값 업데이트

/**
 * MapActivity
 */

viewModel.initMyLocationData.observe(this) {
    Log.d(NAVI_ROTATION, "getInitMyLocationData(): $it")

    if (it != null) {
        myLatitude = it.latitude
        myLongitude = it.longitude
    } else {
        // 위치 정보를 얻지 못했을 경우 임시 좌표 값
        myLatitude = 37.55453
        myLongitude = 126.97071
    }

    binding.mapView.getMapAsync(this)

    setMyLocationMarker()
    setLocationCamera(myLatitude, myLongitude)
}

VIewModel에서 위치 정보를 얻으면 observe를 하던 initMyLocationData에서 현재 위치 좌표 값을 업데이트 한다. 업데이트 이후 Naver Map 객체를 얻고, 현재 위치로 카메라를 이동과 마커를 표시했다.


도착지 설정하기

도착지 설정 Flow
1. 키워드 장소 검색 API 호출 
2. 검색 리스트 표시 
3. Naver Map에 도착지 정보 전달
4. 도착지 상세 뷰 표시

 

검색 리스트 표시하기

/**
 * MapActivity
 */
 
private fun onClickListener() {
    // 생략

    binding.etSearch.setOnClickListener {
        val intent = Intent(this, SearchActivity::class.java)
        intent.putExtra("latitude", myLatitude.toString())
        intent.putExtra("longitude", myLongitude.toString())
        getSearchResult.launch(intent)
    }
}

카카오의 '키워드 장소 검색 API'는 파라미터로 좌표 값을 전달하면 거리 값인 distance를 반환하기 때문에  MapActivity에서 검색 UI를 클릭하면 현재 좌표(출발지) 값을 담아서 SearchActivity로 이동한다. 

 

/**
 * SearchActivity
 */
 
viewModel.searchPlaceData.collectLatest {
    Log.d(NAVI_ROTATION, "searchPlaceData: $it")

    // todo : 에러 핸들링

    if(it.success == null ) return@collectLatest

    val meta = it.success.meta
    val documents = it.success.documents

    if(meta.totalCount == 0) {
        binding.tvEmptyList.setText(R.string.empty_search_list)
        binding.tvEmptyList.visibility = View.VISIBLE
    } else {
        binding.tvEmptyList.visibility = View.GONE
        if(meta.totalCount == 1) {
            searchList.add(documents[0])
        }else {
            searchList = documents as ArrayList<Document>
        }
    }

    adapter.setItem(searchList)
}

StateFlow인 searchPlaceData를 Collect하여 API를 호출했을 때 이벤트를 처리했다. API 호출 결과 데이터가 없을 경우 EmptyView를 보여주고, 있을 경우 RecyclerView Adapter에 데이터를 넘겨줬다.

 

/**
 * SearchActivity
 */
 
adapter.setOnItemClickListener(object : ItemClickListener{
    override fun onItemClickListener(v: View, data: Document, pos: Int) {
        val intent = Intent()
        intent.putExtra("placeName", data.placeName)
        intent.putExtra("addressName", data.addressName)
        intent.putExtra("distance", data.distance)
        intent.putExtra("latitude", data.y.toDouble())
        intent.putExtra("longitude", data.x.toDouble())
        setResult(RESULT_OK, intent)
        finish()
    }
})

검색 리스트에서 항목을 클릭하면 도착지 정보를 갖고 MapActivity로 돌아가도록 했다.

(왼) 검색 전 / (오) 검색 후

 

 도착지 상세 뷰 표시

/*
 * MapActivity
 */
 
private val getSearchResult =
    registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        if(result.resultCode == RESULT_OK) {
            if(result.data != null) {
                // 목적지 상세 View 노출
                binding.etSearch.setText(result.data!!.getStringExtra("placeName"))
                binding.tvPlaceName.text = result.data!!.getStringExtra("placeName")
                binding.tvAddressName.text = result.data!!.getStringExtra("addressName")
                binding.tvDistance.text = result.data!!.getStringExtra("distance")
                binding.ctDetail.visibility = View.VISIBLE

                // 도착지 좌표 업데이트
                endLatitude = result.data!!.getDoubleExtra("latitude", 0.0)
                endLongitude = result.data!!.getDoubleExtra("longitude", 0.0)
            }

            // 목적지 마커 설정
            endMarker.map = null
            endMarker.width = 70
            endMarker.height = 100
            endMarker.icon = MarkerIcons.BLACK
            endMarker.iconTintColor = Color.RED
            endMarker.position = LatLng(endLatitude, endLongitude)
            endMarker.map = naverMap

            setLocationCamera(endLatitude, endLongitude)
        }else {
            // todo : 그냥 돌아왔을 때 처리하게 있나?
        }
    }

SearchActivity에서 받아 온 데이터로 도착지 상세 뷰를 그려주고 도착지 좌표를 업데이트하여 마커 표시와 카메라 이동을 해준다.

 

 

전체 코드

댓글