From 5d6ea6774b2892cf6cd01df064a9ac525b117627 Mon Sep 17 00:00:00 2001 From: klaus Date: Tue, 31 Dec 2024 12:49:16 +0900 Subject: [PATCH] =?UTF-8?q?=EC=98=A4=EB=94=94=EC=85=98=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 1 + .../sodalive/audition/AuditionApi.kt | 8 + .../sodalive/audition/AuditionFragment.kt | 11 +- .../sodalive/audition/AuditionListAdapter.kt | 4 +- .../sodalive/audition/AuditionRepository.kt | 23 +-- .../audition/detail/AuditionDetailActivity.kt | 139 ++++++++++++++++++ .../detail/AuditionDetailRoleAdapter.kt | 77 ++++++++++ .../detail/AuditionDetailViewModel.kt | 67 +++++++++ .../detail/GetAuditionDetailResponse.kt | 21 +++ .../co/vividnext/sodalive/common/Constants.kt | 1 + .../java/kr/co/vividnext/sodalive/di/AppDI.kt | 2 + .../drawable/bg_round_corner_13_3_909090.xml | 8 + .../res/layout/activity_audition_detail.xml | 107 ++++++++++++++ .../main/res/layout/item_audition_role.xml | 60 ++++++++ 14 files changed, 516 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audition/detail/AuditionDetailActivity.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audition/detail/AuditionDetailRoleAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audition/detail/AuditionDetailViewModel.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audition/detail/GetAuditionDetailResponse.kt create mode 100644 app/src/main/res/drawable/bg_round_corner_13_3_909090.xml create mode 100644 app/src/main/res/layout/activity_audition_detail.xml create mode 100644 app/src/main/res/layout/item_audition_role.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9fc7474..c54482e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -149,6 +149,7 @@ + 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 8ec06a8..4f13957 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 @@ -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> + + @GET("/audition/{id}") + fun getAuditionDetail( + @Path("id") id: Long, + @Header("Authorization") authHeader: String + ): Single> } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionFragment.kt index 57bdbea..a77b4a5 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionFragment.kt @@ -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( 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(), diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionListAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionListAdapter.kt index 317cfed..3e1410a 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionListAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionListAdapter.kt @@ -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(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) } } } } 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 43cab77..11ce2c9 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,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> { - return api.getAuditionList( - page = page - 1, - size = size, - authHeader = token - ) - } + ) = api.getAuditionList( + page = page - 1, + size = size, + authHeader = token + ) + + fun getAuditionDetail( + auditionId: Long, + token: String + ) = api.getAuditionDetail( + id = auditionId, + authHeader = token + ) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/detail/AuditionDetailActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/detail/AuditionDetailActivity.kt new file mode 100644 index 0000000..f15f360 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/detail/AuditionDetailActivity.kt @@ -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::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) + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/detail/AuditionDetailRoleAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/detail/AuditionDetailRoleAdapter.kt new file mode 100644 index 0000000..4a5037c --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/detail/AuditionDetailRoleAdapter.kt @@ -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(DiffCallback()) { + private var items = mutableListOf() + + class DiffCallback : DiffUtil.ItemCallback() { + 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) { + this.items.addAll(items) + submitList(this.items.toList()) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/detail/AuditionDetailViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/detail/AuditionDetailViewModel.kt new file mode 100644 index 0000000..350f8db --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/detail/AuditionDetailViewModel.kt @@ -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() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private var _auditionDetailLiveData = MutableLiveData() + val auditionDetailLiveData: LiveData + 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() + } + } + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/detail/GetAuditionDetailResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/detail/GetAuditionDetailResponse.kt new file mode 100644 index 0000000..b6a1ae1 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/detail/GetAuditionDetailResponse.kt @@ -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 +) + +@Keep +data class GetAuditionRoleListData( + @SerializedName("roleId") val roleId: Long, + @SerializedName("name") val name: String, + @SerializedName("imageUrl") val imageUrl: String, + @SerializedName("isComplete") val isComplete: Boolean +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt index 2abe16a..16aac00 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt @@ -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" diff --git a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt index 8a33a70..2f6388f 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt @@ -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 { diff --git a/app/src/main/res/drawable/bg_round_corner_13_3_909090.xml b/app/src/main/res/drawable/bg_round_corner_13_3_909090.xml new file mode 100644 index 0000000..cad5f1b --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_13_3_909090.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_audition_detail.xml b/app/src/main/res/layout/activity_audition_detail.xml new file mode 100644 index 0000000..f3570db --- /dev/null +++ b/app/src/main/res/layout/activity_audition_detail.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_audition_role.xml b/app/src/main/res/layout/item_audition_role.xml new file mode 100644 index 0000000..4d5ceea --- /dev/null +++ b/app/src/main/res/layout/item_audition_role.xml @@ -0,0 +1,60 @@ + + + + + + + + + + +