오디션 배역 상세

- 지원 리스트 UI 작성
- 투표 API 적용
This commit is contained in:
klaus 2025-01-03 04:54:46 +09:00
parent 968428cfe0
commit c6ef5970a5
8 changed files with 211 additions and 31 deletions

View File

@ -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<ApiResponse<GetAuditionApplicantListResponse>>
@POST("/audition/vote")
fun voteApplicant(
@Body request: VoteAuditionApplicantRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
}

View File

@ -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
)
}

View File

@ -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<GetAuditionRoleApplicantItem>,
private val onClickVote: (Int) -> Unit,
private val onClickPlayOrPause: (Int) -> Unit
) : RecyclerView.Adapter<AuditionApplicantListAdapter.ViewHolder>() {
) : ListAdapter<GetAuditionRoleApplicantItem, AuditionApplicantListAdapter.ViewHolder>(DiffCallback()) {
private var currentPlayingIndex: Int = -1
inner class DiffCallback(
private val oldList: List<GetAuditionRoleApplicantItem>,
private val newList: List<GetAuditionRoleApplicantItem>
) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
class DiffCallback : DiffUtil.ItemCallback<GetAuditionRoleApplicantItem>() {
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<GetAuditionRoleApplicantItem>) {
itemList = newData
notifyDataSetChanged()
holder.bind(getItem(position), position)
}
fun updatePlayingIndex(newIndex: Int) {

View File

@ -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<ActivityAuditionRoleDetailBindin
}
}
binding.tvSortNewest.setOnClickListener {
viewModel.setSortType(AuditionRoleDetailViewModel.AuditionApplicantSortType.NEWEST)
}
binding.tvSortLikes.setOnClickListener {
viewModel.setSortType(AuditionRoleDetailViewModel.AuditionApplicantSortType.LIKES)
}
adapter = AuditionApplicantListAdapter(
onClickPlayOrPause = {},
onClickVote = { viewModel.voteApplicant(it) }
)
val recyclerView = binding.rvApplicant
recyclerView.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
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<ActivityAuditionRoleDetailBindin
}
viewModel.applicantListLiveData.observe(this) {
adapter.submitList(it)
}
viewModel.sortTypeLiveData.observe(this) {
binding.tvSortNewest.setTextColor(
ContextCompat.getColor(applicationContext, R.color.color_bbbbbb)
)
binding.tvSortLikes.setTextColor(
ContextCompat.getColor(applicationContext, R.color.color_bbbbbb)
)
if (it == AuditionRoleDetailViewModel.AuditionApplicantSortType.NEWEST) {
binding.tvSortNewest.setTextColor(
ContextCompat.getColor(applicationContext, R.color.color_3bb9f1)
)
} else if (it == AuditionRoleDetailViewModel.AuditionApplicantSortType.LIKES) {
binding.tvSortLikes.setTextColor(
ContextCompat.getColor(applicationContext, R.color.color_3bb9f1)
)
}
}
}
}

View File

@ -9,8 +9,10 @@ import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audition.AuditionRepository
import kr.co.vividnext.sodalive.audition.applicant.GetAuditionRoleApplicantItem
import kr.co.vividnext.sodalive.audition.vote.VoteAuditionApplicantRequest
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import java.util.TimeZone
class AuditionRoleDetailViewModel(private val repository: AuditionRepository) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
@ -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<GetAuditionRoleApplicantItem>) {
val updatedList = _applicantListLiveData.value?.toMutableList() ?: mutableListOf()
updatedList.addAll(itemList)
_applicantListLiveData.value = updatedList
}
enum class AuditionApplicantSortType {
@SerializedName("NEWEST")
NEWEST,

View File

@ -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"
)

View File

@ -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" />
<TextView
android:id="@+id/tv_sort_newest"

View File

@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="13.3dp"
android:paddingVertical="18.7dp"
android:paddingTop="18.7dp"
tools:background="@color/black">
<ImageView
@ -67,4 +67,13 @@
android:textSize="12sp"
tools:text="777" />
</LinearLayout>
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="24dp"
android:background="@color/color_555555"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv_profile" />
</androidx.constraintlayout.widget.ConstraintLayout>