Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions domain/src/main/java/com/project200/domain/model/FilterModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.project200.domain.model

enum class Gender(val code: String) {
MALE("M"),
FEMALE("F")
}

enum class AgeGroup(val code: String) {
TEEN("10"),
TWENTIES("20"),
THIRTIES("30"),
FORTIES("40"),
FIFTIES("50"),
SIXTIES_PLUS("60")
}

enum class DayOfWeek(val index: Int) {
MONDAY(0),
TUESDAY(1),
WEDNESDAY(2),
THURSDAY(3),
FRIDAY(4),
SATURDAY(5),
SUNDAY(6)
}


enum class SkillLevel(val code: String) {
NOVICE("NOVICE"),
BEGINNER("BEGINNER"),
INTERMEDIATE("INTERMEDIATE"),
ADVANCED("ADVANCED"),
EXPERT("EXPERT"),
PROFESSIONAL("PROFESSIONAL")
}


enum class ExerciseScore(val minScore: Int) {
OVER_20(20),
OVER_40(40),
OVER_60(60),
OVER_80(80)
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import com.project200.domain.model.MapPosition
import com.project200.domain.model.MatchingMember
import com.project200.feature.matching.map.cluster.ClusterCalculator
import com.project200.feature.matching.map.cluster.MapClusterItem
import com.project200.feature.matching.map.filter.FilterBottomSheetDialog
import com.project200.feature.matching.map.filter.MatchingFilterRVAdapter
import com.project200.feature.matching.utils.FilterUiMapper
import com.project200.feature.matching.utils.MatchingFilterType
import com.project200.presentation.base.BindingFragment
import com.project200.undabang.feature.matching.R
import com.project200.undabang.feature.matching.databinding.FragmentMatchingMapBinding
Expand All @@ -43,6 +47,17 @@ class MatchingMapFragment :
private lateinit var fusedLocationClient: FusedLocationProviderClient
private val viewModel: MatchingMapViewModel by viewModels()

private val filterAdapter by lazy {
MatchingFilterRVAdapter(
onFilterClick = { type ->
viewModel.onFilterTypeClicked(type)
},
onClearClick = {
viewModel.clearFilters()
},
)
}

private var isMapInitialized: Boolean = false

// 클러스터링 계산을 위한 헬퍼 클래스
Expand All @@ -69,6 +84,8 @@ class MatchingMapFragment :

initMapView()
initListeners()
binding.matchingFilterRv.adapter = filterAdapter
filterAdapter.submitFilterList(MatchingFilterType.entries)
}

private fun initMapView() {
Expand Down Expand Up @@ -157,6 +174,16 @@ class MatchingMapFragment :
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
}
}
launch {
viewModel.filterState.collect { state ->
filterAdapter.submitFilterState(state)
}
}
launch {
viewModel.currentFilterType.collect { type ->
showFilterBottomSheet(type)
}
}
}
}
}
Expand Down Expand Up @@ -252,6 +279,24 @@ class MatchingMapFragment :
bottomSheet.show(parentFragmentManager, MembersBottomSheetDialog::class.java.simpleName)
}

private fun showFilterBottomSheet(type: MatchingFilterType) {
// 필터 옵션들을 UI 모델로 매핑
val options =
FilterUiMapper.mapToUiModels(
type = type,
currentState = viewModel.filterState.value,
)

val bottomSheet =
FilterBottomSheetDialog(
filterType = type,
onOptionSelected = { selectedDomainData ->
viewModel.onFilterOptionSelected(type, selectedDomainData)
},
)
bottomSheet.show(childFragmentManager, FilterBottomSheetDialog::class.java.simpleName)
}

private fun checkPermissionAndMove() {
if (isLocationPermissionGranted()) {
moveToCurrentLocation()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,26 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.project200.domain.model.BaseResult
import com.project200.domain.model.DayOfWeek
import com.project200.domain.model.ExercisePlace
import com.project200.domain.model.MapPosition
import com.project200.domain.model.MatchingMember
import com.project200.domain.usecase.GetExercisePlaceUseCase
import com.project200.domain.usecase.GetLastMapPositionUseCase
import com.project200.domain.usecase.GetMatchingMembersUseCase
import com.project200.domain.usecase.SaveLastMapPositionUseCase
import com.project200.feature.matching.utils.FilterState
import com.project200.feature.matching.utils.MatchingFilterType
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject

Expand All @@ -43,6 +48,14 @@ class MatchingMapViewModel
private val _errorEvents = MutableSharedFlow<String>()
val errorEvents: SharedFlow<String> = _errorEvents

// 필터 상태
private val _filterState = MutableStateFlow(FilterState())
val filterState: StateFlow<FilterState> = _filterState.asStateFlow()

// 현재 선택된 필터 타입
private val _currentFilterType = MutableSharedFlow<MatchingFilterType>()
val currentFilterType: SharedFlow<MatchingFilterType> = _currentFilterType

val combinedMapData: StateFlow<Pair<List<MatchingMember>, List<ExercisePlace>>> =
combine(
matchingMembers,
Expand Down Expand Up @@ -84,6 +97,8 @@ class MatchingMapViewModel
*/
fun fetchMatchingMembers() {
viewModelScope.launch {
val filters = _filterState.value
// useCase 호출 시 filters.gender, filters.ageGroup 등을 전달
matchingMembers.value = getMatchingMembersUseCase()
}
}
Expand Down Expand Up @@ -140,7 +155,56 @@ class MatchingMapViewModel
}
}

companion object {
const val NO_URL = "404"
// 필터 버튼 클릭 시 호출
fun onFilterTypeClicked(type: MatchingFilterType) {
viewModelScope.launch {
_currentFilterType.emit(type)
}
}

/**
* 필터 초기화
*/
fun clearFilters() {
_filterState.value = FilterState()
fetchMatchingMembers()
}

/**
* 필터 옵션 선택 시 호출
*/
fun onFilterOptionSelected(
type: MatchingFilterType,
option: Any?,
) {
_filterState.update { current ->
when (type) {
MatchingFilterType.GENDER -> current.copy(gender = toggle(current.gender, option))
MatchingFilterType.AGE -> current.copy(ageGroup = toggle(current.ageGroup, option))
MatchingFilterType.SKILL -> current.copy(skillLevel = toggle(current.skillLevel, option))
MatchingFilterType.SCORE -> current.copy(exerciseScore = toggle(current.exerciseScore, option))
MatchingFilterType.DAY -> {
val newDays =
if (option == null) {
// 전체 선택 시 모두 비움 (Empty == 전체)
emptySet()
} else {
val day = option as DayOfWeek
// 요일 토글
if (day in current.days) current.days - day else current.days + day
}
current.copy(days = newDays)
}
}
}
fetchMatchingMembers()
}

private fun <T> toggle(
current: T?,
selected: Any?,
): T? {
val selectedCasted = selected as T
return if (current == selectedCasted) null else selectedCasted
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.project200.feature.matching.map.filter

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.project200.feature.matching.map.MatchingMapViewModel
import com.project200.feature.matching.utils.FilterUiMapper
import com.project200.feature.matching.utils.MatchingFilterType
import com.project200.undabang.feature.matching.databinding.DialogFilterBottomSheetBinding
import com.project200.undabang.presentation.R
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch

@AndroidEntryPoint
class FilterBottomSheetDialog(
private val filterType: MatchingFilterType,
private val onOptionSelected: (Any?) -> Unit,
) : BottomSheetDialogFragment() {
private var _binding: DialogFilterBottomSheetBinding? = null
val binding get() = _binding!!

private val viewModel: MatchingMapViewModel by viewModels({ requireParentFragment() })

private val adapter by lazy {
FilterOptionRVAdapter { selectedItem ->
onOptionSelected(selectedItem)
if (!filterType.isMultiSelect) dismiss()
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.CustomBottomSheetDialogTheme)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = DialogFilterBottomSheetBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)
binding.filterOptionsRv.adapter = adapter

viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.filterState.collect { state ->
adapter.submitList(FilterUiMapper.mapToUiModels(filterType, state))
}
}
}

binding.closeBtn.setOnClickListener { dismiss() }
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.project200.feature.matching.map.filter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.core.widget.TextViewCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.project200.feature.matching.utils.FilterOptionUiModel
import com.project200.undabang.feature.matching.databinding.ItemFilterOptionBinding
import com.project200.undabang.presentation.R

class FilterOptionRVAdapter(
private val onClick: (Any?) -> Unit,
) : ListAdapter<FilterOptionUiModel, FilterOptionRVAdapter.ViewHolder>(DiffCallback) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): ViewHolder {
val binding = ItemFilterOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}

override fun onBindViewHolder(
holder: ViewHolder,
position: Int,
) {
holder.bind(getItem(position))
}

inner class ViewHolder(private val binding: ItemFilterOptionBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: FilterOptionUiModel) {
val context = binding.root.context
binding.optionTv.text = context.getString(item.labelResId)
binding.checkIv.isVisible = item.isSelected
TextViewCompat.setTextAppearance(binding.optionTv, if (item.isSelected) R.style.content_bold else R.style.content_regular)
binding.filterOptionLl.setOnClickListener { onClick(item.originalData) }
}
}

companion object {
private val DiffCallback =
object : DiffUtil.ItemCallback<FilterOptionUiModel>() {
override fun areItemsTheSame(
oldItem: FilterOptionUiModel,
newItem: FilterOptionUiModel,
): Boolean {
return oldItem.labelResId == newItem.labelResId
}

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