diff --git a/data/src/main/java/com/d83t/bpm/data/model/response/UserScheduleResponse.kt b/data/src/main/java/com/d83t/bpm/data/model/response/UserScheduleResponse.kt new file mode 100644 index 0000000..f20f98d --- /dev/null +++ b/data/src/main/java/com/d83t/bpm/data/model/response/UserScheduleResponse.kt @@ -0,0 +1,30 @@ +package com.d83t.bpm.data.model.response + +import com.d83t.bpm.data.base.BaseResponse +import com.d83t.bpm.data.mapper.DataMapper +import com.d83t.bpm.domain.model.UserSchedule +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +@Parcelize +data class UserScheduleResponse( + @SerializedName("studioName") + val studioName: String?, + @SerializedName("date") + val date: String?, + @SerializedName("time") + val time: String?, + @SerializedName("memo") + val memo: String?, +) : BaseResponse { + companion object : DataMapper { + override fun UserScheduleResponse.toDataModel(): UserSchedule { + return UserSchedule( + studioName = studioName, + date = date, + time = time, + memo = memo + ) + } + } +} \ No newline at end of file diff --git a/data/src/main/java/com/d83t/bpm/data/network/MainApi.kt b/data/src/main/java/com/d83t/bpm/data/network/MainApi.kt index 6ad3521..1853385 100644 --- a/data/src/main/java/com/d83t/bpm/data/network/MainApi.kt +++ b/data/src/main/java/com/d83t/bpm/data/network/MainApi.kt @@ -7,18 +7,10 @@ import com.d83t.bpm.data.model.response.ScheduleResponse import com.d83t.bpm.data.model.response.SignUpResponse import com.d83t.bpm.data.model.response.StudioListResponse import com.d83t.bpm.data.model.response.StudioResponse -import com.d83t.bpm.domain.model.StudioList +import com.d83t.bpm.data.model.response.UserScheduleResponse import okhttp3.MultipartBody import retrofit2.Response import retrofit2.http.* -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.Headers -import retrofit2.http.Multipart -import retrofit2.http.POST -import retrofit2.http.Part -import retrofit2.http.Path -import retrofit2.http.Query interface MainApi { @Headers("shouldBeAuthorized: false") @@ -77,6 +69,9 @@ interface MainApi { @Query("offset") offset: Int, ): Response + @GET("api/users/schedule") + suspend fun getUserSchedule(): Response + @GET("api/studio") suspend fun searchStudio( @Query("q") query: String diff --git a/data/src/main/java/com/d83t/bpm/data/repositoryImpl/MainRepositoryImpl.kt b/data/src/main/java/com/d83t/bpm/data/repositoryImpl/MainRepositoryImpl.kt index e5afd64..50c2fa9 100644 --- a/data/src/main/java/com/d83t/bpm/data/repositoryImpl/MainRepositoryImpl.kt +++ b/data/src/main/java/com/d83t/bpm/data/repositoryImpl/MainRepositoryImpl.kt @@ -1,12 +1,14 @@ package com.d83t.bpm.data.repositoryImpl import com.d83t.bpm.data.model.response.StudioListResponse.Companion.toDataModel +import com.d83t.bpm.data.model.response.UserScheduleResponse.Companion.toDataModel import com.d83t.bpm.data.network.BPMResponse import com.d83t.bpm.data.network.BPMResponseHandler import com.d83t.bpm.data.network.ErrorResponse.Companion.toDataModel import com.d83t.bpm.data.network.MainApi import com.d83t.bpm.domain.model.ResponseState import com.d83t.bpm.domain.model.StudioList +import com.d83t.bpm.domain.model.UserSchedule import com.d83t.bpm.domain.repository.MainRepository import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -30,4 +32,17 @@ class MainRepositoryImpl @Inject constructor( }.collect() } } + + override suspend fun getUserSchedule(): Flow> { + return flow { + BPMResponseHandler().handle { + mainApi.getUserSchedule() + }.onEach { result -> + when (result) { + is BPMResponse.Success -> emit(ResponseState.Success(result.data.toDataModel())) + is BPMResponse.Error -> emit(ResponseState.Error(result.error.toDataModel())) + } + }.collect() + } + } } \ No newline at end of file diff --git a/domain/build.gradle b/domain/build.gradle index b5373d2..3656eb2 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -73,4 +73,7 @@ dependencies { //timber log implementation 'com.jakewharton.timber:timber:5.0.1' + // joda datetime + implementation "joda-time:joda-time:2.10.13" + } \ No newline at end of file diff --git a/domain/src/main/java/com/d83t/bpm/domain/model/Studio.kt b/domain/src/main/java/com/d83t/bpm/domain/model/Studio.kt index ad78024..d29ccc9 100644 --- a/domain/src/main/java/com/d83t/bpm/domain/model/Studio.kt +++ b/domain/src/main/java/com/d83t/bpm/domain/model/Studio.kt @@ -1,7 +1,7 @@ package com.d83t.bpm.domain.model import com.d83t.bpm.domain.base.BaseModel -import com.google.gson.annotations.SerializedName +import kotlin.math.round import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @@ -27,9 +27,14 @@ data class Studio( val updatedAt: String? ) : BaseModel { + // TODO : 죄송합니다.. + @IgnoredOnParcel val tagList = listOf( "친절해요", "소통이 빨라요", "깨끗해요" ) + @IgnoredOnParcel + val ratingText: String = "${round((rating?.times(10) ?: 0) as Double) / 10}" + } \ No newline at end of file diff --git a/domain/src/main/java/com/d83t/bpm/domain/model/UserSchedule.kt b/domain/src/main/java/com/d83t/bpm/domain/model/UserSchedule.kt new file mode 100644 index 0000000..e286e98 --- /dev/null +++ b/domain/src/main/java/com/d83t/bpm/domain/model/UserSchedule.kt @@ -0,0 +1,12 @@ +package com.d83t.bpm.domain.model + +import com.d83t.bpm.domain.base.BaseModel +import kotlinx.parcelize.Parcelize + +@Parcelize +data class UserSchedule( + val studioName: String? = "", + val date: String? = "", + val time: String? = "", + val memo: String? = "끝까지 화이팅!", +) : BaseModel \ No newline at end of file diff --git a/domain/src/main/java/com/d83t/bpm/domain/repository/MainRepository.kt b/domain/src/main/java/com/d83t/bpm/domain/repository/MainRepository.kt index 2c7b6e1..ba8bc49 100644 --- a/domain/src/main/java/com/d83t/bpm/domain/repository/MainRepository.kt +++ b/domain/src/main/java/com/d83t/bpm/domain/repository/MainRepository.kt @@ -2,10 +2,14 @@ package com.d83t.bpm.domain.repository import com.d83t.bpm.domain.model.ResponseState import com.d83t.bpm.domain.model.StudioList +import com.d83t.bpm.domain.model.UserSchedule import kotlinx.coroutines.flow.Flow interface MainRepository { + // TODO : Move to HomeRepository suspend fun getStudioList(limit: Int, offset: Int): Flow> + suspend fun getUserSchedule(): Flow> + } \ No newline at end of file diff --git a/domain/src/main/java/com/d83t/bpm/domain/usecase/main/GetUserScheduleUseCase.kt b/domain/src/main/java/com/d83t/bpm/domain/usecase/main/GetUserScheduleUseCase.kt new file mode 100644 index 0000000..41738c7 --- /dev/null +++ b/domain/src/main/java/com/d83t/bpm/domain/usecase/main/GetUserScheduleUseCase.kt @@ -0,0 +1,17 @@ +package com.d83t.bpm.domain.usecase.main + +import com.d83t.bpm.domain.model.ResponseState +import com.d83t.bpm.domain.model.UserSchedule +import com.d83t.bpm.domain.repository.MainRepository +import dagger.Reusable +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +@Reusable +class GetUserScheduleUseCase @Inject constructor( + private val mainRepository: MainRepository +) { + suspend operator fun invoke(): Flow> { + return mainRepository.getUserSchedule() + } +} \ No newline at end of file diff --git a/presentation/build.gradle b/presentation/build.gradle index 44690f7..c6a6585 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -172,6 +172,9 @@ dependencies { // 카카오 로그인 implementation "com.kakao.sdk:v2-user:2.12.1" + // joda datetime + implementation "joda-time:joda-time:2.10.13" + // Flipper debugImplementation 'com.facebook.flipper:flipper:0.182.0' debugImplementation 'com.facebook.soloader:soloader:0.10.4' diff --git a/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/HomeFragment.kt b/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/HomeFragment.kt index 066905b..fe75935 100644 --- a/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/HomeFragment.kt +++ b/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/HomeFragment.kt @@ -39,12 +39,22 @@ class HomeFragment : BaseFragment(FragmentHomeBinding::infl viewModel.state.collect { state -> when (state) { HomeState.Init -> { - // TODO : 예약현황 가져오기 + viewModel.getUserSchedule() } - HomeState.StudioList -> Unit + HomeState.UserSchedule -> Unit HomeState.Error -> { // TODO : Error Handling - requireContext().showToast("예약 정보를 가져오는 중 에러가 발생했습니다.") +// requireContext().showToast("예약 정보를 가져오는 중 에러가 발생했습니다.") + } + } + } + } + + repeatCallDefaultOnStarted { + viewModel.event.collect { event -> + when (event) { + HomeViewEvent.ClickSearch -> { + requireContext().showToast("검색페이지 이동") } } } @@ -54,7 +64,12 @@ class HomeFragment : BaseFragment(FragmentHomeBinding::infl private fun setUpPager() { binding.pager.adapter = HomePagerAdapter(requireActivity(), fragmentList) - TabLayoutMediator(binding.tab, binding.pager, false, true) { tab: TabLayout.Tab?, position: Int -> + TabLayoutMediator( + binding.tab, + binding.pager, + false, + true + ) { tab: TabLayout.Tab?, position: Int -> val resId: Int = when (position) { 0 -> R.string.tab_hot 1 -> R.string.tab_review diff --git a/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/HomeState.kt b/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/HomeState.kt index 91251b4..f6196c7 100644 --- a/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/HomeState.kt +++ b/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/HomeState.kt @@ -2,7 +2,7 @@ package com.d83t.bpm.presentation.ui.main.home sealed interface HomeState { object Init : HomeState - object StudioList : HomeState + object UserSchedule : HomeState object Error : HomeState } \ No newline at end of file diff --git a/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/HomeViewEvent.kt b/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/HomeViewEvent.kt index cbb436c..88a5f96 100644 --- a/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/HomeViewEvent.kt +++ b/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/HomeViewEvent.kt @@ -1,3 +1,5 @@ package com.d83t.bpm.presentation.ui.main.home -sealed interface HomeViewEvent \ No newline at end of file +sealed interface HomeViewEvent { + object ClickSearch : HomeViewEvent +} \ No newline at end of file diff --git a/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/HomeViewModel.kt b/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/HomeViewModel.kt index 720c626..4e6ca41 100644 --- a/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/HomeViewModel.kt +++ b/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/HomeViewModel.kt @@ -2,8 +2,8 @@ package com.d83t.bpm.presentation.ui.main.home import androidx.lifecycle.viewModelScope import com.d83t.bpm.domain.model.ResponseState -import com.d83t.bpm.domain.model.Studio -import com.d83t.bpm.domain.usecase.main.GetStudioListUseCase +import com.d83t.bpm.domain.model.UserSchedule +import com.d83t.bpm.domain.usecase.main.GetUserScheduleUseCase import com.d83t.bpm.presentation.base.BaseViewModel import com.d83t.bpm.presentation.di.IoDispatcher import com.d83t.bpm.presentation.di.MainDispatcher @@ -21,7 +21,7 @@ import kotlinx.coroutines.launch @HiltViewModel class HomeViewModel @Inject constructor( - private val getStudioListUseCase: GetStudioListUseCase, + private val getUserScheduleUseCase: GetUserScheduleUseCase, @MainDispatcher private val mainDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher ) : BaseViewModel() { @@ -34,11 +34,36 @@ class HomeViewModel @Inject constructor( val event: SharedFlow get() = _event + private val _userScheduleInfo = MutableStateFlow(UserSchedule()) + val userScheduleInfo: StateFlow + get() = _userScheduleInfo + private val exceptionHandler: CoroutineExceptionHandler by lazy { CoroutineExceptionHandler { coroutineContext, throwable -> } } + fun getUserSchedule() { + viewModelScope.launch(ioDispatcher + exceptionHandler) { + getUserScheduleUseCase().onEach { state -> + when (state) { + is ResponseState.Success -> { + _userScheduleInfo.emit(state.data) + _state.emit(HomeState.UserSchedule) + } + is ResponseState.Error -> { + _state.emit(HomeState.Error) + } + } + }.launchIn(viewModelScope) + } + } + + fun clickSearch(){ + viewModelScope.launch { + _event.emit(HomeViewEvent.ClickSearch) + } + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/recommend/list/HomeRecommendViewHolder.kt b/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/recommend/list/HomeRecommendViewHolder.kt index 5c0ccdb..7be978b 100644 --- a/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/recommend/list/HomeRecommendViewHolder.kt +++ b/presentation/src/main/java/com/d83t/bpm/presentation/ui/main/home/recommend/list/HomeRecommendViewHolder.kt @@ -15,7 +15,7 @@ class HomeRecommendViewHolder( this.item = item list.adapter = HomeRecommendImageAdapter() -// list.addItemDecoration(HomeRecommendImageItemDecoration()) + list.addItemDecoration(HomeRecommendImageItemDecoration()) root.setOnClickListener { listener.invoke(item.id) diff --git a/presentation/src/main/java/com/d83t/bpm/presentation/util/BindingAdapters.kt b/presentation/src/main/java/com/d83t/bpm/presentation/util/BindingAdapters.kt index 7c666e0..b02eb9f 100644 --- a/presentation/src/main/java/com/d83t/bpm/presentation/util/BindingAdapters.kt +++ b/presentation/src/main/java/com/d83t/bpm/presentation/util/BindingAdapters.kt @@ -1,6 +1,8 @@ package com.d83t.bpm.presentation.util +import android.view.View import android.widget.ImageView +import androidx.appcompat.widget.AppCompatTextView import androidx.databinding.BindingAdapter import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView @@ -9,6 +11,28 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.d83t.bpm.presentation.R import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup +import org.joda.time.DateTime +import org.joda.time.Days +import org.joda.time.format.DateTimeFormat + +@BindingAdapter("bind:visibility") +fun View.bindVisibleOrGone(isVisibleOrGone: Boolean?) { + visibility = if (isVisibleOrGone == true) { + View.VISIBLE + } else { + View.GONE + } +} + +@BindingAdapter("bind:visibility") +fun View.bindVisibleOrGone(text: String?) { + visibility = if (!text.isNullOrEmpty()) { + View.VISIBLE + } else { + View.GONE + } +} + @BindingAdapter("bind:list_item") fun RecyclerView.bindListItems(list: List?) { @@ -40,3 +64,23 @@ fun ImageView.bindImageUrl(url: String?) { .transition(DrawableTransitionOptions.withCrossFade()) .into(this) } + +@BindingAdapter("bind:home_user_schedule_date", "bind:home_user_schedule_time") +fun AppCompatTextView.bindHomeUserSchedule(dateString: String?, timeString: String?) { + if (dateString.isNullOrEmpty() || timeString.isNullOrEmpty() || dateString == "-" || timeString == "-") return + else { + val date = DateTime.parse(dateString, DateTimeFormat.forPattern("yyyy-MM-dd")) + val time = DateTime.parse(timeString, DateTimeFormat.forPattern("HH:mm:ss")) + + text = "${date.toString("yyyy년 MM월 dd일 ")} ${time.getKoreanHour()}" + } +} + +@BindingAdapter("bind:home_user_schedule_dday") +fun AppCompatTextView.bindHomeUserScheduleDday(dateString: String?) { + if (dateString.isNullOrEmpty() || dateString == "-") return + else { + val date = DateTime.parse(dateString, DateTimeFormat.forPattern("yyyy-MM-dd")) + text = "D${Days.daysBetween(date, DateTime.now()).days}" + } +} diff --git a/presentation/src/main/java/com/d83t/bpm/presentation/util/ViewUtils.kt b/presentation/src/main/java/com/d83t/bpm/presentation/util/ViewUtils.kt index 696a8b2..e92a29e 100644 --- a/presentation/src/main/java/com/d83t/bpm/presentation/util/ViewUtils.kt +++ b/presentation/src/main/java/com/d83t/bpm/presentation/util/ViewUtils.kt @@ -2,6 +2,7 @@ package com.d83t.bpm.presentation.util import android.content.res.Resources import android.util.TypedValue +import org.joda.time.DateTime val Int.dp: Int get() { @@ -9,3 +10,13 @@ val Int.dp: Int return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), metrics) .toInt() } + +fun DateTime.getKoreanHour(): String { + val nowHour = this.hourOfDay + return if (nowHour < 12) "오전 ${nowHour}시" + else "오후 ${nowHour - 12}시" +} + +fun DateTime.getUserScheduleDate(): String { + return toString("yyyy.MM.dd") + " ${getKoreanHour()}" +} diff --git a/presentation/src/main/res/drawable/ic_nav_add.xml b/presentation/src/main/res/drawable/ic_nav_add.xml index a838b60..aaf362b 100644 --- a/presentation/src/main/res/drawable/ic_nav_add.xml +++ b/presentation/src/main/res/drawable/ic_nav_add.xml @@ -6,10 +6,10 @@ + android:fillColor="#00000000" + android:strokeColor="#9EA0A4"/> diff --git a/presentation/src/main/res/layout/fragment_home.xml b/presentation/src/main/res/layout/fragment_home.xml index 174bfe2..b8782bd 100644 --- a/presentation/src/main/res/layout/fragment_home.xml +++ b/presentation/src/main/res/layout/fragment_home.xml @@ -1,6 +1,8 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:bind="http://schemas.android.com/bind" + xmlns:tools="http://schemas.android.com/tools"> @@ -44,7 +46,8 @@ layout="@layout/layout_home_search" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + app:onSearchClick="@{vm.clickSearch}" /> + app:layout_constraintTop_toTopOf="parent" + bind:home_user_schedule_dday="@{vm.userScheduleInfo.date}" + tools:text="D-3" /> + app:layout_constraintTop_toBottomOf="@id/plan_count" + tools:text="바디프렌즈 스튜디오" /> + app:layout_constraintTop_toBottomOf="@id/plan_studio" + bind:home_user_schedule_date="@{vm.userScheduleInfo.date}" + bind:home_user_schedule_time="@{vm.userScheduleInfo.time}" + tools:text="2022.01.23 오후 5시" /> + app:constraint_referenced_ids="plan_date, plan_count, plan_studio, plan_guide" + bind:visibility="@{vm.userScheduleInfo.studioName}" /> + app:constraint_referenced_ids="book_register, book_plan, book_guide, book_arrow" + bind:visibility="@{vm.userScheduleInfo.studioName.empty}" /> @@ -253,6 +260,7 @@ style="@style/Widget.Design.TabLayout" android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_marginEnd="100dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -261,11 +269,22 @@ app:tabMode="fixed" app:tabRippleColor="@android:color/transparent" /> + + + +