[Android Studio] Airbnb 앱

jeunguri·2022년 12월 28일
0

토이 프로젝트

목록 보기
7/8


앱 소개


네이버 맵 api를 이용해 지도를 메인으로 숙소 목록을 서버에서 받아와 마커로 나타내고 하단에 viewPager를 이용해 좌우로 스크롤이 가능한 페이저를 두어 숙소를 살필 수 있다.

완성 화면


주요 기능

  • 네이버 맵 api 사용해 지도 보여주기
  • Mock api에서 예약 가능한 숙소 목록 받아와 지도에 표시
  • BottomSheetView를 활용해 예약 가능한 숙소 목록을 인터렉션하게 표시
  • viewPager2 를 이용해 현재 보고 있는 숙소를 표시
  • 숙소 버튼을 눌로 현재 보고 있는 숙소를 앱 외부로 공유

사용 기술

  • Naver Map Api
  • ViewPager2
  • CoordinatorLayout
  • BottomSheetBehavior
  • Retrofit
  • Glide
  • Mocky



의존성 추가


build.gradle

implementation 'com.naver.maps:map-sdk:3.11.0'
implementation 'com.google.android.gms:play-services-location:18.0.0'

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

implementation 'com.github.bumptech.glide:glide:4.12.0'


네이버 지도 api 사용하기


회원가입이 필요하며 결제수단을 등록해야만 네이버 맵을 사용할 수 있는 기능이 표시되기 때문에 꼭 결제수단을 등록해야 한다. 또한, 프로젝트 패키지명을 등록해주야 정상적으로 SDK를 받아올 수 있다.

네이버 클라우드 플랫폼

settings.gradle

repositories {
        maven {
            url 'https://naver.jfrog.io/artifactory/maven/'
        }
}


레이아웃 구성


layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
    tools:context=".MainActivity">

    <com.naver.maps.map.MapView
        android:id="@+id/mapView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="80dp" />

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/houseViewPager"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_gravity="bottom"
        android:layout_marginBottom="120dp"
        android:orientation="horizontal" />

    <com.naver.maps.map.widget.LocationButtonView
        android:id="@+id/currentLocationButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="top|start"
        android:layout_margin="12dp" />

    <include layout="@layout/bottom_sheet" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

layout/bottom_sheet.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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/top_radius_white_background"
    app:behavior_peekHeight="100dp"
    app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">

    <View
        android:layout_width="30dp"
        android:layout_height="3dp"
        android:layout_marginTop="12dp"
        android:background="#cccccc"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/bottomSheetTitleTextView"
        android:layout_width="0dp"
        android:layout_height="100dp"
        android:layout_marginTop="16dp"
        android:gravity="center"
        android:text="여러개의 숙소"
        android:textColor="@color/black"
        android:textSize="15sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <View
        android:id="@+id/lineView"
        android:layout_width="0dp"
        android:layout_height="1dp"
        android:background="#cccccc"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/bottomSheetTitleTextView" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/lineView"
        app:layout_constraintVertical_bias="1.0" />

</androidx.constraintlayout.widget.ConstraintLayout>

layout_behavior, behavior_peekHeight 속성을 이용해 아래에서 위로 스크롤하여 당기면 100dp만큼 올라올 수 있도록 해주었다.

layout/item_house.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="wrap_content">

    <ImageView
        android:id="@+id/thumbnailImageView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="24dp"
        app:layout_constraintDimensionRatio="3:2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/titleTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="12dp"
        android:layout_marginEnd="24dp"
        android:textColor="@color/black"
        android:textSize="20sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/thumbnailImageView"
        tools:text="강남역 도보 5분!! 쾌적한 환경!!" />

    <TextView
        android:id="@+id/priceTextView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="12dp"
        android:layout_marginEnd="24dp"
        android:layout_marginBottom="24dp"
        android:textColor="@color/black"
        android:textSize="20sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/titleTextView"
        tools:text="24,000원" />

</androidx.constraintlayout.widget.ConstraintLayout>

layout/item_house_detail_viewpager.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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"
    android:layout_marginStart="30dp"
    android:layout_marginEnd="30dp"
    android:background="@color/white"
    app:cardCornerRadius="16dp"
    tools:layout_height="100dp">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/thumbnailImageView"
            android:layout_width="100dp"
            android:layout_height="100dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/titleTextView"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="12dp"
            android:layout_marginTop="12dp"
            android:layout_marginEnd="12dp"
            android:maxLines="2"
            android:textColor="@color/black"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@id/thumbnailImageView"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="강남역 도보 5분!! 쾌적한 환경!!강남역 도보 5분!! 쾌적한 환경!!강남역 도보 5분!! 쾌적한 환경!!강남역 도보 5분!! 쾌적한 환경!!강남역 도보 5분!! 쾌적한 환경!!강남역 도보 5분!! 쾌적한 환경!!" />

        <TextView
            android:id="@+id/priceTextView"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="12dp"
            android:layout_marginEnd="12dp"
            android:maxLines="1"
            android:textColor="@color/black"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@id/thumbnailImageView"
            app:layout_constraintTop_toBottomOf="@id/titleTextView"
            tools:text="23,000원" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</androidx.cardview.widget.CardView>

drawable/top_radius_white_background.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <solid android:color="@color/white" />
    <corners
        android:topLeftRadius="30dp"
        android:topRightRadius="30dp" />
</shape>


지도 띄우기 및 Map 불러오기


valuse/api_key.xml

<resources>
    <string name="naver_map_client_id">client_id</string>
</resources>

앞서 만든 application 에서 인증정보/Client ID 을 작성해준다.

Manifest

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>

<meta-data
            android:name="com.naver.maps.map.CLIENT_ID"
            android:value="@string/naver_map_client_id"/>

MainActivity

class MainActivity : AppCompatActivity(), OnMapReadyCallback {

	companion object {
        private const val LOCATION_PERMISSION_REQUEST_CODE = 1000
	}

    private val mapView: MapView by lazy {
        findViewById<MapView>(R.id.mapView)
    }
    
    private lateinit var locationSource: FusedLocationSource
    private lateinit var naverMap: NaverMap

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mapView.onCreate(savedInstanceState)
        
        mapView.getMapAsync(this)
    }
    
    override fun onMapReady(map: NaverMap) {
		naverMap = map

        // 줌 범위 설정
        naverMap.maxZoom = 10.0
        naverMap.minZoom = 10.0

        // 현재 보여지는 지도 위치
        val cameraUpdate = CameraUpdate.scrollTo(LatLng(37.497885, 127.027512))
        naverMap.moveCamera(cameraUpdate)

        // 현재 위치 버튼
        val uiSetting = naverMap.uiSettings
        uiSetting.isLocationButtonEnabled = false
        currentLocationButton.map = naverMap

        // 위치 권한 요청
        locationSource = FusedLocationSource(this@MainActivity, LOCATION_PERMISSION_REQUEST_CODE)
        naverMap.locationSource = locationSource
    }
    
    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        if (requestCode != LOCATION_PERMISSION_REQUEST_CODE) {
            return
        }

        if (locationSource.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
            if (!locationSource.isActivated) {
                // 권한 설정 거부시 위치 추적 사용하지 않음
                naverMap.locationTrackingMode = LocationTrackingMode.None
            }
            return
        }
    }
    
    private val currentLocationButton: LocationButtonView by lazy {
        findViewById(R.id.currentLocationButton)
    }

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

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

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

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        mapView.onSaveInstanceState(outState)
    }

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

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

    override fun onLowMemory() { // 메모리가 별로 없을 때 호출되는 함수
        super.onLowMemory()
        mapView.onLowMemory()
    }
}


서버에서 가져온 예약 가능한 숙소 목록 보여주기


mocky를 사용해 작성해둔 json 파일을 서버에서 받아올 수 있도록 하였다. 이후 retorfit을 통해 받아와 처리해주었다.

HouseService

interface HouseService {
    @GET("/v3/511c37d3-79c1-455f-9efb-98b5d594e640")
    fun getHouseList(): Call<HouseDto>
}

HouseDto

data class HouseDto(
    val items: List<HouseModel>
)

HouseModel

data class HouseModel(
    val id: Int,
    val title: String,
    val price: String,
    val lat: Double,
    val lng: Double,
    val imgUrl: String
)

MainActivity

private fun getHouseListFromAPI() {
        val retrofit = Retrofit.Builder()
            .baseUrl("https://run.mocky.io")
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        retrofit.create(HouseService::class.java).also {
            it.getHouseList()
                .enqueue(object : Callback<HouseDto> {
                    @SuppressLint("SetTextI18n")
                    override fun onResponse(call: Call<HouseDto>, response: Response<HouseDto>) {
                        if (response.isSuccessful.not()) {
                            // 실패 처리에 대한 구현
                            return
                        }
                        response.body()?.let { dto ->
                        	// TODO 성공한 경우 마커 추가, viewPager, recyclerView 뿌려주기
                            response.body()?.let { dto ->
                            updateMarker(dto.items)
                            viewPagerAdapter.submitList(dto.items)
                            recyclerAdapter.submitList(dto.items) // 새 리스트로 갱신

                            bottomSheetTitleTextView.text = "${dto.items.size}개의 숙소"
                        }
                    }
                    override fun onFailure(call: Call<HouseDto>, t: Throwable) {
                        // 실패 처리에 대한 구현
                    }
                })
        }
    }

응답을 성공적으로 받아온 경우 바디를 검사해 dto로 받아와 처리해주고, 추후 HouseModel리스트를 받아와 마커를 추가하고 viewPager, recyclerView에 각각 뿌려주도록 할 것이다. (코드는 미리 작성)



지도에 마커 표시하기


MainActivity

private fun updateMarker(houses: List<HouseModel>) {
        houses.forEach { house ->
            val marker = Marker()
            marker.position = LatLng(house.lat, house.lng)
			marker.map = naverMap
            marker.tag = house.id
            marker.icon = MarkerIcons.BLACK
            marker.iconTintColor = Color.RED
        }
    }


지도 위에 예약 가능한 숙소 목록 띄우기


HouseViewPagerAdapter

class HouseViewPagerAdapter :
    ListAdapter<HouseModel, HouseViewPagerAdapter.ItemViewHolder>(differ) {

    inner class ItemViewHolder(val view: View) : RecyclerView.ViewHolder(view) {

        fun bind(houseModel: HouseModel) {
            val titleTextView = view.findViewById<TextView>(R.id.titleTextView)
            val priceTextView = view.findViewById<TextView>(R.id.priceTextView)
            val thumbnailImageView = view.findViewById<ImageView>(R.id.thumbnailImageView)

            titleTextView.text = houseModel.title
            priceTextView.text = houseModel.price
            
            Glide
                .with(thumbnailImageView.context)
                .load(houseModel.imgUrl)
                .into(thumbnailImageView)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return ItemViewHolder(inflater.inflate(R.layout.item_house_detail_viewpager, parent, false))
    }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        holder.bind(currentList[position])
    }

    companion object {
        val differ = object : DiffUtil.ItemCallback<HouseModel>() {
            override fun areItemsTheSame(oldItem: HouseModel, newItem: HouseModel): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: HouseModel, newItem: HouseModel): Boolean {
                return oldItem == newItem
            }
        }
    }
}

MainActivity

private val viewPager: ViewPager2 by lazy {
        findViewById(R.id.houseViewPager)
    }

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mapView.onCreate(savedInstanceState)

        viewPager.adapter = viewPagerAdapter
}        


bottomSheet에 예약 가능한 숙소 목록 띄우기


HouseListAdapter

class HouseListAdapter : ListAdapter<HouseModel, HouseListAdapter.ItemViewHolder>(differ) {

    inner class ItemViewHolder(val view: View) : RecyclerView.ViewHolder(view) {

        fun bind(houseModel: HouseModel) {
            val titleTextView = view.findViewById<TextView>(R.id.titleTextView)
            val priceTextView = view.findViewById<TextView>(R.id.priceTextView)
            val thumbnailImageView = view.findViewById<ImageView>(R.id.thumbnailImageView)

            titleTextView.text = houseModel.title
            priceTextView.text = houseModel.price

            Glide
                .with(thumbnailImageView.context)
                .load(houseModel.imgUrl)
                // transeform()을 통해 이미지를 변형시키고, RoundCorners를 통해 모서리를 둥글게 바꿔줌
                .transform(CenterCrop(), RoundedCorners(dpToPx(thumbnailImageView.context, 12)))
                .into(thumbnailImageView)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return ItemViewHolder(inflater.inflate(R.layout.item_house, parent, false))
    }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        holder.bind(currentList[position])
    }

	// roundCorners는 픽셀 단위로 설정해줘야 하기 때문에 dbToPx()를 이용해 dp를 px로 변환
    private fun dpToPx(context: Context, dp: Int): Int {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), context.resources.displayMetrics).toInt()
    }

    companion object {
        val differ = object : DiffUtil.ItemCallback<HouseModel>() {
            override fun areItemsTheSame(oldItem: HouseModel, newItem: HouseModel): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: HouseModel, newItem: HouseModel): Boolean {
                return oldItem == newItem
            }
        }
    }
}

200*200 이미지를 가져왔기 때문에 목록에서 보일 때 정사각형으로 뜬다. 2:3 크기로 보여주기 위해 glide에서 설정 값을 추가해주었다.

MainActivity

viewPager.adapter = viewPagerAdapter
recyclerView.adapter = recyclerAdapter
recyclerView.layoutManager = LinearLayoutManager(this)

bottom_sheet.xml에 작성해둔 recyclerView를 가져와 onCreate()에 뷰를 연결시켜 주었다.



ViewPager와 마커 연결


MainActivity

viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {

            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)

                val selectedHouseModel = viewPagerAdapter.currentList[position]
                val cameraUpdate =
                    CameraUpdate.scrollTo(LatLng(selectedHouseModel.lat, selectedHouseModel.lng))
                        .animate(CameraAnimation.Easing)

                naverMap.moveCamera(cameraUpdate)
            }
        })
        
private fun updateMarker(houses: List<HouseModel>) {
        houses.forEach { house ->
            
            marker.onClickListener = this
        }
}        

viewPager와 마커를 연결시켜 해당 viewPager를 클릭했을 때 마커가 있는 곳으로 카메라가 이동하도록 구현해주었다.

MainActivity

class MainActivity : AppCompatActivity(), OnMapReadyCallback, Overlay.OnClickListener {
	
    override fun onClick(overly: Overlay): Boolean {
    	// overlay: 마커의 총 집합
        // tag: id 값
        // firstOrNull: 먼저 나오는 아이템 반환, 없으면 null 반환
        val selectedModel = viewPagerAdapter.currentList.firstOrNull {
            it.id == overly.tag
        }
        selectedModel?.let {
        	// 널이 아닌 경우 포지션을 찾았다면 포지션의 위치 이동
            val position = viewPagerAdapter.currentList.indexOf(it)
            viewPager.currentItem = position
        }
        return true
    }
}

viewPager와 마커를 연결시켜 마커를 클릭했을 때 해당 위치의 viewPager가 뜰 수 있도록 구현해주었다.
마커를 클릭하면 viewPager의 리스트에서 해당 아이디를 갖는 숙소를 찾은 뒤 해당 숙소의 현재 아이템 포지션으로 지정해도록 하였다.



공유하기


HouseViewPagerAdapter

class HouseViewPagerAdapter(val itemClicked: (HouseModel) -> Unit) :
    ListAdapter<HouseModel, HouseViewPagerAdapter.ItemViewHolder>(differ) {

    inner class ItemViewHolder(val view: View) : RecyclerView.ViewHolder(view) {

        fun bind(houseModel: HouseModel) {
            
            view.setOnClickListener {
                itemClicked(houseModel)
            }
        }
    }
}    

MainActivity

private val viewPagerAdapter = HouseViewPagerAdapter(itemClicked = {
        val intent = Intent()
            .apply {
                action = Intent.ACTION_SEND
                putExtra(
                    Intent.EXTRA_TEXT,
                    "[지금 이 가격에 예약하세요!!] ${it.title} ${it.price} 사진보기 : ${it.imgUrl}"
                )
                type = "text/plain"
            }
        startActivity(Intent.createChooser(intent, null))
    })

viewPager의 항목이 클릭된 경우 chooser를 통해 다른 앱으로 공유할 수 있도록 해주었고, 클릭리스너를 달아 putExtra로 설정된 텍스트를 공유할 수 있도록 하였다.



최종 코드


HouseViewPagerAdapter

class HouseViewPagerAdapter(val itemClicked: (HouseModel) -> Unit) :
    ListAdapter<HouseModel, HouseViewPagerAdapter.ItemViewHolder>(differ) {

    inner class ItemViewHolder(val view: View) : RecyclerView.ViewHolder(view) {

        fun bind(houseModel: HouseModel) {
            val titleTextView = view.findViewById<TextView>(R.id.titleTextView)
            val priceTextView = view.findViewById<TextView>(R.id.priceTextView)
            val thumbnailImageView = view.findViewById<ImageView>(R.id.thumbnailImageView)

            titleTextView.text = houseModel.title
            priceTextView.text = houseModel.price

            view.setOnClickListener {
                itemClicked(houseModel)
            }

            Glide
                .with(thumbnailImageView.context)
                .load(houseModel.imgUrl)
                .into(thumbnailImageView)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return ItemViewHolder(inflater.inflate(R.layout.item_house_detail_viewpager, parent, false))
    }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        holder.bind(currentList[position])
    }

    companion object {
        val differ = object : DiffUtil.ItemCallback<HouseModel>() {
            override fun areItemsTheSame(oldItem: HouseModel, newItem: HouseModel): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: HouseModel, newItem: HouseModel): Boolean {
                return oldItem == newItem
            }
        }
    }
}

MainActivity

class MainActivity : AppCompatActivity(), OnMapReadyCallback, Overlay.OnClickListener {

    companion object {
        private const val LOCATION_PERMISSION_REQUEST_CODE = 1000
    }

    private lateinit var naverMap: NaverMap
    private lateinit var locationSource: FusedLocationSource

    private val viewPager: ViewPager2 by lazy {
        findViewById(R.id.houseViewPager)
    }

    private val mapView: MapView by lazy {
        findViewById(R.id.mapView)
    }

    private val recyclerView: RecyclerView by lazy {
        findViewById(R.id.recyclerView)
    }

    private val currentLocationButton: LocationButtonView by lazy {
        findViewById(R.id.currentLocationButton)
    }

    private val bottomSheetTitleTextView: TextView by lazy {
        findViewById(R.id.bottomSheetTitleTextView)
    }

    private val viewPagerAdapter = HouseViewPagerAdapter(itemClicked = {
        val intent = Intent()
            .apply {
                action = Intent.ACTION_SEND
                putExtra(
                    Intent.EXTRA_TEXT,
                    "[지금 이 가격에 예약하세요!!] ${it.title} ${it.price} 사진보기 : ${it.imgUrl}"
                )
                type = "text/plain"
            }
        startActivity(Intent.createChooser(intent, null))
    })

    private val recyclerAdapter = HouseListAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mapView.onCreate(savedInstanceState)

        mapView.getMapAsync(this)

        viewPager.adapter = viewPagerAdapter
        recyclerView.adapter = recyclerAdapter
        recyclerView.layoutManager = LinearLayoutManager(this)

        viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {

            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)

                val selectedHouseModel = viewPagerAdapter.currentList[position]
                val cameraUpdate =
                    CameraUpdate.scrollTo(LatLng(selectedHouseModel.lat, selectedHouseModel.lng))
                        .animate(CameraAnimation.Easing)

                naverMap.moveCamera(cameraUpdate)
            }
        })
    }

    override fun onMapReady(map: NaverMap) {
        naverMap = map

        naverMap.maxZoom = 18.0
        naverMap.minZoom = 10.0


        val cameraUpdate = CameraUpdate.scrollTo(LatLng(37.497885, 127.027512))
        naverMap.moveCamera(cameraUpdate)

        val uiSetting = naverMap.uiSettings
        uiSetting.isLocationButtonEnabled = false
        currentLocationButton.map = naverMap

        locationSource = FusedLocationSource(this@MainActivity, LOCATION_PERMISSION_REQUEST_CODE)
        naverMap.locationSource = locationSource

        getHouseListFromAPI()
    }

    private fun getHouseListFromAPI() {
        val retrofit = Retrofit.Builder()
            .baseUrl("https://run.mocky.io")
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        retrofit.create(HouseService::class.java).also {
            it.getHouseList()
                .enqueue(object : Callback<HouseDto> {
                    @SuppressLint("SetTextI18n")
                    override fun onResponse(call: Call<HouseDto>, response: Response<HouseDto>) {
                        if (response.isSuccessful.not()) {
                            // 실패 처리에 대한 구현
                            return
                        }
                        response.body()?.let { dto ->
                            updateMarker(dto.items)
                            viewPagerAdapter.submitList(dto.items)
                            recyclerAdapter.submitList(dto.items) // 새 리스트로 갱신

                            bottomSheetTitleTextView.text = "${dto.items.size}개의 숙소"
                        }
                    }
                    override fun onFailure(call: Call<HouseDto>, t: Throwable) {
                        // 실패 처리에 대한 구현
                    }
                })
        }
    }

    private fun updateMarker(houses: List<HouseModel>) {
        houses.forEach { house ->
            val marker = Marker()
            marker.position = LatLng(house.lat, house.lng)
            marker.onClickListener = this

            marker.map = naverMap
            marker.tag = house.id
            marker.icon = MarkerIcons.BLACK
            marker.iconTintColor = Color.RED
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        if (requestCode != LOCATION_PERMISSION_REQUEST_CODE) {
            return
        }
        if (locationSource.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
            if (!locationSource.isActivated) {
                naverMap.locationTrackingMode = LocationTrackingMode.None
            }
            return
        }
    }

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

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

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

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        mapView.onSaveInstanceState(outState)
    }

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

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

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

    override fun onClick(overly: Overlay): Boolean {
        val selectedModel = viewPagerAdapter.currentList.firstOrNull {
            it.id == overly.tag
        }
        selectedModel?.let {
            val position = viewPagerAdapter.currentList.indexOf(it)
            viewPager.currentItem = position
        }
        return true
    }
}

0개의 댓글