From f5dc4bdc1537ee1cf7222bbf4f5fb1c8a6b8dfe5 Mon Sep 17 00:00:00 2001 From: kim0hoon Date: Sun, 3 Sep 2023 18:16:01 +0900 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=EA=B3=B5=EC=A7=80=EC=82=AC=ED=95=AD?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=EC=B6=94=EA=B0=80=20https://github.com?= =?UTF-8?q?/POLZZAK/POLZZAK-Android/issues/98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/myPage/kid/KidMyPageFragment.kt | 3 +- .../feature/myPage/notice/MyNoticeFragment.kt | 91 +++++++++++++++++++ .../myPage/notice/MyNoticeItemDecoration.kt | 25 +++++ .../myPage/notice/MyNoticeViewModel.kt | 78 ++++++++++++++++ .../myPage/notice/item/MyNoticeEmptyItem.kt | 17 ++++ .../myPage/notice/item/MyNoticeItem.kt | 24 +++++ .../myPage/notice/model/MyNoticeModel.kt | 10 ++ .../myPage/notice/model/MyNoticesModel.kt | 7 ++ .../protector/ProtectorMyPageFragment.kt | 6 +- .../main/res/layout/fragment_my_notice.xml | 30 ++++++ app/src/main/res/layout/item_my_notice.xml | 69 ++++++++++++++ .../main/res/layout/item_my_noticie_empty.xml | 21 +++++ app/src/main/res/navigation/kid_nav_graph.xml | 14 ++- .../res/navigation/protector_nav_graph.xml | 13 ++- app/src/main/res/values/strings.xml | 4 +- 15 files changed, 406 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/MyNoticeFragment.kt create mode 100644 app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/MyNoticeItemDecoration.kt create mode 100644 app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/MyNoticeViewModel.kt create mode 100644 app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/item/MyNoticeEmptyItem.kt create mode 100644 app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/item/MyNoticeItem.kt create mode 100644 app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/model/MyNoticeModel.kt create mode 100644 app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/model/MyNoticesModel.kt create mode 100644 app/src/main/res/layout/fragment_my_notice.xml create mode 100644 app/src/main/res/layout/item_my_notice.xml create mode 100644 app/src/main/res/layout/item_my_noticie_empty.xml diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/myPage/kid/KidMyPageFragment.kt b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/kid/KidMyPageFragment.kt index ea7ea6cf..d75c93ec 100644 --- a/app/src/main/java/com/polzzak_android/presentation/feature/myPage/kid/KidMyPageFragment.kt +++ b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/kid/KidMyPageFragment.kt @@ -1,5 +1,6 @@ package com.polzzak_android.presentation.feature.myPage.kid +import androidx.navigation.fragment.findNavController import com.polzzak_android.R import com.polzzak_android.presentation.common.base.BaseFragment import com.polzzak_android.databinding.FragmentKidMyPageBinding @@ -73,7 +74,7 @@ class KidMyPageFragment : BaseFragment(), ToolbarIconI } fun onClickNotice() { - // todo: 공지사항 클릭 + findNavController().navigate(R.id.action_kidMyPageFragment_to_myNoticeFragment) } fun onClickManageAccount() { diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/MyNoticeFragment.kt b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/MyNoticeFragment.kt new file mode 100644 index 00000000..90a1fff5 --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/MyNoticeFragment.kt @@ -0,0 +1,91 @@ +package com.polzzak_android.presentation.feature.myPage.notice + +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.polzzak_android.R +import com.polzzak_android.databinding.FragmentMyNoticeBinding +import com.polzzak_android.presentation.common.base.BaseFragment +import com.polzzak_android.presentation.common.item.FullLoadingItem +import com.polzzak_android.presentation.common.item.LoadMoreLoadingSpinnerItem +import com.polzzak_android.presentation.common.model.ModelState +import com.polzzak_android.presentation.common.util.BindableItem +import com.polzzak_android.presentation.common.util.BindableItemAdapter +import com.polzzak_android.presentation.common.util.toPx +import com.polzzak_android.presentation.component.toolbar.ToolbarData +import com.polzzak_android.presentation.component.toolbar.ToolbarHelper +import com.polzzak_android.presentation.feature.myPage.notice.item.MyNoticeEmptyItem +import com.polzzak_android.presentation.feature.myPage.notice.item.MyNoticeItem +import com.polzzak_android.presentation.feature.myPage.notice.model.MyNoticesModel + +//TODO 바텀네비 invisible +class MyNoticeFragment : BaseFragment() { + override val layoutResId: Int = R.layout.fragment_my_notice + + private val noticeViewModel by viewModels() + + override fun initView() { + super.initView() + initToolbar() + initRecyclerView() + } + + private fun initToolbar() { + with(binding) { + ToolbarHelper( + data = ToolbarData( + popStack = findNavController(), + titleText = getString(R.string.common_notice) + ), + toolbar = inToolbar + ).set() + } + } + + private fun initRecyclerView() { + val context = binding.root.context ?: return + with(binding.rvNotices) { + layoutManager = LinearLayoutManager(context) + adapter = BindableItemAdapter() + val marginPx = ITEM_MARGIN_DP.toPx(context) + addItemDecoration(MyNoticeItemDecoration(marginPx = marginPx)) + addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + if (!recyclerView.canScrollVertically(1) && noticeViewModel.noticesLiveData.value is ModelState.Success) noticeViewModel.requestMoreNotices() + } + }) + } + } + + override fun initObserver() { + super.initObserver() + noticeViewModel.noticesLiveData.observe(viewLifecycleOwner) { + val adapter = (binding.rvNotices.adapter as? BindableItemAdapter) ?: return@observe + val noticeItem = createNoticeItem(it.data ?: MyNoticesModel()) + val items = mutableListOf>() + when (it) { + is ModelState.Loading -> { + if (it.data?.notices.isNullOrEmpty()) items.add(FullLoadingItem()) + else items.addAll(noticeItem + LoadMoreLoadingSpinnerItem()) + } + + is ModelState.Success -> items.addAll(noticeItem) + is ModelState.Error -> { + //TODO error handling + } + } + adapter.updateItem(item = items) + } + } + + private fun createNoticeItem(model: MyNoticesModel) = model.notices.map { + MyNoticeItem(model = it) + }.ifEmpty { listOf(MyNoticeEmptyItem()) } + + companion object { + private const val ITEM_MARGIN_DP = 16 + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/MyNoticeItemDecoration.kt b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/MyNoticeItemDecoration.kt new file mode 100644 index 00000000..226c974e --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/MyNoticeItemDecoration.kt @@ -0,0 +1,25 @@ +package com.polzzak_android.presentation.feature.myPage.notice + +import android.graphics.Rect +import android.view.View +import androidx.annotation.Px +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration +import com.polzzak_android.presentation.common.util.BindableItemAdapter +import com.polzzak_android.presentation.feature.myPage.notice.item.MyNoticeItem + +class MyNoticeItemDecoration(@Px private val marginPx: Int) : ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + val position = parent.getChildAdapterPosition(view) + val adapter = (parent.adapter as? BindableItemAdapter) ?: return + if (adapter.currentList.getOrNull(position) !is MyNoticeItem) return + outRect.top = marginPx + outRect.bottom = if (position == adapter.currentList.lastIndex) marginPx else 0 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/MyNoticeViewModel.kt b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/MyNoticeViewModel.kt new file mode 100644 index 00000000..3284f46a --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/MyNoticeViewModel.kt @@ -0,0 +1,78 @@ +package com.polzzak_android.presentation.feature.myPage.notice + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.polzzak_android.presentation.common.model.ModelState +import com.polzzak_android.presentation.common.util.toLocalDate +import com.polzzak_android.presentation.feature.myPage.notice.model.MyNoticeModel +import com.polzzak_android.presentation.feature.myPage.notice.model.MyNoticesModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.time.LocalDate + +//TODO api 연동 +class MyNoticeViewModel : ViewModel() { + private val _noticesLiveData = MutableLiveData>() + val noticesLiveData: LiveData> = _noticesLiveData + + private var requestNoticesJob: Job? = null + + init { + initNotices() + } + + private fun initNotices() { + requestNoticesJob?.cancel() + requestNoticesJob = viewModelScope.launch { + _noticesLiveData.value = ModelState.Loading(MyNoticesModel()) + requestNotices() + } + } + + fun requestMoreNotices() { + if (noticesLiveData.value?.data?.hasNextPage == false) return + if (requestNoticesJob?.isCompleted == false) return + requestNoticesJob = viewModelScope.launch { + _noticesLiveData.value = + ModelState.Loading(_noticesLiveData.value?.data ?: MyNoticesModel()) + requestNotices() + } + } + + private suspend fun requestNotices() { + val prevData = noticesLiveData.value?.data ?: MyNoticesModel() + delay(2000) + //TODO api 연동 + //on Success + val nextData = getMockNotices(nextId = prevData.nextId, pageSize = PAGE_SIZE) + val updatedData = nextData.copy(notices = prevData.notices + nextData.notices) + _noticesLiveData.value = ModelState.Success(updatedData) + } + + companion object { + private const val PAGE_SIZE = 10 + } +} + +private fun getMockNotices(nextId: Int?, pageSize: Int): MyNoticesModel { + val startIdx = nextId ?: 0 + val nextIdx = minOf(mockNotices.size, startIdx + pageSize) + val nId = mockNotices.getOrNull(nextIdx)?.id + return MyNoticesModel( + notices = mockNotices.subList(startIdx, nextIdx), + nextId = nId, + hasNextPage = (nId != null) + ) +} + +private val mockNotices = List(27) { + MyNoticeModel( + id = it, + title = "title$it".repeat((it % 12) + 1), + date = "2023-06-04T20:08:23.745393551".toLocalDate() ?: (LocalDate.now()), + content = "content \n\n\n content1 \n\n content \n" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/item/MyNoticeEmptyItem.kt b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/item/MyNoticeEmptyItem.kt new file mode 100644 index 00000000..fd5e3f6e --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/item/MyNoticeEmptyItem.kt @@ -0,0 +1,17 @@ +package com.polzzak_android.presentation.feature.myPage.notice.item + +import com.polzzak_android.R +import com.polzzak_android.databinding.ItemMyNoticieEmptyBinding +import com.polzzak_android.presentation.common.util.BindableItem + +class MyNoticeEmptyItem : BindableItem() { + override val layoutRes: Int = R.layout.item_my_noticie_empty + override fun areItemsTheSame(other: BindableItem<*>): Boolean = other is MyNoticeEmptyItem + + override fun areContentsTheSame(other: BindableItem<*>): Boolean = other is MyNoticeEmptyItem + + override fun bind(binding: ItemMyNoticieEmptyBinding, position: Int) { + //do nothing + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/item/MyNoticeItem.kt b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/item/MyNoticeItem.kt new file mode 100644 index 00000000..776645fe --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/item/MyNoticeItem.kt @@ -0,0 +1,24 @@ +package com.polzzak_android.presentation.feature.myPage.notice.item + +import com.polzzak_android.R +import com.polzzak_android.databinding.ItemMyNoticeBinding +import com.polzzak_android.presentation.common.util.BindableItem +import com.polzzak_android.presentation.common.util.toDateString +import com.polzzak_android.presentation.feature.myPage.notice.model.MyNoticeModel + +class MyNoticeItem(private val model: MyNoticeModel) : BindableItem() { + override val layoutRes: Int = R.layout.item_my_notice + override fun areItemsTheSame(other: BindableItem<*>): Boolean = + other is MyNoticeItem && this.model.id == other.model.id + + override fun areContentsTheSame(other: BindableItem<*>): Boolean = + other is MyNoticeItem && this.model == other.model + + override fun bind(binding: ItemMyNoticeBinding, position: Int) { + with(binding) { + tvTitle.text = model.title + tvDate.text = model.date.toDateString() + tvContent.text = model.content.replace(Regex("(\\r\\n|\\r|\\n)+"), " ") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/model/MyNoticeModel.kt b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/model/MyNoticeModel.kt new file mode 100644 index 00000000..d3fc316c --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/model/MyNoticeModel.kt @@ -0,0 +1,10 @@ +package com.polzzak_android.presentation.feature.myPage.notice.model + +import java.time.LocalDate + +data class MyNoticeModel( + val id: Int, + val title: String, + val date: LocalDate, + val content: String +) diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/model/MyNoticesModel.kt b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/model/MyNoticesModel.kt new file mode 100644 index 00000000..e3e176fc --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/notice/model/MyNoticesModel.kt @@ -0,0 +1,7 @@ +package com.polzzak_android.presentation.feature.myPage.notice.model + +data class MyNoticesModel( + val notices: List = emptyList(), + val nextId: Int? = null, + val hasNextPage: Boolean = true +) \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/myPage/protector/ProtectorMyPageFragment.kt b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/protector/ProtectorMyPageFragment.kt index 89bbdf1e..ed2e7105 100644 --- a/app/src/main/java/com/polzzak_android/presentation/feature/myPage/protector/ProtectorMyPageFragment.kt +++ b/app/src/main/java/com/polzzak_android/presentation/feature/myPage/protector/ProtectorMyPageFragment.kt @@ -1,5 +1,6 @@ package com.polzzak_android.presentation.feature.myPage.protector +import androidx.navigation.fragment.findNavController import com.polzzak_android.R import com.polzzak_android.databinding.FragmentProtectorMyPageBinding import com.polzzak_android.presentation.common.base.BaseFragment @@ -32,7 +33,7 @@ class ProtectorMyPageFragment : BaseFragment(), setUpPointView() } - private fun setUpPointView(){ + private fun setUpPointView() { with(binding.pointRanking) { text = "폴짝 랭킹" icon.setImageResource(R.drawable.ic_point_rank) @@ -74,7 +75,8 @@ class ProtectorMyPageFragment : BaseFragment(), } fun onClickNotice() { - // todo: 공지사항 클릭 + findNavController().navigate(R.id.action_protectorMyPageFragment_to_myNoticeFragment) + } fun onClickManageAccount() { diff --git a/app/src/main/res/layout/fragment_my_notice.xml b/app/src/main/res/layout/fragment_my_notice.xml new file mode 100644 index 00000000..294326bd --- /dev/null +++ b/app/src/main/res/layout/fragment_my_notice.xml @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_my_notice.xml b/app/src/main/res/layout/item_my_notice.xml new file mode 100644 index 00000000..e5b3e1ee --- /dev/null +++ b/app/src/main/res/layout/item_my_notice.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_my_noticie_empty.xml b/app/src/main/res/layout/item_my_noticie_empty.xml new file mode 100644 index 00000000..383fa36e --- /dev/null +++ b/app/src/main/res/layout/item_my_noticie_empty.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/kid_nav_graph.xml b/app/src/main/res/navigation/kid_nav_graph.xml index 7948a3e3..35e5c1c5 100644 --- a/app/src/main/res/navigation/kid_nav_graph.xml +++ b/app/src/main/res/navigation/kid_nav_graph.xml @@ -59,6 +59,18 @@ + android:label="KidMyPageFragment"> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/protector_nav_graph.xml b/app/src/main/res/navigation/protector_nav_graph.xml index c001a9de..f8c8e431 100644 --- a/app/src/main/res/navigation/protector_nav_graph.xml +++ b/app/src/main/res/navigation/protector_nav_graph.xml @@ -75,5 +75,16 @@ android:id="@+id/protectorMyPageFragment" android:name="com.polzzak_android.presentation.feature.myPage.protector.ProtectorMyPageFragment" android:label="ProtectorMyPageFragment" - tools:layout="@layout/fragment_protector_my_page"/> + tools:layout="@layout/fragment_protector_my_page"> + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 60dc29de..6e4b3e11 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -26,6 +26,7 @@ 수락 거절 알림 + 공지사항 참 잘 했어요 도장 쾅! @@ -162,5 +163,6 @@ 선물 전달 확인 알림 선물 전달이 완료되었는지 확인하는 알림을 받을래요 - + + 업데이트된 소식이 없어요 \ No newline at end of file