본문 바로가기
Android App/Kotlin

네트워크-데이터통신/Open API 사용, 마커설정(*레트로핏(Retrofit) 및 converter-gson, Glide 라이브러리)

by AppJinny 2022. 12. 9.

*네트워크-데이터통신/Open API 사용, 마커설정(*레트로핏(Retrofit) 및 converter-gson, Glide 라이브러리)

-Open API : 데이터 또는 서비스를 공개해 일반 개발자들이 사용할 수 있도록 제공하는 인터페이스

-Open API는 주로 인터넷 주소 형태로 제공됨

-마커설정 : 마커에 tag를 달아 클릭리스너 이벤트 적용 및 클러스터링(묶어서 하나의 그룹으로 표시) 가능

 

 

*도서관 현황정보 Open API 사용하여 앱 구현하기

-Open API 데이터 제공(서울 열린데이터 광장) : https://data.seoul.go.kr/

-서울특별시 공공도서관 현황정보 API 데이터 사용 : https://data.seoul.go.kr/dataList/OA-15480/S/1/datasetView.do

-인증키 신청 및 발급

-서울특별시 공공도서관 현황정보 Open API 구조

--문서 형식 : JSON과 XML지원, API에 따라 둘 중 하나만 지원할수도 있음

--서비스ID : 서비스를 구분하는 ID

--요청개수 : 한 번에 요청하는 개수, 트레픽 문제로인해 1,000개까지 요청 가능함, 1,000개가 넘어가면 페이지 값을 증가시키면서 요청

-https://data.seoul.go.kr/dataList/OA-15480/S/1/datasetView.do 하단 출력값 중 일부 데이터 사용

 

-지도 정보가 필요하므로 Google Maps Activity로 프로젝트 생성

 

-AndroidManifest.xml

-- 구글 API키 추가 참고(https://heeyjinny.tistory.com/109, https://heeyjinny.tistory.com/108)

--위치권한 및 인터넷 권한 명세

    <!--  위치권한 2가지 명세  -->
    <!--  도시 블록 내에서의 정확한 위치(네트워크 위치)  -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <!--  정확한 위치 확보(네트워크 + GPS위치)  -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

    <!--  인터넷 권한 명세  -->
    <uses-permission android:name="android.permission.INTERNET"/>

<!--  도서관 정보 API가 HTTPS(보안프로토콜)이 아닌
        HTTP를 사용하기 때문에 usesCleartextTraffic="true" 추가  -->
    <application

        android:usesCleartextTraffic="true"

 

-build.gradle

-- android{} 에 뷰바인딩 추가

--보안저장한 APIKey 적용

--dependencies{} 에 레트로핏, JSON컨버터 라이브러리 추가

//뷰바인딩
buildFeatures{
    viewBinding true
}
//local.properties 불러오기
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
defaultConfig {

    //타입 - 키(변수명) - 정의된 키
    buildConfigField "String", "LBRRY_DOMAIN", properties ['LBRRY_DOMAIN']
    buildConfigField "String", "LBRRY_API_KEY", properties ['LBRRY_API_KEY']
    buildConfigField "String", "MAP_API_KEY", properties ['MAP_API_KEY']
//레트로핏, JSON컨버터 라이브러리 추가
dependencies {

    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

 

-MapsActivity.kt

/**  서울 공공도서관 앱 개발하기  **/

//1
//구글 지도 API키 추가
//레트로핏, JSON컨버터 라이브러리 추가
//build.gradle(:app)

//2
//위치권한 및 인터넷권한 추가
//AndroidManifest.xml

//3
//사용할 JSON 샘플데이터를 사용하여
//코틀린 데이터 클래스 생성
//클래스의 개수가 여러 개일 경우 관리를 위해
//기본 패키지 밑에 data패키지 생성
//패키지 우클릭 - New - Package - data패키지 생성

//data패키지 폴더 우클릭 - New - Kotlin data class File from Json 클릭
//빈 여백에 샘플 데이터 붙여넣기, Format으로 정렬 후
//Class Name은 Library 입력

//4
//Open API 사용을 위해
//기본정보를 담아두는 클래스와
//클래스 안에 레트로핏에서 사용할 인터페이스 생성

class MapsActivity : AppCompatActivity(), OnMapReadyCallback {

    private lateinit var mMap: GoogleMap
    private lateinit var binding: ActivityMapsBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMapsBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Obtain the SupportMapFragment and get notified when the map is ready to be used.
        val mapFragment = supportFragmentManager
            .findFragmentById(R.id.map) as SupportMapFragment
        mapFragment.getMapAsync(this)

    }//onCreate

    /**
     * Manipulates the map once available.
     * This callback is triggered when the map is ready to be used.
     * This is where we can add markers or lines, add listeners or move the camera. In this case,
     * we just add a marker near Sydney, Australia.
     * If Google Play services is not installed on the device, the user will be prompted to install
     * it inside the SupportMapFragment. This method will only be triggered once the user has
     * installed Google Play services and returned to the app.
     */
    override fun onMapReady(googleMap: GoogleMap) {
        mMap = googleMap

        //8
        //loadLibraries()호출
        loadLibraries()

        //9-2
        //마커에 tag단 것을 이용해
        //마커를 클릭했을 때 홈페이지 주소를 웹브라우저로 생성
        mMap.setOnMarkerClickListener {

            //9-3
            //마커의 tag가 null값이 아니라면
            if (it.tag != null){

                //9-4
                //마커의 tag를 String으로 형변환하고
                //tag가 http로 시작하지 않으면
                //http://문자열을 앞에 추가
                var url = it.tag as String
                if (!url.startsWith("http")){
                    url = "http://${url}"
                }
                //9-5
                //완성된 url을 intent로 생성한 후
                //액티비티 호출
                val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
                startActivity(intent)
            }
            true
        }

    }//onMapReady

    //5
    //정의한 인터페이스를 정의하고 데이터 불러오는 코드 작성
    //loadLibraries() 메서드 생성
    fun loadLibraries(){

        //5-1
        //도메인 주소와 JSON컨버터를 설정하여
        //레트로핏 생성
        val retrofit = Retrofit.Builder()
            .baseUrl(SeoulOpenApi.DOMAIN)
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        //5-2
        //레트로핏을 사용해 정의해두었던 인터페이스를
        //실행가능한 서비스 객체로 변환
        val seoulOpenService = retrofit.create(SeoulOpenService::class.java)

        //5-3
        //인터페이스에 정의된 getLibrary()메서드에 파라미터로
        //SeoulOpenApi의 API_KEY를 입력하고
        //enqueue()메서드를 호출하여 서버에 요청
        seoulOpenService.getLibrary(SeoulOpenApi.API_KEY).enqueue(object: Callback<Library>{

            //5-4
            //인터페이스의 코드 2개 자동 생성
            override fun onResponse(call: Call<Library>, response: Response<Library>) {

                //5-5
                //서버 요청이 정상적으로 되었다면
                //지도에 마커를 표시하는 메서드 호출(마커 표시 메서드 아래에 생성필요...)
                showLibraries(response.body() as Library)

            }

            override fun onFailure(call: Call<Library>, t: Throwable) {

                //5-6
                //서버 요청이 실패했을 경우 토스트메시지 생성
                Toast.makeText(this@MapsActivity, "서버요청 실패", Toast.LENGTH_SHORT).show()
            }

        })

    }//loadLibraries

    //6
    //지도에 도서관 마커 표시하는 메서드생성
    fun showLibraries(libraries: Library){

        //7
        //마커가 지도에 표기되지만 지도를 보여주는 카메라는 시드니를 가리킴
        //카메라 위치조정 필요
        //마커 전체의 영역으로 먼저 구해
        //마커의 영역만큼 보여주는 코드 작성
        //마커의 영역으로 저장하는 LatLngBounds.Builder생성
        val latLngBounds = LatLngBounds.Builder()

        //6-1
        //파라미터로 전달된 libraries의
        //SeoulPublicLibraryInfo.row에 도서관 목록이 있음
        //반복문으로 하나씩 꺼내어 미커 생성하여 추가
        for (lib in libraries.SeoulPublicLibraryInfo.row){

            //6-2
            //마커좌표 생성
            val position = LatLng(lib.XCNTS.toDouble(), lib.YDNTS.toDouble())

            //6-3
            //좌표와 도서관 이름으로 마커 생성
            val marker = MarkerOptions().position(position).title(lib.LBRRY_NAME)

            //6-4
            //마커를 지도에 추가
            //mMap.addMarker(marker)

            //9
            //도서관 이름 클릭 시 홈페이지로 이동하기
            //도서관 홈페이지 URL검사 후
            //홈페이지를 웹 프라우저에 띄우는 코드 작성
            //9-1
            //6-4코드 수정
            //마커에 tag정보 추가
            //지도에 마커를 추가하고 그 마커 tag값에 홈페이지 주소 저장
            var obj = mMap.addMarker(marker)
            obj?.tag = lib.HMPG_URL

            //7-1
            //지도에 마커 추가 후 latLngBounds에도 마커 추가
            latLngBounds.include(marker.position)

        }

        //7-2
        //앞에서 저장해둔 마커의 영역 구하기
        //padding변수는 마커의 영역에 여백을 얼마나 줄 것인지 정함
        val bounds = latLngBounds.build()
        val padding = 0

        //7-3
        //카메라 업데이트
        //bounds와 padding설정하여 카메라 업데이트
        val updated = CameraUpdateFactory.newLatLngBounds(bounds, padding)

        //7-4
        //업데이트된 카메라 지도에 반영
        mMap.moveCamera(updated)

    }//showLibraries

}//MapsActivity

 

-SeoulOpenApi.kt

//1
//Open API 사용을 위해
//기본정보를 담아두는 클래스와
//클래스 안에 레트로핏에서 사용할 인터페이스 생성
class SeoulOpenApi {

    //2
    //companion object{}를 만들어
    //도메인 주소와 API키 저장해놓은 변수 2개 생성
    //companion object: 블록안에 변수 생성 시 '클래스명.변수명' 형식으로
    //바로 사용할 수 있음
    companion object{
        val DOMAIN = BuildConfig.LBRRY_DOMAIN
        val API_KEY = BuildConfig.LBRRY_API_KEY
    }

}//SeoulOpenApi

//3
//레트로핏에서 사용할 인터페이스 SeoulOpenService생성
interface SeoulOpenService{

    //3-1
    //도서관 데이터를 가져오는 getLibrary()메서드 정의
    //@GET 애노테이션 사용해 호출할 주소 지정
    //레트로핏에서 사용할 때 @GET에 입력된 주소와
    //정의해둔 DOMAIN주소를 조합해 사용할 것임

    //getLibrary()메서드의 파라미터로 사용된 key는
    //SeoulOpenApi클래스에 정의한 API_KEY를
    //레트로핏을 실행하는 코드에서 넘겨받은 후 주소와 결합하고
    //반환 값은 Call<JSON변환된 클래스>

    //도서관의 개수가 총 120개 이므로 한 페이지에 모두 불러오기 위해
    //주소 끝 부분에 페이지1/가져올개수200 입력

//    @GET("/json/SeoulPublicLibraryInfo/1/200/")
//    fun getLibrary(key: String): Call<Library>

    //위 코드 수정하기...
    //@Path애노테이션 사용
    //메서드의 파라미터로 넘어온 값을
    //@GET에 정의된 주소에 동적으로 삽일할 수 있음...

    //메서드의 파라미터 변수 앞에 @Path 애노테이션으로
    //@GET 주소에 매핑할 이름 작성하고
    //@GET문자열에 {매핑할 이름}형식으로 삽입하면
    //메서드가 호출되는 순간 매핑할 이름이
    //정의된 파라미터의 값으로 대체된 후 사용됨

    @GET("{api_key}/json/SeoulPublicLibraryInfo/1/200/")
    fun getLibrary(@Path("api_key") key: String): Call<Library>

}

//4
//정의한 인터페이스를 정의하고 데이터 불러오는 코드 작성
//MapsActivity.kr

 

*결과

 

 

*맵 클러스터링 사용하여 마커 설정

-구글 맵 클러스터링(Google Map Clustering)

--지도에 나타나는 여러 개의 마커를 묶어 하나의 그룹으로 표시할 수 있음

--클러스터 매니저(ClusterManager)를 통해 사용

--https://developers.google.com/maps/documentation/android-sdk/utility/marker-clustering

--위 프로젝트에 이어서 진행

 

-build.gradle

--dependencies{} 에 클러스터 아이템 라이브러리 추가

--https://developers.google.com/maps/documentation/android-sdk/utility/setup

//++ 클러스터 아이템 라이브러리 추가
dependencies {

    implementation 'com.google.maps.android:android-maps-utils:2.3.0'

 

-Row.kt

import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.clustering.ClusterItem

//++1
//마커에 해당하는 클래스가 Row클래스 이기 때문에
//클러스터아이템 상속 받은 후
//좌표를 반환하는 함수와 부가 정보를 반환하는 함수들 구현
data class Row(
    val ADRES: String,
    val CODE_VALUE: String,
    val FDRM_CLOSE_DATE: String,
    val GU_CODE: String,
    val HMPG_URL: String,
    val LBRRY_NAME: String,
    val LBRRY_SEQ_NO: String,
    val LBRRY_SE_NAME: String,
    val OP_TIME: String,
    val TEL_NO: String,
    val XCNTS: String,
    val YDNTS: String
): ClusterItem {
    //++2
    //데이터 클래스에 클러스터 아이템을 추가하고 필수 메서드 오버라이드

    //++3
    //개별 마커가 표시될 좌표
    override fun getPosition(): LatLng {
        return LatLng(XCNTS.toDouble(), YDNTS.toDouble())
    }

    //++4
    //마커를 클릭했을 때 나타나는 타이틀
    override fun getTitle(): String? {
        return LBRRY_NAME
    }

    //++5
    //마커를 클릭했을 때 나타나는 서브 타이틀
    override fun getSnippet(): String? {
        return ADRES
    }

    //++6
    //id에 해당하는 유일한 값을 Int로 반환
    //값 중에 null이 있을 경우 hashCoda생성 시 오류 발생
    override fun hashCode(): Int {
        return LBRRY_SEQ_NO.toInt()
    }

}

//++7
//앱이 실행되고 지도에 마커표시가 될 때
//안드로이드는 Row클래스의 getPosition()메서드 호출
//해당 마커의 좌표 계산한 뒤
//특정 범위 안에 있는 마커들을 묶어 하나의 마커로 만들고
//몇 개의 마커가 포함되어 있는지 숫자로 표시함
//클러스터 매니저를 통해 클러스터 사용...
//MapsActivity.kt 에 선언

 

-MapsActivity.kt

--++1 ~ ++5 참고

/**  서울 공공도서관 앱 개발하기  **/
/**  ++구글 맵 클러스터링 사용(마커)  **/

//++1 구글 맵 클러스터링
//지도에 나타나는 여러 개의 마커를 묶어 하나의 그룹으로 표시
//마커에 해당하는 클래스가 Row클래스 이기 때문에
//data - Row클래스에서 클러스터아이템 상속 받은 후
//좌표를 반환하는 함수와 부가 정보를 반환하는 함수들 구현
//Row.kt 수정

//1
//구글 지도 API키 추가
//레트로핏, JSON컨버터 라이브러리 추가
//build.gradle(:app)

//2
//위치권한 및 인터넷권한 추가
//AndroidManifest.xml

//3
//사용할 JSON 샘플데이터를 사용하여
//코틀린 데이터 클래스 생성
//클래스의 개수가 여러 개일 경우 관리를 위해
//기본 패키지 밑에 data패키지 생성
//패키지 우클릭 - New - Package - data패키지 생성

//data패키지 폴더 우클릭 - New - Kotlin data class File from Json 클릭
//빈 여백에 샘플 데이터 붙여넣기, Format으로 정렬 후
//Class Name은 Library 입력

//4
//Open API 사용을 위해
//기본정보를 담아두는 클래스와
//클래스 안에 레트로핏에서 사용할 인터페이스 생성

class MapsActivity : AppCompatActivity(), OnMapReadyCallback {

    //++2
    //클러스터 매니저를 통해 클러스터 사용...
    //클러스터 매니저 프로퍼티 선언
    private lateinit var clusterManager: ClusterManager<Row>

    private lateinit var mMap: GoogleMap
    private lateinit var binding: ActivityMapsBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMapsBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Obtain the SupportMapFragment and get notified when the map is ready to be used.
        val mapFragment = supportFragmentManager
            .findFragmentById(R.id.map) as SupportMapFragment
        mapFragment.getMapAsync(this)

    }//onCreate

    /**
     * Manipulates the map once available.
     * This callback is triggered when the map is ready to be used.
     * This is where we can add markers or lines, add listeners or move the camera. In this case,
     * we just add a marker near Sydney, Australia.
     * If Google Play services is not installed on the device, the user will be prompted to install
     * it inside the SupportMapFragment. This method will only be triggered once the user has
     * installed Google Play services and returned to the app.
     */
    override fun onMapReady(googleMap: GoogleMap) {
        mMap = googleMap

        //++3
        //클러스터 매니저 초기화 및 설정
        //마커를 표시하기 전 설정
        //Map이 생성 된 직후 설정함
        clusterManager = ClusterManager(this, mMap)
        //++3-1
        //화면 이동 후 멈췄을 때 설정
        mMap.setOnCameraIdleListener(clusterManager)
        //++3-2
        //마커 클릭 설정
        mMap.setOnMarkerClickListener(clusterManager)

        //8
        //loadLibraries()호출
        loadLibraries()

        //++4
        //기존 마커 세팅 코드 삭제
//        //9-2
//        //마커에 tag단 것을 이용해
//        //마커를 클릭했을 때 홈페이지 주소를 웹브라우저로 생성
//        mMap.setOnMarkerClickListener {
//
//            //9-3
//            //마커의 tag가 null값이 아니라면
//            if (it.tag != null){
//
//                //9-4
//                //마커의 tag를 String으로 형변환하고
//                //tag가 http로 시작하지 않으면
//                //http://문자열을 앞에 추가
//                var url = it.tag as String
//                if (!url.startsWith("http")){
//                    url = "http://${url}"
//                }
//                //9-5
//                //완성된 url을 intent로 생성한 후
//                //액티비티 호출
//                val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
//                startActivity(intent)
//            }
//            true
//        }

    }//onMapReady

    //5
    //정의한 인터페이스를 정의하고 데이터 불러오는 코드 작성
    //loadLibraries() 메서드 생성
    fun loadLibraries(){

        //5-1
        //도메인 주소와 JSON컨버터를 설정하여
        //레트로핏 생성
        val retrofit = Retrofit.Builder()
            .baseUrl(SeoulOpenApi.DOMAIN)
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        //5-2
        //레트로핏을 사용해 정의해두었던 인터페이스를
        //실행가능한 서비스 객체로 변환
        val seoulOpenService = retrofit.create(SeoulOpenService::class.java)

        //5-3
        //인터페이스에 정의된 getLibrary()메서드에 파라미터로
        //SeoulOpenApi의 API_KEY를 입력하고
        //enqueue()메서드를 호출하여 서버에 요청
        seoulOpenService.getLibrary(SeoulOpenApi.API_KEY).enqueue(object: Callback<Library>{

            //5-4
            //인터페이스의 코드 2개 자동 생성
            override fun onResponse(call: Call<Library>, response: Response<Library>) {

                //5-5
                //서버 요청이 정상적으로 되었다면
                //지도에 마커를 표시하는 메서드 호출(마커 표시 메서드 아래에 생성필요...)
                showLibraries(response.body() as Library)

            }

            override fun onFailure(call: Call<Library>, t: Throwable) {

                //5-6
                //서버 요청이 실패했을 경우 토스트메시지 생성
                Toast.makeText(this@MapsActivity, "서버요청 실패", Toast.LENGTH_SHORT).show()
            }

        })

    }//loadLibraries

    //6
    //지도에 도서관 마커 표시하는 메서드생성
    fun showLibraries(libraries: Library){

        //7
        //마커가 지도에 표기되지만 지도를 보여주는 카메라는 시드니를 가리킴
        //카메라 위치조정 필요
        //마커 전체의 영역으로 먼저 구해
        //마커의 영역만큼 보여주는 코드 작성
        //마커의 영역으로 저장하는 LatLngBounds.Builder생성

        //latLngBounds 는 전체 마커를 화면에 보여주기 위한 용도
        //현재 내 위치 기준으로 마커표시한다면 필요하지 않음...
        val latLngBounds = LatLngBounds.Builder()

        //6-1
        //파라미터로 전달된 libraries의
        //SeoulPublicLibraryInfo.row에 도서관 목록이 있음
        //반복문으로 하나씩 꺼내어 미커 생성하여 추가
        //++5
        //반복문에 마커 생성코드 삭제
        //클러스터 메니저에 직접 데이터 추가
        for (lib in libraries.SeoulPublicLibraryInfo.row){

            //++5-1
            //클러스터 매니저에 데이터 추가
            clusterManager.addItem(lib)

            //++5-2
            //첫 화면에 보여줄 범위를 정하기 위해 코드 남겨두기...
            //6-2
            //마커좌표 생성
            val position = LatLng(lib.XCNTS.toDouble(), lib.YDNTS.toDouble())
//
//            //6-3
//            //좌표와 도서관 이름으로 마커 생성
//            val marker = MarkerOptions().position(position).title(lib.LBRRY_NAME)
//
//            //6-4
//            //마커를 지도에 추가
//            mMap.addMarker(marker)
//
//            //9
//            //도서관 이름 클릭 시 홈페이지로 이동하기
//            //도서관 홈페이지 URL검사 후
//            //홈페이지를 웹 프라우저에 띄우는 코드 작성
//            //9-1
//            //6-4코드 수정
//            //마커에 tag정보 추가
//            //지도에 마커를 추가하고 그 마커 tag값에 홈페이지 주소 저장
//            var obj = mMap.addMarker(marker)
//            obj?.tag = lib.HMPG_URL
//
            //++5-2
            //첫 화면에 보여줄 범위를 정하기 위해 코드 남겨두기...
            //7-1
            //지도에 마커 추가 후 latLngBounds에도 마커 추가
            latLngBounds.include(position)

        }

        //7-2
        //앞에서 저장해둔 마커의 영역 구하기
        //padding변수는 마커의 영역에 여백을 얼마나 줄 것인지 정함
        val bounds = latLngBounds.build()
        val padding = 0

        //7-3
        //카메라 업데이트
        //bounds와 padding설정하여 카메라 업데이트
        val updated = CameraUpdateFactory.newLatLngBounds(bounds, padding)

        //7-4
        //업데이트된 카메라 지도에 반영
        mMap.moveCamera(updated)

    }//showLibraries

}//MapsActivity

 

*결과

 

 

 

 


이 포스팅에 작성한 내용은 고돈호, ⌜이것이 안드로이드다⌟, 한빛미디어(주), 2022 에서 발췌하였습니다.