diff --git a/common/src/main/java/com/project200/common/constants/DayOfWeek.kt b/common/src/main/java/com/project200/common/constants/DayOfWeek.kt new file mode 100644 index 00000000..447fa4df --- /dev/null +++ b/common/src/main/java/com/project200/common/constants/DayOfWeek.kt @@ -0,0 +1,22 @@ +package com.project200.common.constants + +import androidx.annotation.StringRes +import com.project200.undabang.common.R + +enum class DayOfWeek( + val index: Int, + @StringRes val resId: Int, +) { + MON(0, R.string.day_mon), + TUE(1, R.string.day_tue), + WED(2, R.string.day_wed), + THU(3, R.string.day_thu), + FRI(4, R.string.day_fri), + SAT(5, R.string.day_sat), + SUN(6, R.string.day_sun), + ; + + companion object { + fun getByIndex(index: Int): DayOfWeek? = entries.find { it.index == index } + } +} diff --git a/common/src/main/java/com/project200/common/utils/PreferredExerciseDayFormatter.kt b/common/src/main/java/com/project200/common/utils/PreferredExerciseDayFormatter.kt new file mode 100644 index 00000000..94db7696 --- /dev/null +++ b/common/src/main/java/com/project200/common/utils/PreferredExerciseDayFormatter.kt @@ -0,0 +1,65 @@ +package com.project200.common.utils + +import android.content.Context +import com.project200.common.constants.DayOfWeek +import com.project200.undabang.common.R +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class PreferredExerciseDayFormatter + @Inject + constructor( + @ApplicationContext private val context: Context, + ) { + /** + * 월요일부터 일요일까지의 선택 여부를 담은 Boolean 리스트를 + * 지정된 규칙에 따라 문자열로 포맷합니다. + * @param days 크기가 7인 Boolean 리스트 [월, 화, 수, 목, 금, 토, 일] 순서 + * @return 포맷팅된 요일 문자열 + */ + fun formatDaysOfWeek(days: List): String { + if (days.size != 7) return "" + + // 모든 요일이 포함될 경우 "매일"로 표기 + if (days.all { it }) return context.getString(R.string.day_everyday) + + val resultParts = mutableListOf() + val weekdays = days.subList(0, 5) // 월요일부터 금요일까지 + val weekend = days.subList(5, 7) // 토요일, 일요일 + + // 평일이 모두 포함될 경우 "평일"로 표기 + if (weekdays.all { it }) { + resultParts.add(context.getString(R.string.day_weekdays)) + } else { + // 각각 해당하는 평일 날짜를 추가 + weekdays.forEachIndexed { index, isSelected -> + if (isSelected) { + addDayName(index, resultParts) + } + } + } + + // 주말이 모두 포함될 경우 "주말"로 표기 + if (weekend.all { it }) { + resultParts.add(context.getString(R.string.day_weekend)) + } else { + // 각각 해당하는 주말 날짜를 추가 + weekend.forEachIndexed { index, isSelected -> + if (isSelected) { + addDayName(index + 5, resultParts) + } + } + } + + return resultParts.joinToString(", ") + } + + private fun addDayName( + index: Int, + resultList: MutableList, + ) { + DayOfWeek.getByIndex(index)?.let { day -> + resultList.add(context.getString(day.resId)) + } + } + } diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml new file mode 100644 index 00000000..4232100b --- /dev/null +++ b/common/src/main/res/values/strings.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + 매일 + 평일 + 주말 + \ No newline at end of file diff --git a/data/src/main/java/com/project200/data/api/ApiService.kt b/data/src/main/java/com/project200/data/api/ApiService.kt index b21c2b7b..598bfa8f 100644 --- a/data/src/main/java/com/project200/data/api/ApiService.kt +++ b/data/src/main/java/com/project200/data/api/ApiService.kt @@ -167,6 +167,13 @@ interface ApiService { @AccessTokenApi suspend fun getBlockedMembers(): BaseResponse> + /** 선호 운동 */ + // 선호 운동 조회 + // TODO: 실제 api 명세 나오면 수정 필요 + @GET("api/v1/preferred-exercises") + @AccessTokenApi + suspend fun getPreferredExercises(): BaseResponse> + /** 운동 기록 */ // 구간별 운동 기록 횟수 조회 @GET("api/v1/exercises/count") diff --git a/data/src/main/java/com/project200/data/impl/MemberRepositoryImpl.kt b/data/src/main/java/com/project200/data/impl/MemberRepositoryImpl.kt index c0094af9..a997af56 100644 --- a/data/src/main/java/com/project200/data/impl/MemberRepositoryImpl.kt +++ b/data/src/main/java/com/project200/data/impl/MemberRepositoryImpl.kt @@ -16,6 +16,7 @@ import com.project200.data.utils.apiCallBuilder import com.project200.domain.model.BaseResult import com.project200.domain.model.BlockedMember import com.project200.domain.model.OpenUrl +import com.project200.domain.model.PreferredExercise import com.project200.domain.model.ProfileImageList import com.project200.domain.model.Score import com.project200.domain.model.UserProfile @@ -158,6 +159,32 @@ class MemberRepositoryImpl ) } + override suspend fun getPreferredExercises(): BaseResult> { +/* return apiCallBuilder( + ioDispatcher = ioDispatcher, + apiCall = { apiService.getPreferredExercises() }, + mapper = { + Unit + }, + )*/ + return BaseResult.Success( + List(6) { index -> + PreferredExercise( + preferredExerciseId = index, + name = "운동종류 $index", + skillLevel = + when (index % 3) { + 0 -> "초급" + 1 -> "중급" + else -> "고급" + }, + daysOfWeek = List(7) { dayIndex -> (dayIndex + index) % 2 == 0 }, + imageUrl = "https://example.com/exercise_image_$index.png", + ) + }, + ) + } + companion object { const val IMAGE_PART_ERROR = "IMAGE_PART_ERROR" } diff --git a/domain/src/main/java/com/project200/domain/repository/MemberRepository.kt b/domain/src/main/java/com/project200/domain/repository/MemberRepository.kt index f2764885..2c5f875a 100644 --- a/domain/src/main/java/com/project200/domain/repository/MemberRepository.kt +++ b/domain/src/main/java/com/project200/domain/repository/MemberRepository.kt @@ -3,6 +3,7 @@ package com.project200.domain.repository import com.project200.domain.model.BaseResult import com.project200.domain.model.BlockedMember import com.project200.domain.model.OpenUrl +import com.project200.domain.model.PreferredExercise import com.project200.domain.model.ProfileImageList import com.project200.domain.model.Score import com.project200.domain.model.UserProfile @@ -21,4 +22,5 @@ interface MemberRepository { suspend fun blockMember(memberId: String): BaseResult suspend fun unblockMember(memberId: String): BaseResult suspend fun getBlockedMembers(): BaseResult> + suspend fun getPreferredExercises(): BaseResult> } \ No newline at end of file diff --git a/domain/src/main/java/com/project200/domain/usecase/GetPreferredExerciseUseCase.kt b/domain/src/main/java/com/project200/domain/usecase/GetPreferredExerciseUseCase.kt new file mode 100644 index 00000000..688a44c8 --- /dev/null +++ b/domain/src/main/java/com/project200/domain/usecase/GetPreferredExerciseUseCase.kt @@ -0,0 +1,14 @@ +package com.project200.domain.usecase + +import com.project200.domain.model.BaseResult +import com.project200.domain.model.PreferredExercise +import com.project200.domain.repository.MemberRepository +import javax.inject.Inject + +class GetPreferredExerciseUseCase @Inject constructor( + private val repository: MemberRepository, +) { + suspend operator fun invoke(): BaseResult> { + return repository.getPreferredExercises() + } +} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/project200/undabang/profile/mypage/MypageFragment.kt b/feature/profile/src/main/java/com/project200/undabang/profile/mypage/MypageFragment.kt index dbcf6bb7..65dc6d31 100644 --- a/feature/profile/src/main/java/com/project200/undabang/profile/mypage/MypageFragment.kt +++ b/feature/profile/src/main/java/com/project200/undabang/profile/mypage/MypageFragment.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager import com.bumptech.glide.Glide import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.DayPosition @@ -16,6 +17,7 @@ import com.kizitonwose.calendar.core.daysOfWeek import com.kizitonwose.calendar.view.MonthDayBinder import com.kizitonwose.calendar.view.ViewContainer import com.project200.common.utils.CommonDateTimeFormatters.YYYY_M_KR +import com.project200.common.utils.PreferredExerciseDayFormatter import com.project200.presentation.base.BindingFragment import com.project200.undabang.feature.profile.R import com.project200.undabang.feature.profile.databinding.CalendarDayLayoutBinding @@ -27,11 +29,16 @@ import java.time.DayOfWeek import java.time.LocalDate import java.time.YearMonth import java.time.ZoneId +import javax.inject.Inject @AndroidEntryPoint class MypageFragment : BindingFragment(R.layout.fragment_mypage) { private val viewModel: MypageViewModel by viewModels() private var exerciseCompleteDates: Set = emptySet() + lateinit var preferredExerciseRVAdapter: PreferredExerciseRVAdapter + + @Inject + lateinit var dayFormatter: PreferredExerciseDayFormatter override fun getViewBinding(view: View): FragmentMypageBinding { return FragmentMypageBinding.bind(view) @@ -40,6 +47,7 @@ class MypageFragment : BindingFragment(R.layout.fragment_ override fun setupViews() { initClickListener() setupCalendar() + setupPreferredExercise() } private fun initClickListener() { @@ -67,6 +75,14 @@ class MypageFragment : BindingFragment(R.layout.fragment_ binding.nextMonthBtn.setOnClickListener { viewModel.onNextMonthClicked() } + + binding.preferredExerciseEditBtn.setOnClickListener { + // TODO: 운동 선호도 설정 화면으로 이동 + } + + binding.preferredExerciseEmptyTv.setOnClickListener { + // TODO: 운동 선호도 설정 화면으로 이동 + } } override fun setupObservers() { @@ -97,6 +113,12 @@ class MypageFragment : BindingFragment(R.layout.fragment_ binding.exerciseCalendar.scrollToMonth(month) } + viewModel.preferredExercise.observe(viewLifecycleOwner) { exercises -> + preferredExerciseRVAdapter.setItems(exercises) + binding.preferredExerciseEmptyTv.isVisible = exercises.isEmpty() + binding.preferredExerciseRv.isVisible = exercises.isNotEmpty() + } + viewModel.exerciseDates.observe(viewLifecycleOwner) { dates -> exerciseCompleteDates = dates binding.exerciseCalendar.notifyCalendarChanged() @@ -201,6 +223,14 @@ class MypageFragment : BindingFragment(R.layout.fragment_ } } + private fun setupPreferredExercise() { + preferredExerciseRVAdapter = PreferredExerciseRVAdapter(formatter = dayFormatter) + binding.preferredExerciseRv.apply { + adapter = preferredExerciseRVAdapter + layoutManager = LinearLayoutManager(requireContext()) + } + } + private fun setupProfileImage( thumbnailUrl: String?, imageUrl: String?, diff --git a/feature/profile/src/main/java/com/project200/undabang/profile/mypage/MypageViewModel.kt b/feature/profile/src/main/java/com/project200/undabang/profile/mypage/MypageViewModel.kt index 9ef63ca2..f09300ef 100644 --- a/feature/profile/src/main/java/com/project200/undabang/profile/mypage/MypageViewModel.kt +++ b/feature/profile/src/main/java/com/project200/undabang/profile/mypage/MypageViewModel.kt @@ -6,9 +6,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.project200.common.utils.ClockProvider import com.project200.domain.model.BaseResult -import com.project200.domain.model.OpenUrl +import com.project200.domain.model.PreferredExercise import com.project200.domain.model.UserProfile import com.project200.domain.usecase.GetExerciseCountInMonthUseCase +import com.project200.domain.usecase.GetPreferredExerciseUseCase import com.project200.domain.usecase.GetUserProfileUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow @@ -24,6 +25,7 @@ class MypageViewModel constructor( private val getExerciseCountInMonthUseCase: GetExerciseCountInMonthUseCase, private val getUserProfileUseCase: GetUserProfileUseCase, + private val getPreferredExerciseUseCase: GetPreferredExerciseUseCase, private val clockProvider: ClockProvider, ) : ViewModel() { private val _profile = MutableLiveData() @@ -37,8 +39,8 @@ class MypageViewModel private val _exerciseDates = MutableLiveData>(emptySet()) val exerciseDates: LiveData> = _exerciseDates - private val _openUrl = MutableLiveData() - val openUrl: LiveData = _openUrl + private val _preferredExercise = MutableLiveData>() + val preferredExercise: LiveData> = _preferredExercise private val _toast = MutableSharedFlow() val toast: SharedFlow = _toast @@ -47,6 +49,7 @@ class MypageViewModel getProfile() val initialMonth = clockProvider.yearMonthNow() onMonthChanged(initialMonth) + getPreferredExercises() } fun onMonthChanged(newMonth: YearMonth) { @@ -120,4 +123,21 @@ class MypageViewModel _selectedMonth.value = newMonth } + + /** + * 선호 운동 조회 + */ + fun getPreferredExercises() { + viewModelScope.launch { + when (val result = getPreferredExerciseUseCase()) { + is BaseResult.Success -> { + _preferredExercise.value = result.data + } + + is BaseResult.Error -> { + _toast.emit(true) + } + } + } + } } diff --git a/feature/profile/src/main/java/com/project200/undabang/profile/mypage/PreferredExerciseRVAdapter.kt b/feature/profile/src/main/java/com/project200/undabang/profile/mypage/PreferredExerciseRVAdapter.kt new file mode 100644 index 00000000..74e76b55 --- /dev/null +++ b/feature/profile/src/main/java/com/project200/undabang/profile/mypage/PreferredExerciseRVAdapter.kt @@ -0,0 +1,53 @@ +package com.project200.undabang.profile.mypage + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.project200.common.utils.PreferredExerciseDayFormatter +import com.project200.domain.model.PreferredExercise +import com.project200.undabang.feature.profile.databinding.ItemPreferredExerciseBinding + +class PreferredExerciseRVAdapter( + private var items: List = emptyList(), + private val formatter: PreferredExerciseDayFormatter, +) : RecyclerView.Adapter() { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): ViewHolder { + val binding = + ItemPreferredExerciseBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false, + ) + return ViewHolder(binding) + } + + override fun onBindViewHolder( + holder: ViewHolder, + position: Int, + ) { + holder.bind(items[position]) + } + + override fun getItemCount(): Int = items.size + + fun setItems(newItems: List) { + items = newItems + notifyDataSetChanged() + } + + inner class ViewHolder(private val binding: ItemPreferredExerciseBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(exercise: PreferredExercise) { + binding.exerciseNameTv.text = exercise.name + binding.skillTv.text = exercise.skillLevel + binding.exerciseDaysTv.text = formatter.formatDaysOfWeek(exercise.daysOfWeek) + Glide.with(binding.exerciseIv.context) + .load(exercise.imageUrl) + .into(binding.exerciseIv) + } + } +} diff --git a/feature/profile/src/main/res/layout/fragment_mypage.xml b/feature/profile/src/main/res/layout/fragment_mypage.xml index 369d129d..1f352380 100644 --- a/feature/profile/src/main/res/layout/fragment_mypage.xml +++ b/feature/profile/src/main/res/layout/fragment_mypage.xml @@ -200,43 +200,42 @@ + android:padding="@dimen/base_horizontal_margin"> + app:layout_constraintTop_toTopOf="parent"/> + + + + + + + + + + \ No newline at end of file