[Android/Kotlin] Naver Map API 사용법 및 예제(2)

최지원·2024년 1월 28일
0

[Android/Kotlin]

목록 보기
3/9

🧾[Android/Kotlin] Naver Map API 사용법 및 예제(2)

📌네이버 맵 사용법

저번 포스트에서 앱에 네이버 지도를 띄우는 부분까지 했었죠? 이번 포스트는 네이버 지도 사용법과 Room을 이용하여 장소 저장하기 까지 구현해서 DB를 응용하는 예제를 해봅시다.

우선 다른 로직을 구현하기 전에 MapView에 네이버맵을 띄우기 위해선 MapView가 포함된 액티비티의 LifeCycle에 맞추어 onCreate(), onStart(), onResume(), onPause(), onStop(), onDestroy(), onSaveInstanceState(), onLowMemory()를 호출해야 합니다.

단, MapView가 프래그먼트에 포함될 경우 프래그먼트의 onCreateView() 또는 onViewCreated()에서 onCreate()를, onDestroyView()에서 onDestroy()를 호출해야 합니다. 그렇지 않으면 지도가 정상적으로 동작하지 않습니다. MapFragment를 사용하면 이러한 절차가 필요하지 않으므로 MapFragment를 사용하는 것을 권장합니다.

MapActivity.kt

override fun onStart() {
        super.onStart()
        mapView.onStart()
    }

    override fun onResume() {
        super.onResume()
        mapView.onResume()
    }

    override fun onPause() {
        super.onPause()
        mapView.onPause()
    }

    override fun onStop() {
        super.onStop()
        mapView.onStop()
    }

    override fun onDestroy() {
        super.onDestroy()
        mapView.onDestroy()
    }

다음은 MapActivity에서 MapView를 띄울 때 추가해야하는 LifeCycle입니다. 이렇게 LifeCycle메서드와 함께 관리함으로써 안정성과 성능을 유지할 수 있습니다.

📌 위치 & 인터넷 권한 부여받기

  • 인터넷 권한 받는 방법

AndroidManifest.xml

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

다음과 같이 매니페스트 파일에 추가하면 됩니다.

  • 위치 권한 받는 방법

AndroidManifest.xml

<!-- 사용자 위치 권한 -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

마찬가지로 이런식으로 추가하시면 됩니다.

ACCESS_COARSE_LOCATION은 대략적인 위치 정보에 접근, ACCESS_FINE_LOCATION는 정확한 위치 정보에 접근할 수 있도록 허용합니다.

MapActivity.kt

override fun onMapReady(map: NaverMap){
	// 사용자에게 실제로 권한 요청
    ActivityCompat.requestPermissions(
        this, PERMISSION,
        LOCATION_PERMISSION_REQUEST_CODE
    )
}

private var PERMISSION = arrayOf(
    android.Manifest.permission.ACCESS_FINE_LOCATION,
    android.Manifest.permission.ACCESS_COARSE_LOCATION
)

companion object {
    private const val LOCATION_PERMISSION_REQUEST_CODE = 1000
}

MapActivity에 다음과 같이 사용자에게 위치 정보 권한을 받을 수 있습니다. 이렇게 하면 처음 맵이 실행될 때 권한 요청이 뜨고, 사용자가 허용할 시 위치 정보를 사용할 수 있는 상태가 됩니다.

📌 네이버 맵 기본 설정 하는법

MapAcitivty.kt


	private lateinit var locationSource: FusedLocationSource

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

    	// FusedLocationSource : 위치 정보를 관리하고 요청하는데 사용되는 클래스.
    	locationSource = FusedLocationSource(this, 	LOCATION_PERMISSION_REQUEST_CODE)
        getMarkers()
	}
    
	override fun onMapReady(map: NaverMap){
        binding.currentLocationButton.map = naverMap
        naverMap?.locationSource = locationSource
        val uiSetting = naverMap?.uiSettings
        uiSetting?.isLocationButtonEnabled = false

        naverMap?.maxZoom = 18.0
        naverMap?.minZoom = 10.0
    }

현재 위치 버튼(currentLocationButton)과 네이버 맵 UI설정 코드입니다. 또한 최대, 최소줌을 각각 18.0, 10.0으로 설정합니다.

override fun onMapReady(map: NaverMap){
	naverMap?.setOnMapClickListener{ _, coord ->
            blueMarker?.map = null

            val bMarker = Marker()
            bMarker.position = coord
            bMarker.icon = MarkerIcons.BLUE
            bMarker.map = naverMap

            blueMarker = bMarker
            getMarkers()
            showSaveDialog(coord)
        }
}

맵 클릭 시 발생하는 이벤트입니다. 클릭한 위치에 파란색 마커가 찍히고 getMarkers(), showSaveDialog() 메서드가 실행됩니다.

	private fun showSaveDialog(coord: LatLng) {
        val alertDialog = AlertDialog.Builder(this)
            .setMessage("장소를 저장하겠습니까?")
            .setPositiveButton("예") { dialogInterface: DialogInterface, i: Int ->
                val intent = Intent(this, SaveActivity::class.java)
                intent.putExtra("latitude", coord.latitude)
                intent.putExtra("longitude", coord.longitude)
                startActivity(intent)
            }
            .setNegativeButton("아니오", null)
            .create()

        alertDialog.show()
    }

showSaveDialog() : 장소를 저장하겠냐는 간단한 다이얼로그를 띄우는 메서드.

	private fun getMarkers(){
        CoroutineScope(Dispatchers.IO).launch {
            withContext(Dispatchers.Main){
                val markerList = database.dao().getAll()
                redMarker?.map = null

                for (marker in markerList){
                    val latlng = LatLng(marker.lat, marker.lng)
                    val mk = Marker()
                    mk.position = latlng
                    mk.iconTintColor = Color.RED
                    mk.map = naverMap
                    mk.onClickListener = this@MapActivity
                    mk.tag = marker

                    redMarker = mk
                }
            }
        }
    }

getMarkers() : 비동기적으로 Room DB에 저장된 모든 위치들을 가져와서 빨간 마커를 찍는 메서드.

	private val markerClickListener = object : Overlay.OnClickListener{
        override fun onClick(overlay: Overlay): Boolean {
            if (overlay is Marker){
                if (overlay == blueMarker){
                    showSaveDialog(overlay.position)
                    return true
                }

                val model = overlay.tag as? MarkerEntity
                model ?: return false

                infoWindow.adapter = object : InfoWindow.DefaultTextAdapter(applicationContext){
                    override fun getText(p0: InfoWindow): CharSequence {
                    
                        return model.title
                    }
                }
                infoWindow.open(overlay)
                return true
            }
            return false
        }

    }

	override fun onClick(p0: Overlay): Boolean {
        return markerClickListener.onClick(p0)
    }

마커 클릭 시 발생하는 이벤트입니다. 파란 마커일 시 showSaveDialog()메서드를 호출, 아닐 경우 RoomDB에 저장된 해당 마커의 제목을 InfoWindow를 통해 출력하여 사용자에게 표시합니다.

📌 Room DB를 활용한 네이버 맵 응용

activity_save.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="#009900"
        android:elevation="4dp"
        android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
        tools:ignore="MissingConstraints" />

    <EditText
        android:id="@+id/titleEditText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/toolbar"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_margin="20dp"
        android:hint="제목을 입력하세요!"/>

    <EditText
        android:id="@+id/contentEditText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/titleEditText"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_margin="20dp"
        android:hint="내용을 입력하세요!"/>

    <Button
        android:id="@+id/saveButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:text="저장하기!"
        android:textStyle="bold"
        android:textSize="20dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>

SaveActivity.kt

class SaveActivity : AppCompatActivity() {

    private lateinit var binding: ActivitySaveBinding
    private var latitude: Double = 0.0
    private var longitude: Double = 0.0
    private lateinit var database: MyDatabase

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

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

        database = MyDatabase.getDatabase(this)

        latitude = intent.getDoubleExtra("latitude", 0.0)
        longitude = intent.getDoubleExtra("longitude", 0.0)

        // 저장 버튼 클릭 시 데이터베이스에 저장
        binding.saveButton.setOnClickListener {
            val markerTitle = binding.titleEditText.text.toString()
            val markerContent = binding.contentEditText.text.toString()

            CoroutineScope(Dispatchers.IO).launch {
                database.dao().insert(MarkerEntity(
                    title = markerTitle,
                    content = markerContent,
                    lat = latitude,
                    lng = longitude
                ))
            }
            finish()
        }
    }
}

다음과 같이 사용자가 입력한 제목과 내용을 Room에 저장합니다.

MarkerDao.kt

@Dao
interface MarkerDao {

    @Insert
    suspend fun insert(entity: MarkerEntity)

    @Query("SELECT * FROM markerentity")
    suspend fun getAll(): List<MarkerEntity>

    @Query("DELETE FROM markerentity")
    suspend fun deleteAll()

}

MarkerEntity.kt

@Entity
data class MarkerEntity (
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val title: String,
    val content: String,
    val lat: Double,
    val lng: Double
)

MyDatabase.kt

@Database(entities = [MarkerEntity::class], version = 1)
abstract class MyDatabase : RoomDatabase(){

    abstract fun dao(): MarkerDao

    companion object{
        @Volatile
        private var INSTANCE: MyDatabase? = null

        fun getDatabase(context: Context): MyDatabase{
            return INSTANCE ?: synchronized(this){
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    MyDatabase::class.java,
                    "marker-database"
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}

Room DB에 관한 내용은 이전에 올린 포스트를 참고하시면 될 것 같습니다.

antivity_data.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/dataText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_marginTop="30dp"/>

    <Button
        android:id="@+id/backButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:text="메인으로"
        android:layout_marginBottom="20dp"/>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/deleteButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_margin="20dp"
        android:src="@drawable/baseline_delete_24"
        android:contentDescription="@string/delete" />

</androidx.constraintlayout.widget.ConstraintLayout>

DataActivity.kt

class DataActivity: AppCompatActivity() {

    private lateinit var binding: ActivityDataBinding
    private lateinit var database: MyDatabase

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

        database = MyDatabase.getDatabase(this)
        getMarkers()

        binding.deleteButton.setOnClickListener {
            deleteMarkers()
        }

        binding.backButton.setOnClickListener {
            finish()
        }
    }

    private fun getMarkers() {
        CoroutineScope(Dispatchers.IO).launch {
            val markers = database.dao().getAll().joinToString("\n\n")
            binding.dataText.text = markers
        }
    }

    private fun deleteMarkers(){
        CoroutineScope(Dispatchers.IO).launch {
            database.dao().deleteAll()
        }
    }
}

MainActivity.kt

class MainActivity : AppCompatActivity() {

    lateinit var binding : ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?){
        super.onCreate(savedInstanceState)

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

        binding.mapButton.setOnClickListener {
            val intent = Intent(this, MapActivity::class.java)
            startActivity(intent)
        }

        binding.dataButton.setOnClickListener {
            val intent = Intent(this, DataActivity::class.java)
            startActivity(intent)
        }
    }
}

antivity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/mapButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toTopOf="@id/dataButton"
        app:layout_constraintVertical_chainStyle="packed"
        android:text="지도 이동"/>

    <Button
        android:id="@+id/dataButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@id/mapButton"
        android:text="저장 확인"/>

</androidx.constraintlayout.widget.ConstraintLayout>

나머지 코드이며 MainActivity에서 MapActivity, DataActivity로 이동합니다. DataActivity에서 저장된 마커들의 정보들을 확인할 수 있으며 삭제도 가능합니다.

여기까지 네이버 맵 api를 사용하면서 Room DB까지 활용한 간단 예제를 살펴보았습니다. 감사합니다.

profile
안드로이드, 플러터 주니어입니다

0개의 댓글