diff --git a/data/src/main/java/org/gdsc/data/database/RestaurantByMapPagingSource.kt b/data/src/main/java/org/gdsc/data/database/RestaurantByMapPagingSource.kt index 2236af2c..01ffc2c3 100644 --- a/data/src/main/java/org/gdsc/data/database/RestaurantByMapPagingSource.kt +++ b/data/src/main/java/org/gdsc/data/database/RestaurantByMapPagingSource.kt @@ -4,10 +4,12 @@ import androidx.paging.PagingSource import androidx.paging.PagingState import org.gdsc.data.model.RegisteredRestaurantResponse import org.gdsc.data.network.RestaurantAPI +import org.gdsc.domain.SortType import org.gdsc.domain.model.request.RestaurantSearchRequest class RestaurantByMapPagingSource( private val api: RestaurantAPI, + private val sortType: SortType, private val restaurantSearchRequest: RestaurantSearchRequest, ): PagingSource() { override suspend fun load(params: LoadParams): LoadResult { @@ -16,6 +18,7 @@ class RestaurantByMapPagingSource( val items = api.getRestaurantLocationInfoByMap( page = page, size = params.loadSize, + sort = sortType.key, restaurantSearchRequest = restaurantSearchRequest ) LoadResult.Page( diff --git a/data/src/main/java/org/gdsc/data/datasource/RestaurantDataSourceImpl.kt b/data/src/main/java/org/gdsc/data/datasource/RestaurantDataSourceImpl.kt index 2d61cf87..e3a0507a 100644 --- a/data/src/main/java/org/gdsc/data/datasource/RestaurantDataSourceImpl.kt +++ b/data/src/main/java/org/gdsc/data/datasource/RestaurantDataSourceImpl.kt @@ -230,6 +230,7 @@ class RestaurantDataSourceImpl @Inject constructor( ) { RestaurantByMapPagingSource( restaurantAPI, + sortType, restaurantSearchRequest ) }.flow.cachedIn(coroutineScope) diff --git a/data/src/main/java/org/gdsc/data/network/RestaurantAPI.kt b/data/src/main/java/org/gdsc/data/network/RestaurantAPI.kt index 5088ea2b..062ced34 100644 --- a/data/src/main/java/org/gdsc/data/network/RestaurantAPI.kt +++ b/data/src/main/java/org/gdsc/data/network/RestaurantAPI.kt @@ -77,7 +77,7 @@ interface RestaurantAPI { suspend fun getRestaurantLocationInfoByMap( @Query("page") page: Int? = null, @Query("size") size: Int? = null, - @Query("sort") sort: Array? = null, + @Query("sort") sort: String? = null, @Body restaurantSearchRequest: RestaurantSearchRequest, ): Response diff --git a/domain/src/main/java/org/gdsc/domain/SortType.kt b/domain/src/main/java/org/gdsc/domain/SortType.kt index dac6f5d6..e93c8cb8 100644 --- a/domain/src/main/java/org/gdsc/domain/SortType.kt +++ b/domain/src/main/java/org/gdsc/domain/SortType.kt @@ -1,9 +1,9 @@ package org.gdsc.domain -enum class SortType(val text: String) { - DISTANCE("가까운 순"), - LIKED("좋아요 순"), - RECENCY("최신 순"); +enum class SortType(val text: String, val key: String) { + DISTANCE("가까운 순", "distance,asc"), + LIKED("좋아요 순", ""), + RECENCY("최신 순", ""); companion object { diff --git a/presentation/src/main/java/org/gdsc/presentation/model/FoodCategoryItem.kt b/presentation/src/main/java/org/gdsc/presentation/model/FoodCategoryItem.kt index e80918af..ec43c060 100644 --- a/presentation/src/main/java/org/gdsc/presentation/model/FoodCategoryItem.kt +++ b/presentation/src/main/java/org/gdsc/presentation/model/FoodCategoryItem.kt @@ -25,4 +25,18 @@ data class FoodCategoryItem( else -> R.drawable.ic_etc } } + + @DrawableRes fun getMarkerIcon(): Int { + return when(this.categoryItem) { + FoodCategory.KOREAN -> R.drawable.ic_marker_korea + FoodCategory.JAPANESE -> R.drawable.ic_marker_japan + FoodCategory.CHINESE -> R.drawable.ic_marker_china + FoodCategory.WESTERN -> R.drawable.ic_marker_foreign + FoodCategory.FUSION -> R.drawable.ic_marker_fusion + FoodCategory.CAFE -> R.drawable.ic_marker_cafe + FoodCategory.BAR -> R.drawable.ic_marker_bar + FoodCategory.ETC -> R.drawable.ic_marker_etc + else -> R.drawable.ic_marker_etc + } + } } \ No newline at end of file diff --git a/presentation/src/main/java/org/gdsc/presentation/view/home/HomeFragment.kt b/presentation/src/main/java/org/gdsc/presentation/view/home/HomeFragment.kt index 56c03a90..baee982e 100644 --- a/presentation/src/main/java/org/gdsc/presentation/view/home/HomeFragment.kt +++ b/presentation/src/main/java/org/gdsc/presentation/view/home/HomeFragment.kt @@ -19,8 +19,6 @@ import com.naver.maps.geometry.LatLng import com.naver.maps.map.CameraUpdate import com.naver.maps.map.MapView import com.naver.maps.map.Projection -import com.naver.maps.map.overlay.Marker -import com.naver.maps.map.overlay.OverlayImage import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -37,7 +35,6 @@ import org.gdsc.presentation.base.BaseViewHolder import org.gdsc.presentation.base.ViewHolderBindListener import org.gdsc.presentation.databinding.FragmentHomeBinding import org.gdsc.presentation.utils.repeatWhenUiStarted -import org.gdsc.presentation.utils.toDp import org.gdsc.presentation.view.custom.JmtSpinner @@ -55,6 +52,7 @@ class HomeFragment : Fragment(), ViewHolderBindListener { private val recommendPopularRestaurantTitleAdapter by lazy { RecommendPopularRestaurantTitleAdapter("그룹에서 인기가 많아요") } private val recommendPopularRestaurantWrapperAdapter by lazy { RecommendPopularRestaurantWrapperAdapter(recommendPopularRestaurantList)} private val restaurantFilterAdapter by lazy { RestaurantFilterAdapter(this) } + private val restaurantListAdapter by lazy { MapMarkerWithRestaurantsAdatper() } private val mapMarkerAdapter by lazy { MapMarkerWithRestaurantsAdatper() } private val emptyAdapter by lazy { EmptyAdapter() } private lateinit var concatAdapter: ConcatAdapter @@ -76,68 +74,27 @@ class HomeFragment : Fragment(), ViewHolderBindListener { recommendPopularRestaurantTitleAdapter, recommendPopularRestaurantWrapperAdapter, restaurantFilterAdapter, - mapMarkerAdapter + restaurantListAdapter ) } else { ConcatAdapter( restaurantFilterAdapter, - mapMarkerAdapter + restaurantListAdapter ) } setRecyclerView() return binding.root } - fun setRecyclerView() { + private fun setRecyclerView() { binding.recyclerView.adapter = concatAdapter binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - standardBottomSheetBehavior.addBottomSheetCallback( - object : BottomSheetBehavior.BottomSheetCallback() { - fun setBottomSheetRelatedView(isVisible: Boolean) { - binding.bottomSheetHandle.isVisible = isVisible - binding.bottomSheetHandleSpace.isVisible = isVisible - binding.mapOptionContainer.isVisible = isVisible - } - override fun onStateChanged(bottomSheet: View, newState: Int) { - when (newState) { - BottomSheetBehavior.STATE_EXPANDED -> { - binding.groupHeader.elevation = 10F - setBottomSheetRelatedView(false) - binding.bottomSheet.background = ResourcesCompat.getDrawable( - resources, - R.color.white, - null - ) - } - BottomSheetBehavior.STATE_HALF_EXPANDED -> { - binding.mapOptionContainer.isVisible = true - } - - BottomSheetBehavior.STATE_DRAGGING -> { - binding.groupHeader.elevation = 0F - setBottomSheetRelatedView(true) - binding.bottomSheet.background = ResourcesCompat.getDrawable( - resources, - R.drawable.bg_bottom_sheet_top_round, - null - ) - } - } - } - - override fun onSlide(bottomSheet: View, slideOffset: Float) { } - }) - - CoroutineScope(Dispatchers.Main).launch { - delay(1000) - if (standardBottomSheetBehavior.state != BottomSheetBehavior.STATE_COLLAPSED) - standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED - } + setRestaurantListBottomSheet() } override fun onViewHolderBind(holder: BaseViewHolder, _item: Any) { @@ -182,9 +139,19 @@ class HomeFragment : Fragment(), ViewHolderBindListener { mapView.onCreate(savedInstanceState) mapView.getMapAsync { naverMap -> + + val markerManager = MarkerManager(naverMap) + naverMap.uiSettings.isZoomControlEnabled = false naverMap.uiSettings.isScaleBarEnabled = false + repeatWhenUiStarted { + + viewModel.registeredPagingDataByMap().collect { + mapMarkerAdapter.submitData(it) + } + } + repeatWhenUiStarted { val location = viewModel.getCurrentLocation() location?.let { @@ -196,43 +163,25 @@ class HomeFragment : Fragment(), ViewHolderBindListener { naverMap.moveCamera(zoom) } - mapMarkerAdapter.addLoadStateListener { loadState -> - if (loadState.append.endOfPaginationReached) { - if (mapMarkerAdapter.itemCount < 1) { - concatAdapter.addAdapter(emptyAdapter) - standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED - } else { - concatAdapter.removeAdapter(emptyAdapter) - } - setRecyclerView() - } - } mapMarkerAdapter.registerAdapterDataObserver(object: RecyclerView.AdapterDataObserver() { - fun setMark() { - CoroutineScope(Dispatchers.Main).launch { - mapMarkerAdapter.snapshot().items.forEach { data -> - Marker().apply { - position = LatLng(data.y, data.x) - icon = OverlayImage.fromResource(R.drawable.jmt_marker) - map = naverMap - } - } - } - } override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { super.onItemRangeInserted(positionStart, itemCount) - setMark() + markerManager.updateDataList(mapMarkerAdapter.snapshot().items) } override fun onChanged() { super.onChanged() - setMark() + markerManager.updateDataList(mapMarkerAdapter.snapshot().items) + } + + override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { + super.onItemRangeRemoved(positionStart, itemCount) + markerManager.updateDataList(mapMarkerAdapter.snapshot().items) } }) } binding.mapRefreshBtn.setOnClickListener { - val projection: Projection = naverMap.projection val topLeft: LatLng = projection.fromScreenLocation(PointF(0F, 0F)) @@ -242,13 +191,84 @@ class HomeFragment : Fragment(), ViewHolderBindListener { viewModel.setEndLocation(Location(bottomRight.longitude.toString(), bottomRight.latitude.toString())) } } + } + + + private fun setRestaurantListBottomSheet() { + + binding.scrollUpButton.setOnClickListener { + binding.recyclerView.scrollToPosition(0) + } + + binding.registRestaurantButton.setOnClickListener { + // TODO : 식당 등록 버튼 클릭 시 동작 정의 필요 + Log.d("testLog", "식당 등록 버튼 클릭") + } + restaurantListAdapter.addLoadStateListener { loadState -> + if (loadState.append.endOfPaginationReached) { + if (restaurantListAdapter.itemCount < 1) { + concatAdapter.addAdapter(emptyAdapter) + standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + } else { + concatAdapter.removeAdapter(emptyAdapter) + } + setRecyclerView() + } + } + + standardBottomSheetBehavior.addBottomSheetCallback( + object : BottomSheetBehavior.BottomSheetCallback() { + fun setBottomSheetRelatedView(isVisible: Boolean) { + binding.bottomSheetHandle.isVisible = isVisible + binding.bottomSheetHandleSpace.isVisible = isVisible + binding.mapOptionContainer.isVisible = isVisible + } + + override fun onStateChanged(bottomSheet: View, newState: Int) { + when (newState) { + BottomSheetBehavior.STATE_EXPANDED -> { + binding.groupHeader.elevation = 10F + binding.bottomSheetActionButtons.isVisible = true + + setBottomSheetRelatedView(false) + binding.bottomSheet.background = ResourcesCompat.getDrawable( + resources, + R.color.white, + null + ) + } + BottomSheetBehavior.STATE_HALF_EXPANDED -> { + binding.mapOptionContainer.isVisible = true + } + + BottomSheetBehavior.STATE_DRAGGING -> { + binding.groupHeader.elevation = 0F + setBottomSheetRelatedView(true) + binding.bottomSheet.background = ResourcesCompat.getDrawable( + resources, + R.drawable.bg_bottom_sheet_top_round, + null + ) + } + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) { } + }) + + CoroutineScope(Dispatchers.Main).launch { + delay(1000) + if (standardBottomSheetBehavior.state != BottomSheetBehavior.STATE_COLLAPSED) + standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED + } } private fun observeState() { + repeatWhenUiStarted { - viewModel.registeredPagingData().collect { - mapMarkerAdapter.submitData(it) + viewModel.registeredPagingDataByGroup().collect { + restaurantListAdapter.submitData(it) } } @@ -261,5 +281,4 @@ class HomeFragment : Fragment(), ViewHolderBindListener { _binding = null super.onDestroyView() } - } \ No newline at end of file diff --git a/presentation/src/main/java/org/gdsc/presentation/view/home/HomeViewModel.kt b/presentation/src/main/java/org/gdsc/presentation/view/home/HomeViewModel.kt index 6562d183..d5f2a5e1 100644 --- a/presentation/src/main/java/org/gdsc/presentation/view/home/HomeViewModel.kt +++ b/presentation/src/main/java/org/gdsc/presentation/view/home/HomeViewModel.kt @@ -1,12 +1,9 @@ package org.gdsc.presentation.view.home -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn -import androidx.paging.map -import com.google.gson.Gson import com.naver.maps.geometry.LatLng import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -21,10 +18,8 @@ import org.gdsc.domain.DrinkPossibility import org.gdsc.domain.FoodCategory import org.gdsc.domain.SortType import org.gdsc.domain.model.Location -import org.gdsc.domain.model.PagingResult import org.gdsc.domain.model.RegisteredRestaurant import org.gdsc.domain.usecase.GetRestaurantsByMapUseCase -import org.gdsc.domain.usecase.token.GetAccessTokenUseCase import org.gdsc.presentation.JmtLocationManager import javax.inject.Inject @@ -96,7 +91,7 @@ class HomeViewModel @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) - suspend fun registeredPagingData(): Flow> { + suspend fun registeredPagingDataByMap(): Flow> { val location = locationManager.getCurrentLocation() ?: return flowOf(PagingData.empty()) val userLoc = Location(location.longitude.toString(), location.latitude.toString()) @@ -113,4 +108,20 @@ class HomeViewModel @Inject constructor( .flatMapLatest { it } }.cachedIn(viewModelScope) } + + @OptIn(ExperimentalCoroutinesApi::class) + suspend fun registeredPagingDataByGroup(): Flow> { + + return run { + return@run combine( + userLocationState, + sortTypeState, + foodCategoryState, + drinkPossibilityState + ) { userLoc, sortType, foodCategory, drinkPossibility -> + getRestaurantsByMapUseCase(sortType, foodCategory, drinkPossibility, userLoc, null, null) + }.distinctUntilChanged() + .flatMapLatest { it } + }.cachedIn(viewModelScope) + } } \ No newline at end of file diff --git a/presentation/src/main/java/org/gdsc/presentation/view/home/MapMarkerWithRestaurantsAdatper.kt b/presentation/src/main/java/org/gdsc/presentation/view/home/MapMarkerWithRestaurantsAdatper.kt index 8bef434e..f05fa295 100644 --- a/presentation/src/main/java/org/gdsc/presentation/view/home/MapMarkerWithRestaurantsAdatper.kt +++ b/presentation/src/main/java/org/gdsc/presentation/view/home/MapMarkerWithRestaurantsAdatper.kt @@ -27,7 +27,7 @@ class MapMarkerWithRestaurantsAdatper oldItem: RegisteredRestaurant, newItem: RegisteredRestaurant ): Boolean { - return oldItem == newItem + return oldItem.id == newItem.id } } } diff --git a/presentation/src/main/java/org/gdsc/presentation/view/home/MarkerManager.kt b/presentation/src/main/java/org/gdsc/presentation/view/home/MarkerManager.kt new file mode 100644 index 00000000..efaf027f --- /dev/null +++ b/presentation/src/main/java/org/gdsc/presentation/view/home/MarkerManager.kt @@ -0,0 +1,55 @@ +package org.gdsc.presentation.view.home + +import com.naver.maps.geometry.LatLng +import com.naver.maps.map.NaverMap +import com.naver.maps.map.overlay.Marker +import com.naver.maps.map.overlay.OverlayImage +import org.gdsc.domain.FoodCategory +import org.gdsc.domain.model.RegisteredRestaurant +import org.gdsc.presentation.model.FoodCategoryItem + + +class MarkerManager { + private var naverMap: NaverMap? = null + private var dataList: MutableList = mutableListOf() // 여기에 페이징된 데이터를 저장 + private var markers: MutableList = mutableListOf() // 마커를 저장할 리스트 + + constructor(naverMap: NaverMap) { + this.naverMap = naverMap + dataList = ArrayList() + } + + fun updateDataList(newDataList: List) { + val dataSet = dataList.toSet() + val newDataSet = newDataList.toSet() + + dataSet.subtract(newDataSet).let { + it.forEach { removeData -> + dataList.indexOf(removeData).let { index -> + if (index != -1) { + markers[index].map = null + markers.removeAt(index) + dataList.removeAt(index) + } + } + } + } + + newDataSet.subtract(dataSet).let { + it.forEach { insertData -> + dataList.add(insertData) + markers.add( + Marker().apply { + position = LatLng(insertData.y, insertData.x) + icon = OverlayImage.fromResource( + FoodCategoryItem( + FoodCategory.fromName(insertData.category) + ).getMarkerIcon() + ) + map = naverMap + } + ) + } + } + } +} \ No newline at end of file diff --git a/presentation/src/main/res/drawable/ic_android_black_24dp.xml b/presentation/src/main/res/drawable/ic_android_black_24dp.xml new file mode 100644 index 00000000..fe512307 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_android_black_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/presentation/src/main/res/drawable/ic_marker_bar.xml b/presentation/src/main/res/drawable/ic_marker_bar.xml new file mode 100644 index 00000000..be230128 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_marker_bar.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_marker_cafe.xml b/presentation/src/main/res/drawable/ic_marker_cafe.xml new file mode 100644 index 00000000..bffcbd69 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_marker_cafe.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_marker_china.xml b/presentation/src/main/res/drawable/ic_marker_china.xml new file mode 100644 index 00000000..20516e58 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_marker_china.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_marker_etc.xml b/presentation/src/main/res/drawable/ic_marker_etc.xml new file mode 100644 index 00000000..ea663786 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_marker_etc.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_marker_foreign.xml b/presentation/src/main/res/drawable/ic_marker_foreign.xml new file mode 100644 index 00000000..f07ec8d8 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_marker_foreign.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_marker_fusion.xml b/presentation/src/main/res/drawable/ic_marker_fusion.xml new file mode 100644 index 00000000..f9dbfb70 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_marker_fusion.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_marker_japan.xml b/presentation/src/main/res/drawable/ic_marker_japan.xml new file mode 100644 index 00000000..fc9825b2 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_marker_japan.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_marker_korea.xml b/presentation/src/main/res/drawable/ic_marker_korea.xml new file mode 100644 index 00000000..9e2e6ed4 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_marker_korea.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_plus.xml b/presentation/src/main/res/drawable/ic_plus.xml new file mode 100644 index 00000000..113c14fc --- /dev/null +++ b/presentation/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/presentation/src/main/res/drawable/shape_circle.xml b/presentation/src/main/res/drawable/shape_circle.xml new file mode 100644 index 00000000..7b5fd5c6 --- /dev/null +++ b/presentation/src/main/res/drawable/shape_circle.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/fragment_home.xml b/presentation/src/main/res/layout/fragment_home.xml index 7460a81c..ae8ae4c6 100644 --- a/presentation/src/main/res/layout/fragment_home.xml +++ b/presentation/src/main/res/layout/fragment_home.xml @@ -178,6 +178,47 @@ app:layout_constraintTop_toBottomOf="@+id/bottom_sheet_handle_space" app:layout_constraintBottom_toBottomOf="parent"/> + + + + + +