From 342dc5a6701af89af0f098c79f12c8a47fe57200 Mon Sep 17 00:00:00 2001 From: edv-Shin Date: Tue, 9 Dec 2025 03:57:36 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=EB=A7=A4=EC=B9=AD=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20ui=20=EA=B5=AC=ED=98=84=20#411?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project200/domain/model/FilterModel.kt | 43 ++++++++++ .../matching/map/FilterBottomSheetDialog.kt | 49 ++++++++++++ .../matching/map/MatchingFilterRVAdapter.kt | 78 +++++++++++++++++++ .../matching/map/MatchingMapFragment.kt | 38 +++++++++ .../matching/map/MatchingMapViewModel.kt | 45 +++++++++++ .../map/cluster/FilterOptionRVAdapter.kt | 47 +++++++++++ .../feature/matching/utils/FilterUiMapper.kt | 51 ++++++++++++ .../feature/matching/utils/FilterUiModel.kt | 30 +++++++ .../src/main/res/drawable/bg_filter_btn.xml | 7 ++ .../res/drawable/bg_filter_btn_selected.xml | 6 ++ .../src/main/res/drawable/ic_filter_check.xml | 10 +++ .../main/res/drawable/selector_filter_btn.xml | 5 ++ .../res/layout/dialog_filter_bottom_sheet.xml | 26 +++++++ .../main/res/layout/fragment_matching_map.xml | 15 +++- .../src/main/res/layout/item_filter.xml | 21 +++++ .../src/main/res/layout/item_filter_clear.xml | 21 +++++ .../main/res/layout/item_filter_option.xml | 20 +++++ .../matching/src/main/res/values/strings.xml | 7 ++ .../utils/FilterToStringMapper.kt | 49 ++++++++++++ presentation/src/main/res/values/strings.xml | 30 ++++++- 20 files changed, 595 insertions(+), 3 deletions(-) create mode 100644 domain/src/main/java/com/project200/domain/model/FilterModel.kt create mode 100644 feature/matching/src/main/java/com/project200/feature/matching/map/FilterBottomSheetDialog.kt create mode 100644 feature/matching/src/main/java/com/project200/feature/matching/map/MatchingFilterRVAdapter.kt create mode 100644 feature/matching/src/main/java/com/project200/feature/matching/map/cluster/FilterOptionRVAdapter.kt create mode 100644 feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt create mode 100644 feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt create mode 100644 feature/matching/src/main/res/drawable/bg_filter_btn.xml create mode 100644 feature/matching/src/main/res/drawable/bg_filter_btn_selected.xml create mode 100644 feature/matching/src/main/res/drawable/ic_filter_check.xml create mode 100644 feature/matching/src/main/res/drawable/selector_filter_btn.xml create mode 100644 feature/matching/src/main/res/layout/dialog_filter_bottom_sheet.xml create mode 100644 feature/matching/src/main/res/layout/item_filter.xml create mode 100644 feature/matching/src/main/res/layout/item_filter_clear.xml create mode 100644 feature/matching/src/main/res/layout/item_filter_option.xml create mode 100644 presentation/src/main/java/com/project200/presentation/utils/FilterToStringMapper.kt diff --git a/domain/src/main/java/com/project200/domain/model/FilterModel.kt b/domain/src/main/java/com/project200/domain/model/FilterModel.kt new file mode 100644 index 00000000..282967b5 --- /dev/null +++ b/domain/src/main/java/com/project200/domain/model/FilterModel.kt @@ -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) +} \ No newline at end of file diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/FilterBottomSheetDialog.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/FilterBottomSheetDialog.kt new file mode 100644 index 00000000..37296385 --- /dev/null +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/FilterBottomSheetDialog.kt @@ -0,0 +1,49 @@ +package com.project200.feature.matching.map + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.project200.feature.matching.map.cluster.FilterOptionRVAdapter +import com.project200.feature.matching.utils.FilterOptionUiModel +import com.project200.undabang.feature.matching.databinding.DialogFilterBottomSheetBinding +import com.project200.undabang.presentation.R + +class FilterBottomSheetDialog ( + private val options: List, + private val onOptionSelected: (Any) -> Unit +) : BottomSheetDialogFragment() { + + private var _binding: DialogFilterBottomSheetBinding? = null + private val binding get() = _binding!! + + 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) + + val adapter = FilterOptionRVAdapter { selectedItem -> + onOptionSelected(selectedItem) + dismiss() // TODO: 선택 시 닫기 (다중 선택이면 닫지 않음) + } + + binding.filterOptionsRv.adapter = adapter + adapter.submitList(options) + + binding.closeBtn.setOnClickListener { dismiss() } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingFilterRVAdapter.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingFilterRVAdapter.kt new file mode 100644 index 00000000..6e938b0b --- /dev/null +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingFilterRVAdapter.kt @@ -0,0 +1,78 @@ +package com.project200.feature.matching.map + +import android.content.res.ColorStateList +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.project200.feature.matching.utils.FilterState +import com.project200.feature.matching.utils.MatchingFilterType +import com.project200.undabang.feature.matching.databinding.ItemFilterBinding + +class MatchingFilterRVAdapter( + private val onClick: (MatchingFilterType) -> Unit +) : ListAdapter(DiffCallback) { + + // 현재 필터 상태를 저장 (어떤 필터가 활성화되었는지 확인용) + private var currentFilterState: FilterState = FilterState() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = ItemFilterBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + /** + * 필터 상태가 변경될 때 호출하여 UI 갱신 (선택 여부 표시) + */ + fun submitFilterState(newState: FilterState) { + this.currentFilterState = newState + notifyDataSetChanged() + } + + inner class ViewHolder(private val binding: ItemFilterBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: MatchingFilterType) { + val context = binding.root.context + binding.filterTitleTv.text = context.getString(item.labelResId) + + // 현재 필터 타입이 활성화되었는지 확인 + if (isFilterSelected(item)) { + binding.root.isSelected = true + binding.filterTitleTv.setTextColor(context.getColor(com.project200.undabang.presentation.R.color.white300)) + binding.filterIv.imageTintList= ColorStateList.valueOf(context.getColor(com.project200.undabang.presentation.R.color.white300)) + } else { + binding.root.isSelected = false + binding.filterTitleTv.setTextColor(context.getColor(com.project200.undabang.presentation.R.color.black)) + binding.filterIv.imageTintList= ColorStateList.valueOf(context.getColor(com.project200.undabang.presentation.R.color.black)) + } + + binding.root.setOnClickListener { onClick(item) } + } + + private fun isFilterSelected(type: MatchingFilterType): Boolean { + return when (type) { + MatchingFilterType.GENDER -> currentFilterState.gender != null + MatchingFilterType.AGE -> currentFilterState.ageGroup != null + MatchingFilterType.DAY -> currentFilterState.days != null + MatchingFilterType.SKILL -> currentFilterState.skillLevel != null + MatchingFilterType.SCORE -> currentFilterState.exerciseScore != null + } + } + } + + companion object { + private val DiffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: MatchingFilterType, newItem: MatchingFilterType): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: MatchingFilterType, newItem: MatchingFilterType): Boolean { + return oldItem == newItem + } + } + } +} \ No newline at end of file diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt index 7038e326..0a97cc35 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt @@ -25,8 +25,11 @@ import com.project200.common.constants.RuleConstants.SEOUL_CITY_HALL_LONGITUDE import com.project200.common.constants.RuleConstants.ZOOM_LEVEL import com.project200.domain.model.MapPosition import com.project200.domain.model.MatchingMember +import com.project200.feature.matching.map.MatchingFilterRVAdapter import com.project200.feature.matching.map.cluster.ClusterCalculator import com.project200.feature.matching.map.cluster.MapClusterItem +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 @@ -43,6 +46,12 @@ class MatchingMapFragment : private lateinit var fusedLocationClient: FusedLocationProviderClient private val viewModel: MatchingMapViewModel by viewModels() + private val filterAdapter by lazy { + MatchingFilterRVAdapter { type -> + viewModel.onFilterTypeClicked(type) + } + } + private var isMapInitialized: Boolean = false // 클러스터링 계산을 위한 헬퍼 클래스 @@ -69,6 +78,8 @@ class MatchingMapFragment : initMapView() initListeners() + binding.matchingFilterRv.adapter = filterAdapter + filterAdapter.submitList(MatchingFilterType.entries) } private fun initMapView() { @@ -157,6 +168,17 @@ 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) + } + } } } } @@ -252,6 +274,22 @@ 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( + options = options, + onOptionSelected = { selectedDomainData -> + viewModel.onFilterOptionSelected(type, selectedDomainData) + } + ) + bottomSheet.show(parentFragmentManager, FilterBottomSheetDialog::class.java.simpleName) + } + private fun checkPermissionAndMove() { if (isLocationPermissionGranted()) { moveToCurrentLocation() diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt index 5e0d0331..bb28ace8 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt @@ -4,22 +4,33 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.project200.domain.model.AgeGroup import com.project200.domain.model.BaseResult +import com.project200.domain.model.DayOfWeek import com.project200.domain.model.ExercisePlace +import com.project200.domain.model.ExerciseScore +import com.project200.domain.model.Gender import com.project200.domain.model.MapPosition import com.project200.domain.model.MatchingMember +import com.project200.domain.model.SkillLevel 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.FilterOptionUiModel +import com.project200.feature.matching.utils.FilterState +import com.project200.feature.matching.utils.MatchingFilterType +import com.project200.presentation.utils.labelResId 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 @@ -43,6 +54,15 @@ class MatchingMapViewModel private val _errorEvents = MutableSharedFlow() val errorEvents: SharedFlow = _errorEvents + // 필터 상태 + private val _filterState = MutableStateFlow(FilterState()) + val filterState: StateFlow = _filterState.asStateFlow() + + // 현재 선택된 필터 타입 + private val _currentFilterType = MutableSharedFlow() + val currentFilterType: SharedFlow = _currentFilterType + + val combinedMapData: StateFlow, List>> = combine( matchingMembers, @@ -84,6 +104,8 @@ class MatchingMapViewModel */ fun fetchMatchingMembers() { viewModelScope.launch { + val filters = _filterState.value + // useCase 호출 시 filters.gender, filters.ageGroup 등을 전달 matchingMembers.value = getMatchingMembersUseCase() } } @@ -140,6 +162,29 @@ class MatchingMapViewModel } } + // 필터 버튼 클릭 시 호출 + fun onFilterTypeClicked(type: MatchingFilterType) { + viewModelScope.launch { + _currentFilterType.emit(type) + } + } + + /** + * 필터 옵션 선택 시 호출 + */ + fun onFilterOptionSelected(type: MatchingFilterType, option: Any) { + _filterState.update { current -> + when (type) { + MatchingFilterType.GENDER -> current.copy(gender = option as? Gender) + MatchingFilterType.AGE -> current.copy(ageGroup = option as? AgeGroup) + MatchingFilterType.SKILL -> current.copy(skillLevel = option as? SkillLevel) + MatchingFilterType.SCORE -> current.copy(exerciseScore = option as? ExerciseScore) + MatchingFilterType.DAY -> { current.copy(days = option as? DayOfWeek) } + } + } + fetchMatchingMembers() + } + companion object { const val NO_URL = "404" } diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/cluster/FilterOptionRVAdapter.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/cluster/FilterOptionRVAdapter.kt new file mode 100644 index 00000000..eb5348f3 --- /dev/null +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/cluster/FilterOptionRVAdapter.kt @@ -0,0 +1,47 @@ +package com.project200.feature.matching.map.cluster + +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(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() { + override fun areItemsTheSame(oldItem: FilterOptionUiModel, newItem: FilterOptionUiModel): Boolean { + return oldItem.labelResId == newItem.labelResId + } + + override fun areContentsTheSame(oldItem: FilterOptionUiModel, newItem: FilterOptionUiModel): Boolean { + return oldItem == newItem + } + } + } +} \ No newline at end of file diff --git a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt new file mode 100644 index 00000000..4a4c9df7 --- /dev/null +++ b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt @@ -0,0 +1,51 @@ +package com.project200.feature.matching.utils + +import com.project200.domain.model.* +import com.project200.feature.matching.utils.* +import com.project200.presentation.utils.labelResId // 기존에 만드신 확장 프로퍼티 import + +object FilterUiMapper { + + fun mapToUiModels( + type: MatchingFilterType, + currentState: FilterState + ): List { + return when (type) { + MatchingFilterType.GENDER -> Gender.entries.map { gender -> + FilterOptionUiModel( + labelResId = gender.labelResId, + isSelected = currentState.gender == gender, + originalData = gender + ) + } + MatchingFilterType.AGE -> AgeGroup.entries.map { age -> + FilterOptionUiModel( + labelResId = age.labelResId, + isSelected = currentState.ageGroup == age, + originalData = age + ) + } + MatchingFilterType.DAY -> DayOfWeek.entries.map { day -> + FilterOptionUiModel( + labelResId = day.labelResId, + isSelected = currentState.days == day, + originalData = day + ) + } + MatchingFilterType.SKILL -> SkillLevel.entries.map { skill -> + FilterOptionUiModel( + labelResId = skill.labelResId, + isSelected = currentState.skillLevel == skill, + originalData = skill + ) + } + MatchingFilterType.SCORE -> ExerciseScore.entries.map { score -> + FilterOptionUiModel( + labelResId = score.labelResId, + isSelected = currentState.exerciseScore == score, + originalData = score + ) + } + } + } +} \ No newline at end of file diff --git a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt new file mode 100644 index 00000000..6aea7906 --- /dev/null +++ b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt @@ -0,0 +1,30 @@ +package com.project200.feature.matching.utils + +import com.project200.domain.model.AgeGroup +import com.project200.domain.model.DayOfWeek +import com.project200.domain.model.ExerciseScore +import com.project200.domain.model.Gender +import com.project200.domain.model.SkillLevel +import com.project200.undabang.feature.matching.R + +enum class MatchingFilterType(val labelResId: Int) { + GENDER(R.string.filter_gender), // 성별 + AGE(R.string.filter_age), // 나이 + DAY(R.string.filter_day), // 요일 + SKILL(R.string.filter_skill), // 숙련도 + SCORE(R.string.filter_score) // 점수 +} + +data class FilterState( + val gender: Gender? = null, + val ageGroup: AgeGroup? = null, + val days: DayOfWeek? = null, + val skillLevel: SkillLevel? = null, + val exerciseScore: ExerciseScore? = null +) + +data class FilterOptionUiModel( + val labelResId: Int, + val isSelected: Boolean, + val originalData: Any // 선택된 Enum 객체 (Gender, AgeGroup 등) +) diff --git a/feature/matching/src/main/res/drawable/bg_filter_btn.xml b/feature/matching/src/main/res/drawable/bg_filter_btn.xml new file mode 100644 index 00000000..96acc1e6 --- /dev/null +++ b/feature/matching/src/main/res/drawable/bg_filter_btn.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/feature/matching/src/main/res/drawable/bg_filter_btn_selected.xml b/feature/matching/src/main/res/drawable/bg_filter_btn_selected.xml new file mode 100644 index 00000000..f8b7a304 --- /dev/null +++ b/feature/matching/src/main/res/drawable/bg_filter_btn_selected.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/feature/matching/src/main/res/drawable/ic_filter_check.xml b/feature/matching/src/main/res/drawable/ic_filter_check.xml new file mode 100644 index 00000000..25fc294b --- /dev/null +++ b/feature/matching/src/main/res/drawable/ic_filter_check.xml @@ -0,0 +1,10 @@ + + + diff --git a/feature/matching/src/main/res/drawable/selector_filter_btn.xml b/feature/matching/src/main/res/drawable/selector_filter_btn.xml new file mode 100644 index 00000000..f76528f7 --- /dev/null +++ b/feature/matching/src/main/res/drawable/selector_filter_btn.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/feature/matching/src/main/res/layout/dialog_filter_bottom_sheet.xml b/feature/matching/src/main/res/layout/dialog_filter_bottom_sheet.xml new file mode 100644 index 00000000..706ab7b6 --- /dev/null +++ b/feature/matching/src/main/res/layout/dialog_filter_bottom_sheet.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/feature/matching/src/main/res/layout/fragment_matching_map.xml b/feature/matching/src/main/res/layout/fragment_matching_map.xml index d1d0b92d..08adf221 100644 --- a/feature/matching/src/main/res/layout/fragment_matching_map.xml +++ b/feature/matching/src/main/res/layout/fragment_matching_map.xml @@ -2,15 +2,16 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + + + + \ No newline at end of file diff --git a/feature/matching/src/main/res/layout/item_filter_clear.xml b/feature/matching/src/main/res/layout/item_filter_clear.xml new file mode 100644 index 00000000..bfe20f18 --- /dev/null +++ b/feature/matching/src/main/res/layout/item_filter_clear.xml @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/feature/matching/src/main/res/layout/item_filter_option.xml b/feature/matching/src/main/res/layout/item_filter_option.xml new file mode 100644 index 00000000..dc654e76 --- /dev/null +++ b/feature/matching/src/main/res/layout/item_filter_option.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/feature/matching/src/main/res/values/strings.xml b/feature/matching/src/main/res/values/strings.xml index 5eb1fc86..976db259 100644 --- a/feature/matching/src/main/res/values/strings.xml +++ b/feature/matching/src/main/res/values/strings.xml @@ -86,4 +86,11 @@ 운동장소 등록에 실패했습니다. 1:1 채팅하기 + + 성별 + 나이 + 운동 요일 + 운동 종목 + 숙련도 + 운동 점수 \ No newline at end of file diff --git a/presentation/src/main/java/com/project200/presentation/utils/FilterToStringMapper.kt b/presentation/src/main/java/com/project200/presentation/utils/FilterToStringMapper.kt new file mode 100644 index 00000000..2e705508 --- /dev/null +++ b/presentation/src/main/java/com/project200/presentation/utils/FilterToStringMapper.kt @@ -0,0 +1,49 @@ +package com.project200.presentation.utils + +import com.project200.domain.model.* +import com.project200.undabang.presentation.R + +val Gender.labelResId: Int + get() = when (this) { + Gender.MALE -> R.string.gender_male + Gender.FEMALE -> R.string.gender_female + } + +val AgeGroup.labelResId: Int + get() = when (this) { + AgeGroup.TEEN -> R.string.age_10 + AgeGroup.TWENTIES -> R.string.age_20 + AgeGroup.THIRTIES -> R.string.age_30 + AgeGroup.FORTIES -> R.string.age_40 + AgeGroup.FIFTIES -> R.string.age_50 + AgeGroup.SIXTIES_PLUS -> R.string.age_60 + } + +val DayOfWeek.labelResId: Int + get() = when (this) { + DayOfWeek.MONDAY -> R.string.mon + DayOfWeek.TUESDAY -> R.string.tue + DayOfWeek.WEDNESDAY -> R.string.wed + DayOfWeek.THURSDAY -> R.string.thu + DayOfWeek.FRIDAY -> R.string.fri + DayOfWeek.SATURDAY -> R.string.sat + DayOfWeek.SUNDAY -> R.string.sun + } + +val SkillLevel.labelResId: Int + get() = when (this) { + SkillLevel.NOVICE -> R.string.novice + SkillLevel.BEGINNER -> R.string.beginner + SkillLevel.INTERMEDIATE -> R.string.intermediate + SkillLevel.ADVANCED -> R.string.advanced + SkillLevel.EXPERT -> R.string.expert + SkillLevel.PROFESSIONAL -> R.string.professional + } + +val ExerciseScore.labelResId: Int + get() = when (this) { + ExerciseScore.OVER_20 -> R.string.score_over_20 + ExerciseScore.OVER_40 -> R.string.score_over_40 + ExerciseScore.OVER_60 -> R.string.score_over_60 + ExerciseScore.OVER_80 -> R.string.score_over_80 + } \ No newline at end of file diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 905f07b3..bd1bed1b 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -15,7 +15,35 @@ 업데이트 알림 새로운 버전이 출시되었습니다.\n앱을 업데이트해주세요. - 업데이트 + 업데이트 남성 + 여성 + + 10대 + 20대 + 30대 + 40대 + 50대 + 60대 이상 + + 20점 이상 + 40점 이상 + 60점 이상 + 80점 이상 + + + + + + + + + + 입문 + 초급 + 중급 + 고급 + 숙련 + 선출 나중에 From c6a3d80013c62c792e9dc2fccd09bb4eac6a6e7a Mon Sep 17 00:00:00 2001 From: edv-Shin Date: Tue, 9 Dec 2025 05:03:38 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EB=8B=A4=EC=A4=91=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=B6=94=EA=B0=80=20#411?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../matching/map/FilterBottomSheetDialog.kt | 33 +++++++++++++++---- .../matching/map/MatchingFilterRVAdapter.kt | 23 +++++++++++-- .../matching/map/MatchingMapFragment.kt | 10 ++---- .../matching/map/MatchingMapViewModel.kt | 25 +++++++++----- .../feature/matching/utils/FilterUiMapper.kt | 2 +- .../feature/matching/utils/FilterUiModel.kt | 9 +++-- 6 files changed, 73 insertions(+), 29 deletions(-) diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/FilterBottomSheetDialog.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/FilterBottomSheetDialog.kt index 37296385..3c160abd 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/FilterBottomSheetDialog.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/FilterBottomSheetDialog.kt @@ -4,20 +4,38 @@ 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.cluster.FilterOptionRVAdapter import com.project200.feature.matching.utils.FilterOptionUiModel +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 options: List, + private val filterType: MatchingFilterType, private val onOptionSelected: (Any) -> Unit ) : BottomSheetDialogFragment() { private var _binding: DialogFilterBottomSheetBinding? = null private 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) @@ -30,15 +48,16 @@ class FilterBottomSheetDialog ( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.filterOptionsRv.adapter = adapter - val adapter = FilterOptionRVAdapter { selectedItem -> - onOptionSelected(selectedItem) - dismiss() // TODO: 선택 시 닫기 (다중 선택이면 닫지 않음) + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.filterState.collect { state -> + adapter.submitList(FilterUiMapper.mapToUiModels(filterType, state)) + } + } } - binding.filterOptionsRv.adapter = adapter - adapter.submitList(options) - binding.closeBtn.setOnClickListener { dismiss() } } diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingFilterRVAdapter.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingFilterRVAdapter.kt index 6e938b0b..46107366 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingFilterRVAdapter.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingFilterRVAdapter.kt @@ -8,6 +8,7 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.project200.feature.matching.utils.FilterState import com.project200.feature.matching.utils.MatchingFilterType +import com.project200.presentation.utils.labelResId import com.project200.undabang.feature.matching.databinding.ItemFilterBinding class MatchingFilterRVAdapter( @@ -37,7 +38,14 @@ class MatchingFilterRVAdapter( inner class ViewHolder(private val binding: ItemFilterBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(item: MatchingFilterType) { val context = binding.root.context - binding.filterTitleTv.text = context.getString(item.labelResId) + val labelResId = if (item.isMultiSelect) { + // 다중 선택은 필터 이름 표시 + item.labelResId + } else { + // 단일 선택은 선택된 값이 있으면 그 값의 이름을, 없으면 필터 이름을 표시 + getSelectedOptionLabelResId(item) ?: item.labelResId + } + binding.filterTitleTv.text = context.getString(labelResId) // 현재 필터 타입이 활성화되었는지 확인 if (isFilterSelected(item)) { @@ -57,11 +65,22 @@ class MatchingFilterRVAdapter( return when (type) { MatchingFilterType.GENDER -> currentFilterState.gender != null MatchingFilterType.AGE -> currentFilterState.ageGroup != null - MatchingFilterType.DAY -> currentFilterState.days != null + MatchingFilterType.DAY -> currentFilterState.days.isNotEmpty() MatchingFilterType.SKILL -> currentFilterState.skillLevel != null MatchingFilterType.SCORE -> currentFilterState.exerciseScore != null } } + + // 선택된 옵션의 라벨 리소스 ID를 반환 + private fun getSelectedOptionLabelResId(type: MatchingFilterType): Int? { + return when (type) { + MatchingFilterType.GENDER -> currentFilterState.gender?.labelResId + MatchingFilterType.AGE -> currentFilterState.ageGroup?.labelResId + MatchingFilterType.SKILL -> currentFilterState.skillLevel?.labelResId + MatchingFilterType.SCORE -> currentFilterState.exerciseScore?.labelResId + else -> null + } + } } companion object { diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt index 0a97cc35..4465f582 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt @@ -168,12 +168,6 @@ 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) @@ -282,12 +276,12 @@ class MatchingMapFragment : ) val bottomSheet = FilterBottomSheetDialog( - options = options, + filterType = type, onOptionSelected = { selectedDomainData -> viewModel.onFilterOptionSelected(type, selectedDomainData) } ) - bottomSheet.show(parentFragmentManager, FilterBottomSheetDialog::class.java.simpleName) + bottomSheet.show(childFragmentManager, FilterBottomSheetDialog::class.java.simpleName) } private fun checkPermissionAndMove() { diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt index bb28ace8..c3a470e4 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt @@ -12,6 +12,7 @@ import com.project200.domain.model.ExerciseScore import com.project200.domain.model.Gender import com.project200.domain.model.MapPosition import com.project200.domain.model.MatchingMember +import com.project200.domain.model.Score import com.project200.domain.model.SkillLevel import com.project200.domain.usecase.GetExercisePlaceUseCase import com.project200.domain.usecase.GetLastMapPositionUseCase @@ -175,17 +176,25 @@ class MatchingMapViewModel fun onFilterOptionSelected(type: MatchingFilterType, option: Any) { _filterState.update { current -> when (type) { - MatchingFilterType.GENDER -> current.copy(gender = option as? Gender) - MatchingFilterType.AGE -> current.copy(ageGroup = option as? AgeGroup) - MatchingFilterType.SKILL -> current.copy(skillLevel = option as? SkillLevel) - MatchingFilterType.SCORE -> current.copy(exerciseScore = option as? ExerciseScore) - MatchingFilterType.DAY -> { current.copy(days = option as? DayOfWeek) } + 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 day = option as DayOfWeek + current.copy(days = if (day in current.days) { + current.days - day // 이미 있으면 제거 + } else { + current.days + day // 없으면 추가 + }) + } } } fetchMatchingMembers() } - companion object { - const val NO_URL = "404" - } + private fun toggle(current: T?, selected: Any): T? { + val selectedCasted = selected as T + return if (current == selectedCasted) null else selectedCasted + } } diff --git a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt index 4a4c9df7..3361bab7 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt @@ -28,7 +28,7 @@ object FilterUiMapper { MatchingFilterType.DAY -> DayOfWeek.entries.map { day -> FilterOptionUiModel( labelResId = day.labelResId, - isSelected = currentState.days == day, + isSelected = currentState.days.contains(day), originalData = day ) } diff --git a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt index 6aea7906..8a290a4f 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt @@ -7,10 +7,13 @@ import com.project200.domain.model.Gender import com.project200.domain.model.SkillLevel import com.project200.undabang.feature.matching.R -enum class MatchingFilterType(val labelResId: Int) { +enum class MatchingFilterType( + val labelResId: Int, + val isMultiSelect: Boolean = false +) { GENDER(R.string.filter_gender), // 성별 AGE(R.string.filter_age), // 나이 - DAY(R.string.filter_day), // 요일 + DAY(R.string.filter_day, true), // 요일 SKILL(R.string.filter_skill), // 숙련도 SCORE(R.string.filter_score) // 점수 } @@ -18,7 +21,7 @@ enum class MatchingFilterType(val labelResId: Int) { data class FilterState( val gender: Gender? = null, val ageGroup: AgeGroup? = null, - val days: DayOfWeek? = null, + val days: Set = emptySet(), val skillLevel: SkillLevel? = null, val exerciseScore: ExerciseScore? = null ) From 92a599be037d4f515bf6e1f9a4a1f999803efff8 Mon Sep 17 00:00:00 2001 From: edv-Shin Date: Tue, 9 Dec 2025 05:05:11 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=ED=95=84=ED=84=B0=20=EA=B0=84?= =?UTF-8?q?=EA=B2=A9=20=EC=B6=94=EA=B0=80=20#411?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/matching/src/main/res/layout/fragment_matching_map.xml | 3 ++- feature/matching/src/main/res/layout/item_filter.xml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/feature/matching/src/main/res/layout/fragment_matching_map.xml b/feature/matching/src/main/res/layout/fragment_matching_map.xml index 08adf221..07e18ce9 100644 --- a/feature/matching/src/main/res/layout/fragment_matching_map.xml +++ b/feature/matching/src/main/res/layout/fragment_matching_map.xml @@ -32,7 +32,8 @@ android:clipToPadding="false" android:orientation="horizontal" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - android:paddingHorizontal="@dimen/base_horizontal_margin"/> + android:paddingStart="@dimen/base_horizontal_margin" + android:paddingEnd="10dp"/> + android:paddingVertical="4dp" + android:layout_marginEnd="10dp"> Date: Tue, 9 Dec 2025 05:07:27 +0900 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20=ED=95=84=ED=84=B0=20ui=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0=20=EC=95=88=EB=90=98=EB=8A=94=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20#411?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project200/feature/matching/map/MatchingMapFragment.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt index 4465f582..258ae2a3 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt @@ -168,6 +168,11 @@ 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) From 8a597c705a9ea4039f65e2303e0ae64aca99e6c8 Mon Sep 17 00:00:00 2001 From: edv-Shin Date: Tue, 9 Dec 2025 05:43:02 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD,=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#411?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../matching/map/MatchingFilterRVAdapter.kt | 97 ------------------- .../matching/map/MatchingMapFragment.kt | 16 ++- .../matching/map/MatchingMapViewModel.kt | 8 ++ .../{ => filter}/FilterBottomSheetDialog.kt | 6 +- .../FilterOptionRVAdapter.kt | 2 +- .../map/filter/MatchingFilterRVAdapter.kt | 75 ++++++++++++++ .../map/filter/MatchingFilterViewHolders.kt | 78 +++++++++++++++ .../feature/matching/utils/FilterUiModel.kt | 5 + .../src/main/res/drawable/ic_filter_clear.xml | 9 ++ .../src/main/res/layout/item_filter_clear.xml | 7 +- .../matching/src/main/res/values/strings.xml | 2 + 11 files changed, 196 insertions(+), 109 deletions(-) delete mode 100644 feature/matching/src/main/java/com/project200/feature/matching/map/MatchingFilterRVAdapter.kt rename feature/matching/src/main/java/com/project200/feature/matching/map/{ => filter}/FilterBottomSheetDialog.kt (92%) rename feature/matching/src/main/java/com/project200/feature/matching/map/{cluster => filter}/FilterOptionRVAdapter.kt (97%) create mode 100644 feature/matching/src/main/java/com/project200/feature/matching/map/filter/MatchingFilterRVAdapter.kt create mode 100644 feature/matching/src/main/java/com/project200/feature/matching/map/filter/MatchingFilterViewHolders.kt create mode 100644 feature/matching/src/main/res/drawable/ic_filter_clear.xml diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingFilterRVAdapter.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingFilterRVAdapter.kt deleted file mode 100644 index 46107366..00000000 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingFilterRVAdapter.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.project200.feature.matching.map - -import android.content.res.ColorStateList -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.project200.feature.matching.utils.FilterState -import com.project200.feature.matching.utils.MatchingFilterType -import com.project200.presentation.utils.labelResId -import com.project200.undabang.feature.matching.databinding.ItemFilterBinding - -class MatchingFilterRVAdapter( - private val onClick: (MatchingFilterType) -> Unit -) : ListAdapter(DiffCallback) { - - // 현재 필터 상태를 저장 (어떤 필터가 활성화되었는지 확인용) - private var currentFilterState: FilterState = FilterState() - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val binding = ItemFilterBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return ViewHolder(binding) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - /** - * 필터 상태가 변경될 때 호출하여 UI 갱신 (선택 여부 표시) - */ - fun submitFilterState(newState: FilterState) { - this.currentFilterState = newState - notifyDataSetChanged() - } - - inner class ViewHolder(private val binding: ItemFilterBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(item: MatchingFilterType) { - val context = binding.root.context - val labelResId = if (item.isMultiSelect) { - // 다중 선택은 필터 이름 표시 - item.labelResId - } else { - // 단일 선택은 선택된 값이 있으면 그 값의 이름을, 없으면 필터 이름을 표시 - getSelectedOptionLabelResId(item) ?: item.labelResId - } - binding.filterTitleTv.text = context.getString(labelResId) - - // 현재 필터 타입이 활성화되었는지 확인 - if (isFilterSelected(item)) { - binding.root.isSelected = true - binding.filterTitleTv.setTextColor(context.getColor(com.project200.undabang.presentation.R.color.white300)) - binding.filterIv.imageTintList= ColorStateList.valueOf(context.getColor(com.project200.undabang.presentation.R.color.white300)) - } else { - binding.root.isSelected = false - binding.filterTitleTv.setTextColor(context.getColor(com.project200.undabang.presentation.R.color.black)) - binding.filterIv.imageTintList= ColorStateList.valueOf(context.getColor(com.project200.undabang.presentation.R.color.black)) - } - - binding.root.setOnClickListener { onClick(item) } - } - - private fun isFilterSelected(type: MatchingFilterType): Boolean { - return when (type) { - MatchingFilterType.GENDER -> currentFilterState.gender != null - MatchingFilterType.AGE -> currentFilterState.ageGroup != null - MatchingFilterType.DAY -> currentFilterState.days.isNotEmpty() - MatchingFilterType.SKILL -> currentFilterState.skillLevel != null - MatchingFilterType.SCORE -> currentFilterState.exerciseScore != null - } - } - - // 선택된 옵션의 라벨 리소스 ID를 반환 - private fun getSelectedOptionLabelResId(type: MatchingFilterType): Int? { - return when (type) { - MatchingFilterType.GENDER -> currentFilterState.gender?.labelResId - MatchingFilterType.AGE -> currentFilterState.ageGroup?.labelResId - MatchingFilterType.SKILL -> currentFilterState.skillLevel?.labelResId - MatchingFilterType.SCORE -> currentFilterState.exerciseScore?.labelResId - else -> null - } - } - } - - companion object { - private val DiffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: MatchingFilterType, newItem: MatchingFilterType): Boolean { - return oldItem == newItem - } - - override fun areContentsTheSame(oldItem: MatchingFilterType, newItem: MatchingFilterType): Boolean { - return oldItem == newItem - } - } - } -} \ No newline at end of file diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt index 258ae2a3..3f1d634d 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt @@ -25,9 +25,10 @@ import com.project200.common.constants.RuleConstants.SEOUL_CITY_HALL_LONGITUDE import com.project200.common.constants.RuleConstants.ZOOM_LEVEL import com.project200.domain.model.MapPosition import com.project200.domain.model.MatchingMember -import com.project200.feature.matching.map.MatchingFilterRVAdapter +import com.project200.feature.matching.map.filter.MatchingFilterRVAdapter 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.utils.FilterUiMapper import com.project200.feature.matching.utils.MatchingFilterType import com.project200.presentation.base.BindingFragment @@ -47,9 +48,14 @@ class MatchingMapFragment : private val viewModel: MatchingMapViewModel by viewModels() private val filterAdapter by lazy { - MatchingFilterRVAdapter { type -> - viewModel.onFilterTypeClicked(type) - } + MatchingFilterRVAdapter( + onFilterClick = { type -> + viewModel.onFilterTypeClicked(type) + }, + onClearClick = { + viewModel.clearFilters() + } + ) } private var isMapInitialized: Boolean = false @@ -79,7 +85,7 @@ class MatchingMapFragment : initMapView() initListeners() binding.matchingFilterRv.adapter = filterAdapter - filterAdapter.submitList(MatchingFilterType.entries) + filterAdapter.submitFilterList(MatchingFilterType.entries) } private fun initMapView() { diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt index c3a470e4..cb511337 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt @@ -170,6 +170,14 @@ class MatchingMapViewModel } } + /** + * 필터 초기화 + */ + fun clearFilters() { + _filterState.value = FilterState() + fetchMatchingMembers() + } + /** * 필터 옵션 선택 시 호출 */ diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/FilterBottomSheetDialog.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterBottomSheetDialog.kt similarity index 92% rename from feature/matching/src/main/java/com/project200/feature/matching/map/FilterBottomSheetDialog.kt rename to feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterBottomSheetDialog.kt index 3c160abd..a86f16c9 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/FilterBottomSheetDialog.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterBottomSheetDialog.kt @@ -1,4 +1,4 @@ -package com.project200.feature.matching.map +package com.project200.feature.matching.map.filter import android.os.Bundle import android.view.LayoutInflater @@ -9,8 +9,8 @@ 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.cluster.FilterOptionRVAdapter -import com.project200.feature.matching.utils.FilterOptionUiModel +import com.project200.feature.matching.map.MatchingMapViewModel +import com.project200.feature.matching.map.filter.FilterOptionRVAdapter import com.project200.feature.matching.utils.FilterUiMapper import com.project200.feature.matching.utils.MatchingFilterType import com.project200.undabang.feature.matching.databinding.DialogFilterBottomSheetBinding diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/cluster/FilterOptionRVAdapter.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterOptionRVAdapter.kt similarity index 97% rename from feature/matching/src/main/java/com/project200/feature/matching/map/cluster/FilterOptionRVAdapter.kt rename to feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterOptionRVAdapter.kt index eb5348f3..78595a88 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/cluster/FilterOptionRVAdapter.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterOptionRVAdapter.kt @@ -1,4 +1,4 @@ -package com.project200.feature.matching.map.cluster +package com.project200.feature.matching.map.filter import android.view.LayoutInflater import android.view.ViewGroup diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/filter/MatchingFilterRVAdapter.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/MatchingFilterRVAdapter.kt new file mode 100644 index 00000000..c5baf2b7 --- /dev/null +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/MatchingFilterRVAdapter.kt @@ -0,0 +1,75 @@ +package com.project200.feature.matching.map.filter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.project200.feature.matching.utils.FilterListItem +import com.project200.feature.matching.utils.FilterState +import com.project200.feature.matching.utils.MatchingFilterType +import com.project200.undabang.feature.matching.databinding.ItemFilterBinding +import com.project200.undabang.feature.matching.databinding.ItemFilterClearBinding + +class MatchingFilterRVAdapter( + private val onFilterClick: (MatchingFilterType) -> Unit, + private val onClearClick: () -> Unit +) : ListAdapter(DiffCallback) { + + // 현재 필터 상태를 저장 (어떤 필터가 활성화되었는지 확인용) + private var currentFilterState: FilterState = FilterState() + + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is FilterListItem.ClearButton -> TYPE_CLEAR + is FilterListItem.FilterItem -> TYPE_FILTER + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return if (viewType == TYPE_CLEAR) { + val binding = ItemFilterClearBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ClearViewHolder(binding, onClearClick) + } else { + val binding = ItemFilterBinding.inflate(LayoutInflater.from(parent.context), parent, false) + FilterViewHolder(binding, onFilterClick) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (val item = getItem(position)) { + is FilterListItem.ClearButton -> (holder as ClearViewHolder).bind() + is FilterListItem.FilterItem -> (holder as FilterViewHolder).bind(item.type, currentFilterState) + } + } + + /** + * 필터 상태가 변경될 때 호출하여 UI 갱신 (선택 여부 표시) + */ + fun submitFilterState(newState: FilterState) { + this.currentFilterState = newState + notifyDataSetChanged() + } + + fun submitFilterList(filters: List) { + val list = mutableListOf() + list.add(FilterListItem.ClearButton) // 맨 앞에 초기화 버튼 추가 + list.addAll(filters.map { FilterListItem.FilterItem(it) }) + submitList(list) + } + + companion object { + private const val TYPE_CLEAR = 0 + private const val TYPE_FILTER = 1 + + private val DiffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: FilterListItem, newItem: FilterListItem): Boolean { + return oldItem == newItem + } + override fun areContentsTheSame(oldItem: FilterListItem, newItem: FilterListItem): Boolean { + return oldItem == newItem + } + } + } + +} \ No newline at end of file diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/filter/MatchingFilterViewHolders.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/MatchingFilterViewHolders.kt new file mode 100644 index 00000000..e11212e3 --- /dev/null +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/MatchingFilterViewHolders.kt @@ -0,0 +1,78 @@ +package com.project200.feature.matching.map.filter + +import android.content.res.ColorStateList +import androidx.recyclerview.widget.RecyclerView +import com.project200.feature.matching.utils.FilterState +import com.project200.feature.matching.utils.MatchingFilterType +import com.project200.presentation.utils.labelResId +import com.project200.undabang.feature.matching.databinding.ItemFilterBinding +import com.project200.undabang.feature.matching.databinding.ItemFilterClearBinding +import com.project200.undabang.presentation.R + +/** + * 초기화 뷰홀더 + */ +class ClearViewHolder( + private val binding: ItemFilterClearBinding, + private val onClearClick: () -> Unit +) : RecyclerView.ViewHolder(binding.root) { + + fun bind() { + binding.root.setOnClickListener { onClearClick() } + } +} + +/** + * 필터 항목 뷰홀더 + */ +class FilterViewHolder( + private val binding: ItemFilterBinding, + private val onFilterClick: (MatchingFilterType) -> Unit +) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: MatchingFilterType, currentState: FilterState) { + val context = binding.root.context + + // 라벨 설정 + val labelResId = if (item.isMultiSelect) { + item.labelResId + } else { + getSelectedOptionLabelResId(item, currentState) ?: item.labelResId + } + binding.filterTitleTv.text = context.getString(labelResId) + + // 선택 상태 디자인 + val isSelected = isFilterSelected(item, currentState) + binding.root.isSelected = isSelected + + + binding.filterTitleTv.setTextColor( + context.getColor(if (isSelected) R.color.white300 else R.color.black) + ) + binding.filterIv.imageTintList = ColorStateList.valueOf( + context.getColor(if (isSelected) R.color.white300 else R.color.black) + ) + + binding.root.setOnClickListener { onFilterClick(item) } + } + + private fun isFilterSelected(type: MatchingFilterType, state: FilterState): Boolean { + return when (type) { + MatchingFilterType.GENDER -> state.gender != null + MatchingFilterType.AGE -> state.ageGroup != null + MatchingFilterType.DAY -> state.days.isNotEmpty() + MatchingFilterType.SKILL -> state.skillLevel != null + MatchingFilterType.SCORE -> state.exerciseScore != null + } + } + + // 선택된 옵션의 라벨 리소스 ID를 반환 + private fun getSelectedOptionLabelResId(type: MatchingFilterType, state: FilterState): Int? { + return when (type) { + MatchingFilterType.GENDER -> state.gender?.labelResId + MatchingFilterType.AGE -> state.ageGroup?.labelResId + MatchingFilterType.SKILL -> state.skillLevel?.labelResId + MatchingFilterType.SCORE -> state.exerciseScore?.labelResId + else -> null + } + } +} \ No newline at end of file diff --git a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt index 8a290a4f..fbe512fb 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt @@ -7,6 +7,11 @@ import com.project200.domain.model.Gender import com.project200.domain.model.SkillLevel import com.project200.undabang.feature.matching.R +sealed class FilterListItem { + data object ClearButton : FilterListItem() // 초기화 버튼 + data class FilterItem(val type: MatchingFilterType) : FilterListItem() // 필터 버튼 +} + enum class MatchingFilterType( val labelResId: Int, val isMultiSelect: Boolean = false diff --git a/feature/matching/src/main/res/drawable/ic_filter_clear.xml b/feature/matching/src/main/res/drawable/ic_filter_clear.xml new file mode 100644 index 00000000..55a80212 --- /dev/null +++ b/feature/matching/src/main/res/drawable/ic_filter_clear.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/matching/src/main/res/layout/item_filter_clear.xml b/feature/matching/src/main/res/layout/item_filter_clear.xml index bfe20f18..02c4b2c9 100644 --- a/feature/matching/src/main/res/layout/item_filter_clear.xml +++ b/feature/matching/src/main/res/layout/item_filter_clear.xml @@ -4,17 +4,18 @@ android:layout_height="wrap_content" android:background="@drawable/bg_filter_btn" android:paddingHorizontal="10dp" - android:paddingVertical="4dp"> + android:paddingVertical="4dp" + android:layout_marginEnd="10dp"> diff --git a/feature/matching/src/main/res/values/strings.xml b/feature/matching/src/main/res/values/strings.xml index 976db259..a9c144bc 100644 --- a/feature/matching/src/main/res/values/strings.xml +++ b/feature/matching/src/main/res/values/strings.xml @@ -86,6 +86,8 @@ 운동장소 등록에 실패했습니다. 1:1 채팅하기 + + 초기화 성별 나이 From 39ee8a773b771a14623205b0f668a13db68a73ab Mon Sep 17 00:00:00 2001 From: edv-Shin Date: Tue, 9 Dec 2025 05:55:46 +0900 Subject: [PATCH 6/8] =?UTF-8?q?fix:=20ktlint=20=EB=AC=B8=EB=B2=95=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20#411?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../matching/map/MatchingMapFragment.kt | 26 +++--- .../matching/map/MatchingMapViewModel.kt | 87 +++++++++--------- .../map/filter/FilterBottomSheetDialog.kt | 21 +++-- .../map/filter/FilterOptionRVAdapter.kt | 37 +++++--- .../map/filter/MatchingFilterRVAdapter.kt | 38 +++++--- .../map/filter/MatchingFilterViewHolders.kt | 43 +++++---- .../feature/matching/utils/FilterUiMapper.kt | 89 ++++++++++--------- .../feature/matching/utils/FilterUiModel.kt | 17 ++-- .../utils/FilterToStringMapper.kt | 81 +++++++++-------- 9 files changed, 249 insertions(+), 190 deletions(-) diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt index 3f1d634d..82bebe33 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt @@ -25,10 +25,10 @@ import com.project200.common.constants.RuleConstants.SEOUL_CITY_HALL_LONGITUDE import com.project200.common.constants.RuleConstants.ZOOM_LEVEL import com.project200.domain.model.MapPosition import com.project200.domain.model.MatchingMember -import com.project200.feature.matching.map.filter.MatchingFilterRVAdapter 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 @@ -54,7 +54,7 @@ class MatchingMapFragment : }, onClearClick = { viewModel.clearFilters() - } + }, ) } @@ -281,17 +281,19 @@ class MatchingMapFragment : private fun showFilterBottomSheet(type: MatchingFilterType) { // 필터 옵션들을 UI 모델로 매핑 - val options = FilterUiMapper.mapToUiModels( - type = type, - currentState = viewModel.filterState.value - ) + val options = + FilterUiMapper.mapToUiModels( + type = type, + currentState = viewModel.filterState.value, + ) - val bottomSheet = FilterBottomSheetDialog( - filterType = type, - onOptionSelected = { selectedDomainData -> - viewModel.onFilterOptionSelected(type, selectedDomainData) - } - ) + val bottomSheet = + FilterBottomSheetDialog( + filterType = type, + onOptionSelected = { selectedDomainData -> + viewModel.onFilterOptionSelected(type, selectedDomainData) + }, + ) bottomSheet.show(childFragmentManager, FilterBottomSheetDialog::class.java.simpleName) } diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt index cb511337..99914a8f 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt @@ -4,24 +4,17 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.project200.domain.model.AgeGroup import com.project200.domain.model.BaseResult import com.project200.domain.model.DayOfWeek import com.project200.domain.model.ExercisePlace -import com.project200.domain.model.ExerciseScore -import com.project200.domain.model.Gender import com.project200.domain.model.MapPosition import com.project200.domain.model.MatchingMember -import com.project200.domain.model.Score -import com.project200.domain.model.SkillLevel 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.FilterOptionUiModel import com.project200.feature.matching.utils.FilterState import com.project200.feature.matching.utils.MatchingFilterType -import com.project200.presentation.utils.labelResId import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -63,7 +56,6 @@ class MatchingMapViewModel private val _currentFilterType = MutableSharedFlow() val currentFilterType: SharedFlow = _currentFilterType - val combinedMapData: StateFlow, List>> = combine( matchingMembers, @@ -163,46 +155,55 @@ class MatchingMapViewModel } } - // 필터 버튼 클릭 시 호출 - fun onFilterTypeClicked(type: MatchingFilterType) { - viewModelScope.launch { - _currentFilterType.emit(type) + // 필터 버튼 클릭 시 호출 + fun onFilterTypeClicked(type: MatchingFilterType) { + viewModelScope.launch { + _currentFilterType.emit(type) + } } - } - /** - * 필터 초기화 - */ - fun clearFilters() { - _filterState.value = FilterState() - fetchMatchingMembers() - } + /** + * 필터 초기화 + */ + 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 day = option as DayOfWeek - current.copy(days = if (day in current.days) { - current.days - day // 이미 있으면 제거 - } else { - current.days + day // 없으면 추가 - }) + /** + * 필터 옵션 선택 시 호출 + */ + 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 day = option as DayOfWeek + current.copy( + days = + if (day in current.days) { + current.days - day // 이미 있으면 제거 + } else { + current.days + day // 없으면 추가 + }, + ) + } } } + fetchMatchingMembers() } - fetchMatchingMembers() - } - private fun toggle(current: T?, selected: Any): T? { - val selectedCasted = selected as T - return if (current == selectedCasted) null else selectedCasted - } + private fun toggle( + current: T?, + selected: Any, + ): T? { + val selectedCasted = selected as T + return if (current == selectedCasted) null else selectedCasted + } } diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterBottomSheetDialog.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterBottomSheetDialog.kt index a86f16c9..cab42aa0 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterBottomSheetDialog.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterBottomSheetDialog.kt @@ -10,7 +10,6 @@ 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.map.filter.FilterOptionRVAdapter import com.project200.feature.matching.utils.FilterUiMapper import com.project200.feature.matching.utils.MatchingFilterType import com.project200.undabang.feature.matching.databinding.DialogFilterBottomSheetBinding @@ -19,13 +18,12 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @AndroidEntryPoint -class FilterBottomSheetDialog ( +class FilterBottomSheetDialog( private val filterType: MatchingFilterType, - private val onOptionSelected: (Any) -> Unit + private val onOptionSelected: (Any) -> Unit, ) : BottomSheetDialogFragment() { - private var _binding: DialogFilterBottomSheetBinding? = null - private val binding get() = _binding!! + val binding get() = _binding!! private val viewModel: MatchingMapViewModel by viewModels({ requireParentFragment() }) @@ -41,12 +39,19 @@ class FilterBottomSheetDialog ( setStyle(STYLE_NORMAL, R.style.CustomBottomSheetDialogTheme) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + 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?) { + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { super.onViewCreated(view, savedInstanceState) binding.filterOptionsRv.adapter = adapter @@ -65,4 +70,4 @@ class FilterBottomSheetDialog ( super.onDestroyView() _binding = null } -} \ No newline at end of file +} diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterOptionRVAdapter.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterOptionRVAdapter.kt index 78595a88..a5ed7257 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterOptionRVAdapter.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterOptionRVAdapter.kt @@ -12,15 +12,20 @@ import com.project200.undabang.feature.matching.databinding.ItemFilterOptionBind import com.project200.undabang.presentation.R class FilterOptionRVAdapter( - private val onClick: (Any) -> Unit + private val onClick: (Any) -> Unit, ) : ListAdapter(DiffCallback) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + 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) { + override fun onBindViewHolder( + holder: ViewHolder, + position: Int, + ) { holder.bind(getItem(position)) } @@ -33,15 +38,23 @@ class FilterOptionRVAdapter( binding.filterOptionLl.setOnClickListener { onClick(item.originalData) } } } + companion object { - private val DiffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: FilterOptionUiModel, newItem: FilterOptionUiModel): Boolean { - return oldItem.labelResId == newItem.labelResId - } + private val DiffCallback = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: FilterOptionUiModel, + newItem: FilterOptionUiModel, + ): Boolean { + return oldItem.labelResId == newItem.labelResId + } - override fun areContentsTheSame(oldItem: FilterOptionUiModel, newItem: FilterOptionUiModel): Boolean { - return oldItem == newItem + override fun areContentsTheSame( + oldItem: FilterOptionUiModel, + newItem: FilterOptionUiModel, + ): Boolean { + return oldItem == newItem + } } - } } -} \ No newline at end of file +} diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/filter/MatchingFilterRVAdapter.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/MatchingFilterRVAdapter.kt index c5baf2b7..5d4b6cd2 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/filter/MatchingFilterRVAdapter.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/MatchingFilterRVAdapter.kt @@ -13,9 +13,8 @@ import com.project200.undabang.feature.matching.databinding.ItemFilterClearBindi class MatchingFilterRVAdapter( private val onFilterClick: (MatchingFilterType) -> Unit, - private val onClearClick: () -> Unit + private val onClearClick: () -> Unit, ) : ListAdapter(DiffCallback) { - // 현재 필터 상태를 저장 (어떤 필터가 활성화되었는지 확인용) private var currentFilterState: FilterState = FilterState() @@ -26,7 +25,10 @@ class MatchingFilterRVAdapter( } } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): RecyclerView.ViewHolder { return if (viewType == TYPE_CLEAR) { val binding = ItemFilterClearBinding.inflate(LayoutInflater.from(parent.context), parent, false) ClearViewHolder(binding, onClearClick) @@ -36,7 +38,10 @@ class MatchingFilterRVAdapter( } } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + ) { when (val item = getItem(position)) { is FilterListItem.ClearButton -> (holder as ClearViewHolder).bind() is FilterListItem.FilterItem -> (holder as FilterViewHolder).bind(item.type, currentFilterState) @@ -62,14 +67,21 @@ class MatchingFilterRVAdapter( private const val TYPE_CLEAR = 0 private const val TYPE_FILTER = 1 - private val DiffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: FilterListItem, newItem: FilterListItem): Boolean { - return oldItem == newItem - } - override fun areContentsTheSame(oldItem: FilterListItem, newItem: FilterListItem): Boolean { - return oldItem == newItem + private val DiffCallback = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: FilterListItem, + newItem: FilterListItem, + ): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame( + oldItem: FilterListItem, + newItem: FilterListItem, + ): Boolean { + return oldItem == newItem + } } - } } - -} \ No newline at end of file +} diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/filter/MatchingFilterViewHolders.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/MatchingFilterViewHolders.kt index e11212e3..593827f9 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/filter/MatchingFilterViewHolders.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/MatchingFilterViewHolders.kt @@ -14,9 +14,8 @@ import com.project200.undabang.presentation.R */ class ClearViewHolder( private val binding: ItemFilterClearBinding, - private val onClearClick: () -> Unit + private val onClearClick: () -> Unit, ) : RecyclerView.ViewHolder(binding.root) { - fun bind() { binding.root.setOnClickListener { onClearClick() } } @@ -27,35 +26,42 @@ class ClearViewHolder( */ class FilterViewHolder( private val binding: ItemFilterBinding, - private val onFilterClick: (MatchingFilterType) -> Unit + private val onFilterClick: (MatchingFilterType) -> Unit, ) : RecyclerView.ViewHolder(binding.root) { - fun bind(item: MatchingFilterType, currentState: FilterState) { + fun bind( + item: MatchingFilterType, + currentState: FilterState, + ) { val context = binding.root.context // 라벨 설정 - val labelResId = if (item.isMultiSelect) { - item.labelResId - } else { - getSelectedOptionLabelResId(item, currentState) ?: item.labelResId - } + val labelResId = + if (item.isMultiSelect) { + item.labelResId + } else { + getSelectedOptionLabelResId(item, currentState) ?: item.labelResId + } binding.filterTitleTv.text = context.getString(labelResId) // 선택 상태 디자인 val isSelected = isFilterSelected(item, currentState) binding.root.isSelected = isSelected - binding.filterTitleTv.setTextColor( - context.getColor(if (isSelected) R.color.white300 else R.color.black) - ) - binding.filterIv.imageTintList = ColorStateList.valueOf( - context.getColor(if (isSelected) R.color.white300 else R.color.black) + context.getColor(if (isSelected) R.color.white300 else R.color.black), ) + binding.filterIv.imageTintList = + ColorStateList.valueOf( + context.getColor(if (isSelected) R.color.white300 else R.color.black), + ) binding.root.setOnClickListener { onFilterClick(item) } } - private fun isFilterSelected(type: MatchingFilterType, state: FilterState): Boolean { + private fun isFilterSelected( + type: MatchingFilterType, + state: FilterState, + ): Boolean { return when (type) { MatchingFilterType.GENDER -> state.gender != null MatchingFilterType.AGE -> state.ageGroup != null @@ -66,7 +72,10 @@ class FilterViewHolder( } // 선택된 옵션의 라벨 리소스 ID를 반환 - private fun getSelectedOptionLabelResId(type: MatchingFilterType, state: FilterState): Int? { + private fun getSelectedOptionLabelResId( + type: MatchingFilterType, + state: FilterState, + ): Int? { return when (type) { MatchingFilterType.GENDER -> state.gender?.labelResId MatchingFilterType.AGE -> state.ageGroup?.labelResId @@ -75,4 +84,4 @@ class FilterViewHolder( else -> null } } -} \ No newline at end of file +} diff --git a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt index 3361bab7..7589ebb2 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt @@ -1,51 +1,58 @@ package com.project200.feature.matching.utils -import com.project200.domain.model.* -import com.project200.feature.matching.utils.* -import com.project200.presentation.utils.labelResId // 기존에 만드신 확장 프로퍼티 import +import com.project200.domain.model.AgeGroup +import com.project200.domain.model.DayOfWeek +import com.project200.domain.model.ExerciseScore +import com.project200.domain.model.Gender +import com.project200.domain.model.SkillLevel +import com.project200.presentation.utils.labelResId object FilterUiMapper { - fun mapToUiModels( type: MatchingFilterType, - currentState: FilterState + currentState: FilterState, ): List { return when (type) { - MatchingFilterType.GENDER -> Gender.entries.map { gender -> - FilterOptionUiModel( - labelResId = gender.labelResId, - isSelected = currentState.gender == gender, - originalData = gender - ) - } - MatchingFilterType.AGE -> AgeGroup.entries.map { age -> - FilterOptionUiModel( - labelResId = age.labelResId, - isSelected = currentState.ageGroup == age, - originalData = age - ) - } - MatchingFilterType.DAY -> DayOfWeek.entries.map { day -> - FilterOptionUiModel( - labelResId = day.labelResId, - isSelected = currentState.days.contains(day), - originalData = day - ) - } - MatchingFilterType.SKILL -> SkillLevel.entries.map { skill -> - FilterOptionUiModel( - labelResId = skill.labelResId, - isSelected = currentState.skillLevel == skill, - originalData = skill - ) - } - MatchingFilterType.SCORE -> ExerciseScore.entries.map { score -> - FilterOptionUiModel( - labelResId = score.labelResId, - isSelected = currentState.exerciseScore == score, - originalData = score - ) - } + MatchingFilterType.GENDER -> + Gender.entries.map { gender -> + FilterOptionUiModel( + labelResId = gender.labelResId, + isSelected = currentState.gender == gender, + originalData = gender, + ) + } + MatchingFilterType.AGE -> + AgeGroup.entries.map { age -> + FilterOptionUiModel( + labelResId = age.labelResId, + isSelected = currentState.ageGroup == age, + originalData = age, + ) + } + MatchingFilterType.DAY -> + DayOfWeek.entries.map { day -> + FilterOptionUiModel( + labelResId = day.labelResId, + isSelected = currentState.days.contains(day), + originalData = day, + ) + } + MatchingFilterType.SKILL -> + SkillLevel.entries.map { skill -> + FilterOptionUiModel( + labelResId = skill.labelResId, + isSelected = currentState.skillLevel == skill, + originalData = skill, + ) + } + MatchingFilterType.SCORE -> + ExerciseScore.entries.map { score -> + FilterOptionUiModel( + labelResId = score.labelResId, + isSelected = currentState.exerciseScore == score, + originalData = score, + ) + } } } -} \ No newline at end of file +} diff --git a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt index fbe512fb..b71f09eb 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt @@ -9,18 +9,19 @@ import com.project200.undabang.feature.matching.R sealed class FilterListItem { data object ClearButton : FilterListItem() // 초기화 버튼 + data class FilterItem(val type: MatchingFilterType) : FilterListItem() // 필터 버튼 } enum class MatchingFilterType( val labelResId: Int, - val isMultiSelect: Boolean = false + val isMultiSelect: Boolean = false, ) { - GENDER(R.string.filter_gender), // 성별 - AGE(R.string.filter_age), // 나이 - DAY(R.string.filter_day, true), // 요일 - SKILL(R.string.filter_skill), // 숙련도 - SCORE(R.string.filter_score) // 점수 + GENDER(R.string.filter_gender), // 성별 + AGE(R.string.filter_age), // 나이 + DAY(R.string.filter_day, true), // 요일 + SKILL(R.string.filter_skill), // 숙련도 + SCORE(R.string.filter_score), // 점수 } data class FilterState( @@ -28,11 +29,11 @@ data class FilterState( val ageGroup: AgeGroup? = null, val days: Set = emptySet(), val skillLevel: SkillLevel? = null, - val exerciseScore: ExerciseScore? = null + val exerciseScore: ExerciseScore? = null, ) data class FilterOptionUiModel( val labelResId: Int, val isSelected: Boolean, - val originalData: Any // 선택된 Enum 객체 (Gender, AgeGroup 등) + val originalData: Any, // 선택된 Enum 객체 (Gender, AgeGroup 등) ) diff --git a/presentation/src/main/java/com/project200/presentation/utils/FilterToStringMapper.kt b/presentation/src/main/java/com/project200/presentation/utils/FilterToStringMapper.kt index 2e705508..f8d093a0 100644 --- a/presentation/src/main/java/com/project200/presentation/utils/FilterToStringMapper.kt +++ b/presentation/src/main/java/com/project200/presentation/utils/FilterToStringMapper.kt @@ -1,49 +1,58 @@ package com.project200.presentation.utils -import com.project200.domain.model.* +import com.project200.domain.model.AgeGroup +import com.project200.domain.model.DayOfWeek +import com.project200.domain.model.ExerciseScore +import com.project200.domain.model.Gender +import com.project200.domain.model.SkillLevel import com.project200.undabang.presentation.R val Gender.labelResId: Int - get() = when (this) { - Gender.MALE -> R.string.gender_male - Gender.FEMALE -> R.string.gender_female - } + get() = + when (this) { + Gender.MALE -> R.string.gender_male + Gender.FEMALE -> R.string.gender_female + } val AgeGroup.labelResId: Int - get() = when (this) { - AgeGroup.TEEN -> R.string.age_10 - AgeGroup.TWENTIES -> R.string.age_20 - AgeGroup.THIRTIES -> R.string.age_30 - AgeGroup.FORTIES -> R.string.age_40 - AgeGroup.FIFTIES -> R.string.age_50 - AgeGroup.SIXTIES_PLUS -> R.string.age_60 - } + get() = + when (this) { + AgeGroup.TEEN -> R.string.age_10 + AgeGroup.TWENTIES -> R.string.age_20 + AgeGroup.THIRTIES -> R.string.age_30 + AgeGroup.FORTIES -> R.string.age_40 + AgeGroup.FIFTIES -> R.string.age_50 + AgeGroup.SIXTIES_PLUS -> R.string.age_60 + } val DayOfWeek.labelResId: Int - get() = when (this) { - DayOfWeek.MONDAY -> R.string.mon - DayOfWeek.TUESDAY -> R.string.tue - DayOfWeek.WEDNESDAY -> R.string.wed - DayOfWeek.THURSDAY -> R.string.thu - DayOfWeek.FRIDAY -> R.string.fri - DayOfWeek.SATURDAY -> R.string.sat - DayOfWeek.SUNDAY -> R.string.sun - } + get() = + when (this) { + DayOfWeek.MONDAY -> R.string.mon + DayOfWeek.TUESDAY -> R.string.tue + DayOfWeek.WEDNESDAY -> R.string.wed + DayOfWeek.THURSDAY -> R.string.thu + DayOfWeek.FRIDAY -> R.string.fri + DayOfWeek.SATURDAY -> R.string.sat + DayOfWeek.SUNDAY -> R.string.sun + } val SkillLevel.labelResId: Int - get() = when (this) { - SkillLevel.NOVICE -> R.string.novice - SkillLevel.BEGINNER -> R.string.beginner - SkillLevel.INTERMEDIATE -> R.string.intermediate - SkillLevel.ADVANCED -> R.string.advanced - SkillLevel.EXPERT -> R.string.expert - SkillLevel.PROFESSIONAL -> R.string.professional - } + get() = + when (this) { + SkillLevel.NOVICE -> R.string.novice + SkillLevel.BEGINNER -> R.string.beginner + SkillLevel.INTERMEDIATE -> R.string.intermediate + SkillLevel.ADVANCED -> R.string.advanced + SkillLevel.EXPERT -> R.string.expert + SkillLevel.PROFESSIONAL -> R.string.professional + } val ExerciseScore.labelResId: Int - get() = when (this) { - ExerciseScore.OVER_20 -> R.string.score_over_20 - ExerciseScore.OVER_40 -> R.string.score_over_40 - ExerciseScore.OVER_60 -> R.string.score_over_60 - ExerciseScore.OVER_80 -> R.string.score_over_80 - } \ No newline at end of file + get() = + when (this) { + ExerciseScore.OVER_20 -> R.string.score_over_20 + ExerciseScore.OVER_40 -> R.string.score_over_40 + ExerciseScore.OVER_60 -> R.string.score_over_60 + ExerciseScore.OVER_80 -> R.string.score_over_80 + } From 732fa9f47e64a57383ee9912440f9d19f2ab0c01 Mon Sep 17 00:00:00 2001 From: edv-Shin Date: Tue, 9 Dec 2025 11:00:34 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=EB=8B=A4=EC=A4=91=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=A0=84=EC=B2=B4=20=EC=98=B5=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#411?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../matching/map/MatchingMapViewModel.kt | 22 ++++++++--------- .../map/filter/FilterBottomSheetDialog.kt | 2 +- .../map/filter/FilterOptionRVAdapter.kt | 2 +- .../feature/matching/utils/FilterUiMapper.kt | 24 +++++++++++++++---- .../feature/matching/utils/FilterUiModel.kt | 2 +- presentation/src/main/res/values/strings.xml | 2 ++ 6 files changed, 36 insertions(+), 18 deletions(-) diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt index 99914a8f..beb29304 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt @@ -175,7 +175,7 @@ class MatchingMapViewModel */ fun onFilterOptionSelected( type: MatchingFilterType, - option: Any, + option: Any?, ) { _filterState.update { current -> when (type) { @@ -184,15 +184,15 @@ class MatchingMapViewModel MatchingFilterType.SKILL -> current.copy(skillLevel = toggle(current.skillLevel, option)) MatchingFilterType.SCORE -> current.copy(exerciseScore = toggle(current.exerciseScore, option)) MatchingFilterType.DAY -> { - val day = option as DayOfWeek - current.copy( - days = - if (day in current.days) { - current.days - day // 이미 있으면 제거 - } else { - current.days + 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) } } } @@ -201,7 +201,7 @@ class MatchingMapViewModel private fun toggle( current: T?, - selected: Any, + selected: Any?, ): T? { val selectedCasted = selected as T return if (current == selectedCasted) null else selectedCasted diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterBottomSheetDialog.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterBottomSheetDialog.kt index cab42aa0..62f8f918 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterBottomSheetDialog.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterBottomSheetDialog.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.launch @AndroidEntryPoint class FilterBottomSheetDialog( private val filterType: MatchingFilterType, - private val onOptionSelected: (Any) -> Unit, + private val onOptionSelected: (Any?) -> Unit, ) : BottomSheetDialogFragment() { private var _binding: DialogFilterBottomSheetBinding? = null val binding get() = _binding!! diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterOptionRVAdapter.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterOptionRVAdapter.kt index a5ed7257..d621a694 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterOptionRVAdapter.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/filter/FilterOptionRVAdapter.kt @@ -12,7 +12,7 @@ import com.project200.undabang.feature.matching.databinding.ItemFilterOptionBind import com.project200.undabang.presentation.R class FilterOptionRVAdapter( - private val onClick: (Any) -> Unit, + private val onClick: (Any?) -> Unit, ) : ListAdapter(DiffCallback) { override fun onCreateViewHolder( parent: ViewGroup, diff --git a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt index 7589ebb2..d20d6d36 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt @@ -5,6 +5,7 @@ import com.project200.domain.model.DayOfWeek import com.project200.domain.model.ExerciseScore import com.project200.domain.model.Gender import com.project200.domain.model.SkillLevel +import com.project200.feature.matching.map.MatchingMapViewModel import com.project200.presentation.utils.labelResId object FilterUiMapper { @@ -29,14 +30,29 @@ object FilterUiMapper { originalData = age, ) } - MatchingFilterType.DAY -> - DayOfWeek.entries.map { day -> + MatchingFilterType.DAY -> { + val list = mutableListOf() + + // 전체 옵션 + list.add( + FilterOptionUiModel( + labelResId = com.project200.undabang.presentation.R.string.filter_all, + isSelected = currentState.days.isEmpty(), + originalData = null + ) + ) + + // 요일 옵션 + list.addAll(DayOfWeek.entries.map { day -> FilterOptionUiModel( labelResId = day.labelResId, isSelected = currentState.days.contains(day), - originalData = day, + originalData = day ) - } + }) + + list + } MatchingFilterType.SKILL -> SkillLevel.entries.map { skill -> FilterOptionUiModel( diff --git a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt index b71f09eb..fdee4c41 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiModel.kt @@ -35,5 +35,5 @@ data class FilterState( data class FilterOptionUiModel( val labelResId: Int, val isSelected: Boolean, - val originalData: Any, // 선택된 Enum 객체 (Gender, AgeGroup 등) + val originalData: Any?, // 선택된 Enum 객체 (Gender, AgeGroup 등) ) diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index bd1bed1b..cd69bd4f 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -18,6 +18,8 @@ 업데이트 남성 여성 + 전체 + 10대 20대 30대 From b8cb29b595404938baac5f92cbe88072cd582938 Mon Sep 17 00:00:00 2001 From: edv-Shin Date: Tue, 9 Dec 2025 11:26:22 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20ktlint=20=EB=AC=B8=EB=B2=95=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20#411?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../matching/map/MatchingMapViewModel.kt | 17 ++++++++------- .../feature/matching/utils/FilterUiMapper.kt | 21 ++++++++++--------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt index beb29304..d192d6e0 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapViewModel.kt @@ -184,14 +184,15 @@ class MatchingMapViewModel 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 - } + 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) } } diff --git a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt index d20d6d36..919c47df 100644 --- a/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt +++ b/feature/matching/src/main/java/com/project200/feature/matching/utils/FilterUiMapper.kt @@ -5,7 +5,6 @@ import com.project200.domain.model.DayOfWeek import com.project200.domain.model.ExerciseScore import com.project200.domain.model.Gender import com.project200.domain.model.SkillLevel -import com.project200.feature.matching.map.MatchingMapViewModel import com.project200.presentation.utils.labelResId object FilterUiMapper { @@ -38,18 +37,20 @@ object FilterUiMapper { FilterOptionUiModel( labelResId = com.project200.undabang.presentation.R.string.filter_all, isSelected = currentState.days.isEmpty(), - originalData = null - ) + originalData = null, + ), ) // 요일 옵션 - list.addAll(DayOfWeek.entries.map { day -> - FilterOptionUiModel( - labelResId = day.labelResId, - isSelected = currentState.days.contains(day), - originalData = day - ) - }) + list.addAll( + DayOfWeek.entries.map { day -> + FilterOptionUiModel( + labelResId = day.labelResId, + isSelected = currentState.days.contains(day), + originalData = day, + ) + }, + ) list }