오디션 상세 페이지 추가

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.create.AudioContentPlaylistCreateActivity" />
<activity android:name=".audio_content.playlist.modify.AudioContentPlaylistModifyActivity" /> <activity android:name=".audio_content.playlist.modify.AudioContentPlaylistModifyActivity" />
<activity android:name=".audio_content.box.AudioContentBoxActivity" /> <activity android:name=".audio_content.box.AudioContentBoxActivity" />
<activity android:name=".audition.detail.AuditionDetailActivity" />
<activity android:name=".mypage.alarm.AlarmListActivity" /> <activity android:name=".mypage.alarm.AlarmListActivity" />
<activity android:name=".mypage.alarm.AddAlarmActivity" /> <activity android:name=".mypage.alarm.AddAlarmActivity" />

View File

@ -1,9 +1,11 @@
package kr.co.vividnext.sodalive.audition package kr.co.vividnext.sodalive.audition
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.audition.detail.GetAuditionDetailResponse
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header
import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
interface AuditionApi { interface AuditionApi {
@ -13,4 +15,10 @@ interface AuditionApi {
@Query("size") size: Int, @Query("size") size: Int,
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<GetAuditionListResponse>> ): 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 package kr.co.vividnext.sodalive.audition
import android.content.Intent
import android.graphics.Rect import android.graphics.Rect
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.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView 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.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.FragmentAuditionBinding import kr.co.vividnext.sodalive.databinding.FragmentAuditionBinding
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
@ -33,7 +36,13 @@ class AuditionFragment : BaseFragment<FragmentAuditionBinding>(
loadingDialog = LoadingDialog(requireActivity(), layoutInflater) loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
val recyclerView = binding.rvAudition val recyclerView = binding.rvAudition
adapter = AuditionListAdapter { } adapter = AuditionListAdapter {
startActivity(
Intent(requireContext(), AuditionDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDITION_ID, it)
}
)
}
recyclerView.layoutManager = LinearLayoutManager( recyclerView.layoutManager = LinearLayoutManager(
requireContext(), requireContext(),

View File

@ -16,7 +16,7 @@ import kr.co.vividnext.sodalive.databinding.ItemAuditionListInProgressHeaderBind
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
class AuditionListAdapter( class AuditionListAdapter(
private val onItemClick: (GetAuditionListItem) -> Unit private val onItemClick: (Long) -> Unit
) : ListAdapter<AuditionListAdapter.DisplayItem, RecyclerView.ViewHolder>(DiffCallback()) { ) : ListAdapter<AuditionListAdapter.DisplayItem, RecyclerView.ViewHolder>(DiffCallback()) {
companion object { companion object {
private const val TYPE_IN_PROGRESS_HEADER = 0 private const val TYPE_IN_PROGRESS_HEADER = 0
@ -73,7 +73,7 @@ class AuditionListAdapter(
binding.blackCover.visibility = View.VISIBLE binding.blackCover.visibility = View.VISIBLE
} else { } else {
binding.blackCover.visibility = View.GONE 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 package kr.co.vividnext.sodalive.audition
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
class AuditionRepository( class AuditionRepository(
private val api: AuditionApi private val api: AuditionApi
) { ) {
@ -10,11 +7,17 @@ class AuditionRepository(
page: Int, page: Int,
size: Int, size: Int,
token: String token: String
): Single<ApiResponse<GetAuditionListResponse>> { ) = api.getAuditionList(
return api.getAuditionList(
page = page - 1, page = page - 1,
size = size, size = size,
authHeader = token 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_SERIES_ID = "extra_series_id"
const val EXTRA_NICKNAME = "extra_nickname" const val EXTRA_NICKNAME = "extra_nickname"
const val EXTRA_MESSAGE_ID = "extra_message_id" 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_ROOM_DETAIL = "extra_room_detail"
const val EXTRA_MESSAGE_BOX = "extra_message_box" const val EXTRA_MESSAGE_BOX = "extra_message_box"
const val EXTRA_SERIES_TITLE = "extra_series_title" 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.AuditionApi
import kr.co.vividnext.sodalive.audition.AuditionRepository import kr.co.vividnext.sodalive.audition.AuditionRepository
import kr.co.vividnext.sodalive.audition.AuditionViewModel 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.ApiBuilder
import kr.co.vividnext.sodalive.common.ObjectBox import kr.co.vividnext.sodalive.common.ObjectBox
import kr.co.vividnext.sodalive.explorer.ExplorerApi import kr.co.vividnext.sodalive.explorer.ExplorerApi
@ -284,6 +285,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { AudioContentPlaylistModifyViewModel(get()) } viewModel { AudioContentPlaylistModifyViewModel(get()) }
viewModel { AudioContentPlayerViewModel() } viewModel { AudioContentPlayerViewModel() }
viewModel { AuditionViewModel(get()) } viewModel { AuditionViewModel(get()) }
viewModel { AuditionDetailViewModel(get()) }
} }
private val repositoryModule = module { 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>