diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 082eaa5..09b08bb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -115,6 +115,7 @@ <activity android:name=".mypage.profile.ProfileUpdateActivity" /> <activity android:name=".mypage.profile.nickname.NicknameUpdateActivity" /> <activity android:name=".mypage.profile.password.ModifyPasswordActivity" /> + <activity android:name=".audio_content.curation.AudioContentCurationActivity" /> <activity android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity" diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentApi.kt index 4f2dc71..0add8c2 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentApi.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentApi.kt @@ -4,6 +4,7 @@ import io.reactivex.rxjava3.core.Single import kr.co.vividnext.sodalive.audio_content.comment.GetAudioContentCommentListResponse import kr.co.vividnext.sodalive.audio_content.comment.ModifyCommentRequest import kr.co.vividnext.sodalive.audio_content.comment.RegisterAudioContentCommentRequest +import kr.co.vividnext.sodalive.audio_content.curation.GetCurationContentResponse import kr.co.vividnext.sodalive.audio_content.detail.GetAudioContentDetailResponse import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeRequest import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeResponse @@ -144,4 +145,13 @@ interface AudioContentApi { @Body request: ModifyCommentRequest, @Header("Authorization") authHeader: String ): Single<ApiResponse<Any>> + + @GET("/audio-content/curation/{id}") + fun getAudioContentListByCurationId( + @Path("id") id: Long, + @Query("page") page: Int, + @Query("size") size: Int, + @Query("sort-type") sort: AudioContentViewModel.Sort, + @Header("Authorization") authHeader: String + ): Single<ApiResponse<GetCurationContentResponse>> } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentRepository.kt index 726edc4..bb25cfe 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentRepository.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentRepository.kt @@ -14,6 +14,20 @@ class AudioContentRepository( private val api: AudioContentApi, private val userApi: UserApi ) { + fun getAudioContentListByCurationId( + curationId: Long, + page: Int, + size: Int, + sort: AudioContentViewModel.Sort = AudioContentViewModel.Sort.NEWEST, + token: String + ) = api.getAudioContentListByCurationId( + id = curationId, + page = page - 1, + size = size, + sort = sort, + authHeader = token + ) + fun getAudioContentList( id: Long, page: Int, diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/curation/AudioContentCurationActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/curation/AudioContentCurationActivity.kt new file mode 100644 index 0000000..48a4c4c --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/curation/AudioContentCurationActivity.kt @@ -0,0 +1,199 @@ +package kr.co.vividnext.sodalive.audio_content.curation + +import android.annotation.SuppressLint +import android.content.Intent +import android.graphics.Rect +import android.os.Bundle +import android.view.View +import android.widget.TextView +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.audio_content.AudioContentAdapter +import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel +import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity +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.ActivityAudioContentCurationBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import org.koin.android.ext.android.inject + +class AudioContentCurationActivity : BaseActivity<ActivityAudioContentCurationBinding>( + ActivityAudioContentCurationBinding::inflate +) { + + private val viewModel: AudioContentCurationViewModel by inject() + + private lateinit var loadingDialog: LoadingDialog + private lateinit var adapter: AudioContentAdapter + + private var curationId: Long = 0 + private lateinit var title: String + + override fun onCreate(savedInstanceState: Bundle?) { + title = intent.getStringExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_TITLE) ?: "" + curationId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_ID, 0) + super.onCreate(savedInstanceState) + + if (title.isBlank() || curationId <= 0) { + Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show() + finish() + } + + bindData() + viewModel.getContentList(curationId = curationId) + } + + override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + binding.toolbar.tvBack.text = title + binding.toolbar.tvBack.setOnClickListener { finish() } + + adapter = AudioContentAdapter { + startActivity( + Intent(applicationContext, AudioContentDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) + } + ) + } + + binding.rvCuration.layoutManager = LinearLayoutManager( + applicationContext, + LinearLayoutManager.VERTICAL, + false + ) + + binding.rvCuration.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 = 40f.dpToPx().toInt() + outRect.bottom = 20f.dpToPx().toInt() + } + + adapter.itemCount - 1 -> { + outRect.top = 20f.dpToPx().toInt() + outRect.bottom = 40f.dpToPx().toInt() + } + + else -> { + outRect.top = 20f.dpToPx().toInt() + outRect.bottom = 20f.dpToPx().toInt() + } + } + + outRect.left = 13.3f.dpToPx().toInt() + outRect.right = 13.3f.dpToPx().toInt() + } + }) + + binding.rvCuration.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.getContentList(curationId) + } + } + }) + + binding.rvCuration.adapter = adapter + + binding.tvSortNewest.setOnClickListener { + viewModel.changeSort(AudioContentViewModel.Sort.NEWEST) + } + + binding.tvSortPriceLow.setOnClickListener { + viewModel.changeSort(AudioContentViewModel.Sort.PRICE_LOW) + } + + binding.tvSortPriceHigh.setOnClickListener { + viewModel.changeSort(AudioContentViewModel.Sort.PRICE_HIGH) + } + } + + @SuppressLint("NotifyDataSetChanged") + 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.contentListLiveData.observe(this) { + if (viewModel.page - 1 == 1) { + adapter.items.clear() + binding.rvCuration.scrollToPosition(0) + } + + binding.tvTotalCount.text = "${it.totalCount}" + + adapter.items.addAll(it.items) + adapter.notifyDataSetChanged() + } + + viewModel.sort.observe(this) { + deselectSort() + selectSort( + when (it) { + AudioContentViewModel.Sort.PRICE_HIGH -> { + binding.tvSortPriceHigh + } + + AudioContentViewModel.Sort.PRICE_LOW -> { + binding.tvSortPriceLow + } + + else -> { + binding.tvSortNewest + } + } + ) + + viewModel.getContentList(curationId = curationId) + } + } + + private fun deselectSort() { + val color = ContextCompat.getColor( + applicationContext, + R.color.color_88e2e2e2 + ) + + binding.tvSortNewest.setTextColor(color) + binding.tvSortPriceLow.setTextColor(color) + binding.tvSortPriceHigh.setTextColor(color) + } + + private fun selectSort(view: TextView) { + view.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_e2e2e2 + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/curation/AudioContentCurationViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/curation/AudioContentCurationViewModel.kt new file mode 100644 index 0000000..c8f5c75 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/curation/AudioContentCurationViewModel.kt @@ -0,0 +1,86 @@ +package kr.co.vividnext.sodalive.audio_content.curation + +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.audio_content.AudioContentRepository +import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager + +class AudioContentCurationViewModel( + private val repository: AudioContentRepository +) : 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 _contentListLiveData = MutableLiveData<GetCurationContentResponse>() + val contentListLiveData: LiveData<GetCurationContentResponse> + get() = _contentListLiveData + + private val _sort = MutableLiveData(AudioContentViewModel.Sort.NEWEST) + val sort: LiveData<AudioContentViewModel.Sort> + get() = _sort + + private var isLast = false + var page = 1 + private val size = 10 + + fun getContentList(curationId: Long) { + if (!_isLoading.value!! && !isLast) { + _isLoading.value = true + + compositeDisposable.add( + repository.getAudioContentListByCurationId( + curationId = curationId, + page = page, + size = size, + sort = _sort.value!!, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + if (it.data.items.isNotEmpty()) { + page += 1 + _contentListLiveData.postValue(it.data!!) + } else { + isLast = true + } + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + } + + fun changeSort(sort: AudioContentViewModel.Sort) { + page = 1 + isLast = false + _sort.postValue(sort) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/curation/GetCurationContentResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/curation/GetCurationContentResponse.kt new file mode 100644 index 0000000..125fc5c --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/curation/GetCurationContentResponse.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.audio_content.curation + +import com.google.gson.annotations.SerializedName +import kr.co.vividnext.sodalive.explorer.profile.GetAudioContentListItem + +data class GetCurationContentResponse( + @SerializedName("totalCount") val totalCount: Int, + @SerializedName("items") val items: List<GetAudioContentListItem> +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainCurationAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainCurationAdapter.kt index 34b3883..54012ae 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainCurationAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainCurationAdapter.kt @@ -14,6 +14,7 @@ import kr.co.vividnext.sodalive.extensions.dpToPx class AudioContentMainCurationAdapter( private val onClickItem: (Long) -> Unit, private val onClickCreator: (Long) -> Unit, + private val onClickCurationMore: (Long, String) -> Unit ) : RecyclerView.Adapter<AudioContentMainCurationAdapter.ViewHolder>() { private val items = mutableListOf<GetAudioContentCurationResponse>() @@ -25,6 +26,7 @@ class AudioContentMainCurationAdapter( fun bind(item: GetAudioContentCurationResponse) { binding.tvTitle.text = item.title binding.tvDesc.text = item.description + binding.ivAll.setOnClickListener { onClickCurationMore(item.curationId, item.title) } setAudioContentList(item.audioContents) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainFragment.kt index 5675a0a..38f0f16 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainFragment.kt @@ -18,6 +18,7 @@ import com.zhpan.indicator.enums.IndicatorSlideMode import com.zhpan.indicator.enums.IndicatorStyle import kr.co.pointclick.sdk.offerwall.core.PointClickAd import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.audio_content.curation.AudioContentCurationActivity import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListActivity import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity @@ -387,6 +388,15 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>( putExtra(Constants.EXTRA_USER_ID, it) } ) + }, + + onClickCurationMore = { curationId, title -> + startActivity( + Intent(requireContext(), AudioContentCurationActivity::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_ID, curationId) + putExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_TITLE, title) + } + ) } ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/GetAudioContentMainResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/GetAudioContentMainResponse.kt index 3394576..2781910 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/GetAudioContentMainResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/GetAudioContentMainResponse.kt @@ -30,6 +30,7 @@ data class GetAudioContentMainItem( ) data class GetAudioContentCurationResponse( + @SerializedName("curationId") val curationId: Long, @SerializedName("title") val title: String, @SerializedName("description") val description: String, @SerializedName("contents") val audioContents: List<GetAudioContentMainItem> 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 6eb7224..015812b 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 @@ -46,6 +46,8 @@ object Constants { const val EXTRA_AUDIO_CONTENT_COMMENT = "audio_content_comment" const val EXTRA_AUDIO_CONTENT_LOADING = "audio_content_loading" const val EXTRA_AUDIO_CONTENT_CREATOR_ID = "audio_content_creator_id" + const val EXTRA_AUDIO_CONTENT_CURATION_ID = "extra_audio_content_curation_id" + const val EXTRA_AUDIO_CONTENT_CURATION_TITLE = "extra_audio_content_curation_title" const val EXTRA_AUDIO_CONTENT_NEXT_ACTION = "audio_content_next_action" const val EXTRA_AUDIO_CONTENT_ALERT_PREVIEW = "audio_content_alert_preview" const val EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL = "audio_content_cover_image_url" 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 37c62c0..6bf9cc7 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 @@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.audio_content.PlaybackTrackingRepository import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentListViewModel import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentReplyViewModel import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentRepository +import kr.co.vividnext.sodalive.audio_content.curation.AudioContentCurationViewModel import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailViewModel import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainViewModel import kr.co.vividnext.sodalive.audio_content.modify.AudioContentModifyViewModel @@ -191,6 +192,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { viewModel { NicknameUpdateViewModel(get()) } viewModel { MemberTagViewModel(get()) } viewModel { UserProfileDonationAllViewModel(get()) } + viewModel { AudioContentCurationViewModel(get()) } } private val repositoryModule = module { diff --git a/app/src/main/res/layout/activity_audio_content_curation.xml b/app/src/main/res/layout/activity_audio_content_curation.xml new file mode 100644 index 0000000..a260fa1 --- /dev/null +++ b/app/src/main/res/layout/activity_audio_content_curation.xml @@ -0,0 +1,100 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/black" + android:orientation="vertical"> + + <include + android:id="@+id/toolbar" + layout="@layout/detail_toolbar" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="13.3dp" + android:background="@color/color_161616" + android:gravity="end" + android:orientation="horizontal" + android:paddingHorizontal="20dp" + android:paddingVertical="13.3dp"> + + <TextView + android:id="@+id/tv_sort_newest" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="@font/gmarket_sans_medium" + android:gravity="center" + android:text="최신순" + android:textColor="@color/color_88e2e2e2" + android:textSize="13.3sp" /> + + <TextView + android:id="@+id/tv_sort_price_high" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginHorizontal="13.3dp" + android:fontFamily="@font/gmarket_sans_medium" + android:gravity="center" + android:text="높은 가격순" + android:textColor="@color/color_88e2e2e2" + android:textSize="13.3sp" /> + + <TextView + android:id="@+id/tv_sort_price_low" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="@font/gmarket_sans_medium" + android:gravity="center" + android:text="낮은 가격순" + android:textColor="@color/color_88e2e2e2" + android:textSize="13.3sp" /> + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:orientation="horizontal" + android:paddingHorizontal="20dp" + android:paddingVertical="13.3dp"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="@font/gmarket_sans_medium" + android:gravity="center" + android:text="전체" + android:textColor="@color/color_e2e2e2" + android:textSize="13.3sp" /> + + <TextView + android:id="@+id/tv_total_count" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:fontFamily="@font/gmarket_sans_medium" + android:gravity="center" + android:text="0" + android:textColor="@color/color_ff5c49" + android:textSize="13.3sp" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="2dp" + android:fontFamily="@font/gmarket_sans_medium" + android:gravity="center" + android:text="개" + android:textColor="@color/color_e2e2e2" + android:textSize="13.3sp" /> + </LinearLayout> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/rv_curation" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingHorizontal="13.3dp" /> + +</LinearLayout>