From c6ef5970a583b35d6ed60d0910f3e4f0fdb0c2da Mon Sep 17 00:00:00 2001 From: klaus Date: Fri, 3 Jan 2025 04:54:46 +0900 Subject: [PATCH] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EB=B0=B0?= =?UTF-8?q?=EC=97=AD=20=EC=83=81=EC=84=B8=20-=20=EC=A7=80=EC=9B=90=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20UI=20=EC=9E=91=EC=84=B1=20-=20?= =?UTF-8?q?=ED=88=AC=ED=91=9C=20API=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/audition/AuditionApi.kt | 10 +++ .../sodalive/audition/AuditionRepository.kt | 9 ++ .../applicant/AuditionApplicantListAdapter.kt | 39 +++----- .../role/AuditionRoleDetailActivity.kt | 89 +++++++++++++++++++ .../role/AuditionRoleDetailViewModel.kt | 70 ++++++++++++++- .../vote/VoteAuditionApplicantRequest.kt | 11 +++ .../layout/activity_audition_role_detail.xml | 3 +- .../res/layout/item_audition_applicant.xml | 11 ++- 8 files changed, 211 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audition/vote/VoteAuditionApplicantRequest.kt diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionApi.kt index 66ca14c..7a0601e 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionApi.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionApi.kt @@ -5,9 +5,13 @@ import kr.co.vividnext.sodalive.audition.applicant.GetAuditionApplicantListRespo import kr.co.vividnext.sodalive.audition.detail.GetAuditionDetailResponse import kr.co.vividnext.sodalive.audition.role.AuditionRoleDetailViewModel import kr.co.vividnext.sodalive.audition.role.GetAuditionRoleDetailResponse +import kr.co.vividnext.sodalive.audition.vote.VoteAuditionApplicantRequest import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.live.room.menu.UpdateLiveMenuRequest +import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header +import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.Query @@ -39,4 +43,10 @@ interface AuditionApi { @Query("size") size: Int, @Header("Authorization") authHeader: String ): Single> + + @POST("/audition/vote") + fun voteApplicant( + @Body request: VoteAuditionApplicantRequest, + @Header("Authorization") authHeader: String + ): Single> } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionRepository.kt index 3934cea..3422ec9 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionRepository.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionRepository.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.audition import kr.co.vividnext.sodalive.audition.role.AuditionRoleDetailViewModel +import kr.co.vividnext.sodalive.audition.vote.VoteAuditionApplicantRequest class AuditionRepository( private val api: AuditionApi @@ -44,4 +45,12 @@ class AuditionRepository( size = size, authHeader = token ) + + fun voteApplicant( + request: VoteAuditionApplicantRequest, + token: String + ) = api.voteApplicant( + request = request, + authHeader = token + ) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantListAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantListAdapter.kt index 263d5bc..8a98440 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantListAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/applicant/AuditionApplicantListAdapter.kt @@ -1,9 +1,9 @@ package kr.co.vividnext.sodalive.audition.applicant -import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import coil.load import coil.transform.CircleCropTransformation @@ -11,29 +11,26 @@ import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.databinding.ItemAuditionApplicantBinding class AuditionApplicantListAdapter( - private var itemList: List, private val onClickVote: (Int) -> Unit, private val onClickPlayOrPause: (Int) -> Unit -) : RecyclerView.Adapter() { +) : ListAdapter(DiffCallback()) { private var currentPlayingIndex: Int = -1 - inner class DiffCallback( - private val oldList: List, - private val newList: List - ) : DiffUtil.Callback() { - override fun getOldListSize() = oldList.size - override fun getNewListSize() = newList.size - + class DiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItemPosition: Int, - newItemPosition: Int - ) = oldList[oldItemPosition].applicantId == newList[newItemPosition].applicantId + oldItem: GetAuditionRoleApplicantItem, + newItem: GetAuditionRoleApplicantItem + ): Boolean { + return oldItem.applicantId == newItem.applicantId + } override fun areContentsTheSame( - oldItemPosition: Int, - newItemPosition: Int - ) = oldList[oldItemPosition] == newList[newItemPosition] + oldItem: GetAuditionRoleApplicantItem, + newItem: GetAuditionRoleApplicantItem + ): Boolean { + return oldItem == newItem + } } inner class ViewHolder( @@ -68,16 +65,8 @@ class AuditionApplicantListAdapter( ) ) - override fun getItemCount() = itemList.size - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(itemList[position], position) - } - - @SuppressLint("NotifyDataSetChanged") - fun updateData(newData: List) { - itemList = newData - notifyDataSetChanged() + holder.bind(getItem(position), position) } fun updatePlayingIndex(newIndex: Int) { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/role/AuditionRoleDetailActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/role/AuditionRoleDetailActivity.kt index 09e8c88..c346791 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audition/role/AuditionRoleDetailActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/role/AuditionRoleDetailActivity.kt @@ -1,10 +1,14 @@ package kr.co.vividnext.sodalive.audition.role import android.content.Intent +import android.graphics.Rect import android.net.Uri import android.os.Bundle import android.view.View import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import coil.load import coil.transform.RoundedCornersTransformation import kr.co.vividnext.sodalive.R @@ -72,7 +76,72 @@ class AuditionRoleDetailActivity : BaseActivity { + outRect.top = 0 + outRect.bottom = 2.7f.dpToPx().toInt() + } + + adapter.itemCount - 1 -> { + outRect.top = 2.7f.dpToPx().toInt() + outRect.bottom = 0 + } + + else -> { + outRect.top = 2.7f.dpToPx().toInt() + outRect.bottom = 2.7f.dpToPx().toInt() + } + } + } + }) + + recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!! + .findLastCompletelyVisibleItemPosition() + val itemTotalCount = recyclerView.adapter!!.itemCount - 1 + + // 스크롤이 끝에 도달했는지 확인 + if (!recyclerView.canScrollVertically(1) && + lastVisibleItemPosition == itemTotalCount + ) { + viewModel.getAuditionApplicantList() + } + } + }) + + recyclerView.adapter = adapter } private fun bindData() { @@ -127,6 +196,26 @@ class AuditionRoleDetailActivity : BaseActivity() @@ -91,8 +93,13 @@ class AuditionRoleDetailViewModel(private val repository: AuditionRepository) : val data = applicantListResponse.data _totalCountLiveData.value = data.totalCount - _applicantListLiveData.value = data.items - page += 1 + addApplicantList(data.items) + + if (data.items.isEmpty()) { + isLast = true + } else { + page += 1 + } } else { if (applicantListResponse.message != null) { _toastLiveData.value = applicantListResponse.message @@ -138,8 +145,13 @@ class AuditionRoleDetailViewModel(private val repository: AuditionRepository) : val data = it.data _totalCountLiveData.value = data.totalCount - _applicantListLiveData.value = data.items - page += 1 + addApplicantList(data.items) + + if (data.items.isEmpty()) { + isLast = true + } else { + page += 1 + } } else { if (it.message != null) { _toastLiveData.value = it.message @@ -170,6 +182,56 @@ class AuditionRoleDetailViewModel(private val repository: AuditionRepository) : } } + fun voteApplicant(position: Int) { + val updatedList = _applicantListLiveData.value?.toMutableList() + val applicantId = updatedList?.get(position)?.applicantId + + if (applicantId != null) { + _isLoading.value = false + val request = VoteAuditionApplicantRequest( + applicantId = applicantId, + timezone = TimeZone.getDefault().id + ) + + compositeDisposable.add( + repository.voteApplicant( + request = request, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success) { + updatedList[position].voteCount += 1 + _applicantListLiveData.value = updatedList!! + } else { + if (it.message != null) { + _toastLiveData.value = it.message + } else { + _toastLiveData.value = + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + } + } + + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + } + ) + ) + } + } + + private fun addApplicantList(itemList: List) { + val updatedList = _applicantListLiveData.value?.toMutableList() ?: mutableListOf() + updatedList.addAll(itemList) + _applicantListLiveData.value = updatedList + } + enum class AuditionApplicantSortType { @SerializedName("NEWEST") NEWEST, diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/vote/VoteAuditionApplicantRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/vote/VoteAuditionApplicantRequest.kt new file mode 100644 index 0000000..2ce974d --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/vote/VoteAuditionApplicantRequest.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.audition.vote + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +data class VoteAuditionApplicantRequest( + @SerializedName("applicantId") val applicantId: Long, + @SerializedName("timezone") val timezone: String, + @SerializedName("container") val container: String = "aos" +) diff --git a/app/src/main/res/layout/activity_audition_role_detail.xml b/app/src/main/res/layout/activity_audition_role_detail.xml index c621903..d7e538c 100644 --- a/app/src/main/res/layout/activity_audition_role_detail.xml +++ b/app/src/main/res/layout/activity_audition_role_detail.xml @@ -166,7 +166,8 @@ android:textColor="@color/color_bbbbbb" android:textSize="10.7sp" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@+id/ll_applicant_count" /> + app:layout_constraintTop_toTopOf="@+id/ll_applicant_count" + tools:ignore="SmallSp" /> + +