오디션 상세 페이지 추가

This commit is contained in:
klaus 2024-12-31 12:49:16 +09:00
parent 4331792b75
commit 5d6ea6774b
14 changed files with 516 additions and 13 deletions

View File

@ -149,6 +149,7 @@
<activity android:name=".audio_content.playlist.create.AudioContentPlaylistCreateActivity" />
<activity android:name=".audio_content.playlist.modify.AudioContentPlaylistModifyActivity" />
<activity android:name=".audio_content.box.AudioContentBoxActivity" />
<activity android:name=".audition.detail.AuditionDetailActivity" />
<activity android:name=".mypage.alarm.AlarmListActivity" />
<activity android:name=".mypage.alarm.AddAlarmActivity" />

View File

@ -1,9 +1,11 @@
package kr.co.vividnext.sodalive.audition
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.audition.detail.GetAuditionDetailResponse
import kr.co.vividnext.sodalive.common.ApiResponse
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Path
import retrofit2.http.Query
interface AuditionApi {
@ -13,4 +15,10 @@ interface AuditionApi {
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAuditionListResponse>>
@GET("/audition/{id}")
fun getAuditionDetail(
@Path("id") id: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAuditionDetailResponse>>
}

View File

@ -1,12 +1,15 @@
package kr.co.vividnext.sodalive.audition
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audition.detail.AuditionDetailActivity
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.FragmentAuditionBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
@ -33,7 +36,13 @@ class AuditionFragment : BaseFragment<FragmentAuditionBinding>(
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
val recyclerView = binding.rvAudition
adapter = AuditionListAdapter { }
adapter = AuditionListAdapter {
startActivity(
Intent(requireContext(), AuditionDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDITION_ID, it)
}
)
}
recyclerView.layoutManager = LinearLayoutManager(
requireContext(),

View File

@ -16,7 +16,7 @@ import kr.co.vividnext.sodalive.databinding.ItemAuditionListInProgressHeaderBind
import kr.co.vividnext.sodalive.extensions.dpToPx
class AuditionListAdapter(
private val onItemClick: (GetAuditionListItem) -> Unit
private val onItemClick: (Long) -> Unit
) : ListAdapter<AuditionListAdapter.DisplayItem, RecyclerView.ViewHolder>(DiffCallback()) {
companion object {
private const val TYPE_IN_PROGRESS_HEADER = 0
@ -73,7 +73,7 @@ class AuditionListAdapter(
binding.blackCover.visibility = View.VISIBLE
} else {
binding.blackCover.visibility = View.GONE
binding.root.setOnClickListener { onItemClick(data.item) }
binding.root.setOnClickListener { onItemClick(data.item.id) }
}
}
}

View File

@ -1,8 +1,5 @@
package kr.co.vividnext.sodalive.audition
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
class AuditionRepository(
private val api: AuditionApi
) {
@ -10,11 +7,17 @@ class AuditionRepository(
page: Int,
size: Int,
token: String
): Single<ApiResponse<GetAuditionListResponse>> {
return api.getAuditionList(
) = api.getAuditionList(
page = page - 1,
size = size,
authHeader = token
)
}
fun getAuditionDetail(
auditionId: Long,
token: String
) = api.getAuditionDetail(
id = auditionId,
authHeader = token
)
}

View File

@ -0,0 +1,139 @@
package kr.co.vividnext.sodalive.audition.detail
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAuditionDetailBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class AuditionDetailActivity : BaseActivity<ActivityAuditionDetailBinding>(
ActivityAuditionDetailBinding::inflate
) {
private val viewModel: AuditionDetailViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AuditionDetailRoleAdapter
private var auditionId: Long = 0
private var isOpenInformation = false
override fun onCreate(savedInstanceState: Bundle?) {
auditionId = intent.getLongExtra(Constants.EXTRA_AUDITION_ID, 0)
if (auditionId <= 0) {
Toast.makeText(
applicationContext,
"잘못된 요청입니다.\n다시 시도해 주세요.",
Toast.LENGTH_LONG
).show()
finish()
}
super.onCreate(savedInstanceState)
bindData()
viewModel.getAuditionDetail(auditionId = auditionId)
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.tvOpen.setOnClickListener {
isOpenInformation = !isOpenInformation
if (isOpenInformation) {
binding.tvInformation.maxLines = Int.MAX_VALUE
binding.tvOpen.text = "접기"
binding.tvOpen.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_live_detail_top,
0,
0,
0
)
} else {
binding.tvInformation.maxLines = 3
binding.tvOpen.text = "펼치기"
binding.tvOpen.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_live_detail_bottom,
0,
0,
0
)
}
}
adapter = AuditionDetailRoleAdapter { }
binding.rvRole.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
binding.rvRole.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 = 7.5f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.top = 7.5f.dpToPx().toInt()
outRect.bottom = 0
}
else -> {
outRect.top = 7.5f.dpToPx().toInt()
outRect.bottom = 7.5f.dpToPx().toInt()
}
}
}
})
binding.rvRole.adapter = adapter
}
private fun bindData() {
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth, "")
} else {
loadingDialog.dismiss()
}
}
viewModel.auditionDetailLiveData.observe(this) {
binding.ivCover.load(it.imageUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5f.dpToPx()))
}
binding.toolbar.tvBack.text = it.title
binding.tvInformation.text = it.information
adapter.addItems(it.roleList)
}
}
}

View File

@ -0,0 +1,77 @@
package kr.co.vividnext.sodalive.audition.detail
import android.view.LayoutInflater
import android.view.View
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.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemAuditionRoleBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class AuditionDetailRoleAdapter(
private val onItemClick: (Long) -> Unit
) : ListAdapter<GetAuditionRoleListData, AuditionDetailRoleAdapter.ViewHolder>(DiffCallback()) {
private var items = mutableListOf<GetAuditionRoleListData>()
class DiffCallback : DiffUtil.ItemCallback<GetAuditionRoleListData>() {
override fun areItemsTheSame(
oldItem: GetAuditionRoleListData,
newItem: GetAuditionRoleListData
): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: GetAuditionRoleListData,
newItem: GetAuditionRoleListData
): Boolean {
return oldItem.roleId == newItem.roleId
}
}
inner class ViewHolder(
private val binding: ItemAuditionRoleBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAuditionRoleListData) {
binding.tvName.text = item.name
binding.ivCover.load(item.imageUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(6.7f.dpToPx()))
}
if (item.isComplete) {
binding.blackCover.visibility = View.VISIBLE
binding.tvStatus.text = "모집완료"
binding.tvStatus.setBackgroundResource(R.drawable.bg_round_corner_13_3_909090)
} else {
binding.blackCover.visibility = View.GONE
binding.tvStatus.text = "모집중"
binding.tvStatus.setBackgroundResource(R.drawable.bg_round_corner_13_3_3bb9f1)
}
binding.root.setOnClickListener { onItemClick(item.roleId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemAuditionRoleBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
fun addItems(items: List<GetAuditionRoleListData>) {
this.items.addAll(items)
submitList(this.items.toList())
}
}

View File

@ -0,0 +1,67 @@
package kr.co.vividnext.sodalive.audition.detail
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audition.AuditionRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AuditionDetailViewModel(
private val repository: AuditionRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _auditionDetailLiveData = MutableLiveData<GetAuditionDetailResponse>()
val auditionDetailLiveData: LiveData<GetAuditionDetailResponse>
get() = _auditionDetailLiveData
fun getAuditionDetail(auditionId: Long, onFailure: (() -> Unit)? = null) {
_isLoading.value = true
compositeDisposable.add(
repository.getAuditionDetail(
auditionId = auditionId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_auditionDetailLiveData.value = it.data
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
if (onFailure != null) {
onFailure()
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
if (onFailure != null) {
onFailure()
}
}
)
)
}
}

View File

@ -0,0 +1,21 @@
package kr.co.vividnext.sodalive.audition.detail
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class GetAuditionDetailResponse(
@SerializedName("auditionId") val auditionId: Long,
@SerializedName("title") val title: String,
@SerializedName("imageUrl") val imageUrl: String,
@SerializedName("information") val information: String,
@SerializedName("roleList") val roleList: List<GetAuditionRoleListData>
)
@Keep
data class GetAuditionRoleListData(
@SerializedName("roleId") val roleId: Long,
@SerializedName("name") val name: String,
@SerializedName("imageUrl") val imageUrl: String,
@SerializedName("isComplete") val isComplete: Boolean
)

View File

@ -32,6 +32,7 @@ object Constants {
const val EXTRA_SERIES_ID = "extra_series_id"
const val EXTRA_NICKNAME = "extra_nickname"
const val EXTRA_MESSAGE_ID = "extra_message_id"
const val EXTRA_AUDITION_ID = "extra_audition_id"
const val EXTRA_ROOM_DETAIL = "extra_room_detail"
const val EXTRA_MESSAGE_BOX = "extra_message_box"
const val EXTRA_SERIES_TITLE = "extra_series_title"

View File

@ -42,6 +42,7 @@ import kr.co.vividnext.sodalive.audio_content.upload.theme.AudioContentThemeView
import kr.co.vividnext.sodalive.audition.AuditionApi
import kr.co.vividnext.sodalive.audition.AuditionRepository
import kr.co.vividnext.sodalive.audition.AuditionViewModel
import kr.co.vividnext.sodalive.audition.detail.AuditionDetailViewModel
import kr.co.vividnext.sodalive.common.ApiBuilder
import kr.co.vividnext.sodalive.common.ObjectBox
import kr.co.vividnext.sodalive.explorer.ExplorerApi
@ -284,6 +285,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { AudioContentPlaylistModifyViewModel(get()) }
viewModel { AudioContentPlayerViewModel() }
viewModel { AuditionViewModel(get()) }
viewModel { AuditionDetailViewModel(get()) }
}
private val repositoryModule = module {

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_909090" />
<corners android:radius="13.3dp" />
<stroke
android:width="1dp"
android:color="@color/color_909090" />
</shape>

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:background="@color/black">
<include
android:id="@+id/toolbar"
layout="@layout/detail_toolbar" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:paddingHorizontal="13.3dp"
android:paddingVertical="8dp">
<ImageView
android:id="@+id/iv_cover"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@null"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="1000:530"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_launcher_background" />
<TextView
android:id="@+id/tv_information_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:fontFamily="@font/gmarket_sans_bold"
android:text="오디션 정보"
android:textColor="@color/color_eeeeee"
android:textSize="14.7sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv_cover" />
<TextView
android:id="@+id/tv_information"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:ellipsize="end"
android:fontFamily="@font/gmarket_sans_medium"
android:maxLines="3"
android:textColor="@color/color_777777"
android:textSize="13.3sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_information_title" />
<TextView
android:id="@+id/tv_open"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="7dp"
android:drawablePadding="6.7dp"
android:fontFamily="@font/gmarket_sans_medium"
android:gravity="center"
android:text="펼치기"
android:textColor="@color/color_bbbbbb"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_information" />
<TextView
android:id="@+id/tv_role_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:fontFamily="@font/gmarket_sans_bold"
android:text="오디션 캐릭터"
android:textColor="@color/color_eeeeee"
android:textSize="14.7sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_open" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_role"
android:layout_width="0dp"
android:layout_height="match_parent"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingVertical="15dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_role_title" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
tools:background="@color/black"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/iv_cover"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@null"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="1000:350"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_launcher_background" />
<View
android:id="@+id/black_cover"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/color_cc000000"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/iv_cover"
app:layout_constraintEnd_toEndOf="@+id/iv_cover"
app:layout_constraintStart_toStartOf="@+id/iv_cover"
app:layout_constraintTop_toTopOf="@+id/iv_cover" />
<TextView
android:id="@+id/tv_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:layout_marginTop="7dp"
android:fontFamily="@font/gmarket_sans_medium"
android:paddingHorizontal="9dp"
android:paddingVertical="3dp"
android:textColor="@color/white"
android:textSize="10.3sp"
app:layout_constraintStart_toStartOf="@+id/iv_cover"
app:layout_constraintTop_toTopOf="@+id/iv_cover"
tools:background="@drawable/bg_round_corner_13_3_909090"
tools:text="모집완료" />
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:fontFamily="@font/gmarket_sans_medium"
android:textColor="@color/color_eeeeee"
android:textSize="13.3sp"
app:layout_constraintEnd_toEndOf="@+id/iv_cover"
app:layout_constraintStart_toStartOf="@+id/iv_cover"
app:layout_constraintTop_toBottomOf="@+id/iv_cover"
tools:text="[원작] 성인식" />
</androidx.constraintlayout.widget.ConstraintLayout>