오디션 배역 상세

- 지원 리스트 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.detail.GetAuditionDetailResponse
import kr.co.vividnext.sodalive.audition.role.AuditionRoleDetailViewModel import kr.co.vividnext.sodalive.audition.role.AuditionRoleDetailViewModel
import kr.co.vividnext.sodalive.audition.role.GetAuditionRoleDetailResponse 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.common.ApiResponse
import kr.co.vividnext.sodalive.live.room.menu.UpdateLiveMenuRequest
import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
@ -39,4 +43,10 @@ interface AuditionApi {
@Query("size") size: Int, @Query("size") size: Int,
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<GetAuditionApplicantListResponse>> ): 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 package kr.co.vividnext.sodalive.audition
import kr.co.vividnext.sodalive.audition.role.AuditionRoleDetailViewModel import kr.co.vividnext.sodalive.audition.role.AuditionRoleDetailViewModel
import kr.co.vividnext.sodalive.audition.vote.VoteAuditionApplicantRequest
class AuditionRepository( class AuditionRepository(
private val api: AuditionApi private val api: AuditionApi
@ -44,4 +45,12 @@ class AuditionRepository(
size = size, size = size,
authHeader = token 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 package kr.co.vividnext.sodalive.audition.applicant
import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.load import coil.load
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
@ -11,29 +11,26 @@ import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemAuditionApplicantBinding import kr.co.vividnext.sodalive.databinding.ItemAuditionApplicantBinding
class AuditionApplicantListAdapter( class AuditionApplicantListAdapter(
private var itemList: List<GetAuditionRoleApplicantItem>,
private val onClickVote: (Int) -> Unit, private val onClickVote: (Int) -> Unit,
private val onClickPlayOrPause: (Int) -> Unit private val onClickPlayOrPause: (Int) -> Unit
) : RecyclerView.Adapter<AuditionApplicantListAdapter.ViewHolder>() { ) : ListAdapter<GetAuditionRoleApplicantItem, AuditionApplicantListAdapter.ViewHolder>(DiffCallback()) {
private var currentPlayingIndex: Int = -1 private var currentPlayingIndex: Int = -1
inner class DiffCallback( class DiffCallback : DiffUtil.ItemCallback<GetAuditionRoleApplicantItem>() {
private val oldList: List<GetAuditionRoleApplicantItem>,
private val newList: List<GetAuditionRoleApplicantItem>
) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areItemsTheSame( override fun areItemsTheSame(
oldItemPosition: Int, oldItem: GetAuditionRoleApplicantItem,
newItemPosition: Int newItem: GetAuditionRoleApplicantItem
) = oldList[oldItemPosition].applicantId == newList[newItemPosition].applicantId ): Boolean {
return oldItem.applicantId == newItem.applicantId
}
override fun areContentsTheSame( override fun areContentsTheSame(
oldItemPosition: Int, oldItem: GetAuditionRoleApplicantItem,
newItemPosition: Int newItem: GetAuditionRoleApplicantItem
) = oldList[oldItemPosition] == newList[newItemPosition] ): Boolean {
return oldItem == newItem
}
} }
inner class ViewHolder( inner class ViewHolder(
@ -68,16 +65,8 @@ class AuditionApplicantListAdapter(
) )
) )
override fun getItemCount() = itemList.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(itemList[position], position) holder.bind(getItem(position), position)
}
@SuppressLint("NotifyDataSetChanged")
fun updateData(newData: List<GetAuditionRoleApplicantItem>) {
itemList = newData
notifyDataSetChanged()
} }
fun updatePlayingIndex(newIndex: Int) { fun updatePlayingIndex(newIndex: Int) {

View File

@ -1,10 +1,14 @@
package kr.co.vividnext.sodalive.audition.role package kr.co.vividnext.sodalive.audition.role
import android.content.Intent import android.content.Intent
import android.graphics.Rect
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load import coil.load
import coil.transform.RoundedCornersTransformation import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R 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() { private fun bindData() {
@ -127,6 +196,26 @@ class AuditionRoleDetailActivity : BaseActivity<ActivityAuditionRoleDetailBindin
} }
viewModel.applicantListLiveData.observe(this) { 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 io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audition.AuditionRepository import kr.co.vividnext.sodalive.audition.AuditionRepository
import kr.co.vividnext.sodalive.audition.applicant.GetAuditionRoleApplicantItem 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.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import java.util.TimeZone
class AuditionRoleDetailViewModel(private val repository: AuditionRepository) : BaseViewModel() { class AuditionRoleDetailViewModel(private val repository: AuditionRepository) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>() private val _toastLiveData = MutableLiveData<String?>()
@ -91,8 +93,13 @@ class AuditionRoleDetailViewModel(private val repository: AuditionRepository) :
val data = applicantListResponse.data val data = applicantListResponse.data
_totalCountLiveData.value = data.totalCount _totalCountLiveData.value = data.totalCount
_applicantListLiveData.value = data.items addApplicantList(data.items)
page += 1
if (data.items.isEmpty()) {
isLast = true
} else {
page += 1
}
} else { } else {
if (applicantListResponse.message != null) { if (applicantListResponse.message != null) {
_toastLiveData.value = applicantListResponse.message _toastLiveData.value = applicantListResponse.message
@ -138,8 +145,13 @@ class AuditionRoleDetailViewModel(private val repository: AuditionRepository) :
val data = it.data val data = it.data
_totalCountLiveData.value = data.totalCount _totalCountLiveData.value = data.totalCount
_applicantListLiveData.value = data.items addApplicantList(data.items)
page += 1
if (data.items.isEmpty()) {
isLast = true
} else {
page += 1
}
} else { } else {
if (it.message != null) { if (it.message != null) {
_toastLiveData.value = it.message _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 { enum class AuditionApplicantSortType {
@SerializedName("NEWEST") @SerializedName("NEWEST")
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:textColor="@color/color_bbbbbb"
android:textSize="10.7sp" android:textSize="10.7sp"
app:layout_constraintEnd_toEndOf="parent" 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 <TextView
android:id="@+id/tv_sort_newest" android:id="@+id/tv_sort_newest"

View File

@ -5,7 +5,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingHorizontal="13.3dp" android:paddingHorizontal="13.3dp"
android:paddingVertical="18.7dp" android:paddingTop="18.7dp"
tools:background="@color/black"> tools:background="@color/black">
<ImageView <ImageView
@ -67,4 +67,13 @@
android:textSize="12sp" android:textSize="12sp"
tools:text="777" /> tools:text="777" />
</LinearLayout> </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> </androidx.constraintlayout.widget.ConstraintLayout>