From f928fac9dae907a7c1472f8722d6c93e238e4ed0 Mon Sep 17 00:00:00 2001 From: klaus Date: Wed, 12 Nov 2025 15:26:02 +0900 Subject: [PATCH] =?UTF-8?q?fix(audio-content):=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=ED=8E=98=EC=9D=B4=EC=A7=80=20UI/API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 1 + .../sodalive/audio_content/AudioContentApi.kt | 12 ++ .../audio_content/AudioContentRepository.kt | 15 +++ .../all/AudioContentAllActivity.kt | 118 ++++++++++++++++++ .../all/AudioContentAllViewModel.kt | 67 ++++++++++ .../co/vividnext/sodalive/common/Constants.kt | 1 + .../java/kr/co/vividnext/sodalive/di/AppDI.kt | 1 + .../sodalive/home/HomeContentAdapter.kt | 6 + .../vividnext/sodalive/home/HomeFragment.kt | 22 +++- .../res/layout/activity_audio_content_all.xml | 38 ++++++ app/src/main/res/layout/fragment_home.xml | 31 +++-- 11 files changed, 304 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentAllActivity.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentAllViewModel.kt create mode 100644 app/src/main/res/layout/activity_audio_content_all.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4a055a12..3f0857c8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -106,6 +106,7 @@ + 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 bedbef6b..17c5632a 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 @@ -21,6 +21,7 @@ import kr.co.vividnext.sodalive.audio_content.player.GenerateUrlResponse import kr.co.vividnext.sodalive.audio_content.upload.theme.GetAudioContentThemeResponse import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.explorer.profile.GetAudioContentListResponse +import kr.co.vividnext.sodalive.home.AudioContentMainItem import kr.co.vividnext.sodalive.settings.ContentType import okhttp3.MultipartBody import okhttp3.RequestBody @@ -36,6 +37,17 @@ import retrofit2.http.Path import retrofit2.http.Query interface AudioContentApi { + @GET("/audio-content/all") + fun getAllAudioContents( + @Query("isAdultContentVisible") isAdultContentVisible: Boolean, + @Query("contentType") contentType: ContentType, + @Query("page") page: Int, + @Query("size") size: Int, + @Query("isFree") isFree: Boolean?, + @Query("isPointAvailableOnly") isPointAvailableOnly: Boolean?, + @Header("Authorization") authHeader: String + ): Single>> + @GET("/audio-content") fun getAudioContentList( @Query("creator-id") id: Long, 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 9fadeecc..b6bc8554 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 @@ -189,4 +189,19 @@ class AudioContentRepository( sort = sort, authHeader = token ) + fun getAllAudioContents( + page: Int, + size: Int, + isFree: Boolean? = null, + isPointAvailableOnly: Boolean? = null, + token: String + ) = api.getAllAudioContents( + isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible, + contentType = ContentType.values()[SharedPreferenceManager.contentPreference], + page = page - 1, + size = size, + isFree = isFree, + isPointAvailableOnly = isPointAvailableOnly, + authHeader = token + ) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentAllActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentAllActivity.kt new file mode 100644 index 00000000..e4450793 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentAllActivity.kt @@ -0,0 +1,118 @@ +package kr.co.vividnext.sodalive.audio_content.all + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +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.GridSpacingItemDecoration +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.databinding.ActivityAudioContentAllBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.home.HomeContentAdapter +import org.koin.android.ext.android.inject + +@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) +class AudioContentAllActivity : BaseActivity( + ActivityAudioContentAllBinding::inflate +) { + private val viewModel: AudioContentAllViewModel by inject() + + private lateinit var loadingDialog: LoadingDialog + private lateinit var adapter: HomeContentAdapter + + private var isFree: Boolean = false + private var isPointOnly: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + isFree = intent.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, false) + isPointOnly = intent.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_POINT_ONLY, false) + super.onCreate(savedInstanceState) + + bindData() + viewModel.reset() + viewModel.loadAll( + isFree = if (isFree) true else null, + isPointAvailableOnly = if (isPointOnly) true else null + ) + } + + override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + binding.toolbar.tvBack.text = when { + isPointOnly -> "포인트 대여 전체" + isFree -> "무료 콘텐츠 전체" + else -> "콘텐츠 전체보기" + } + binding.toolbar.tvBack.setOnClickListener { finish() } + + setupRecycler() + } + + private fun setupRecycler() { + // 아이템 정사각형 크기 계산: (screenWidth - (24*2) - 16) / 2 + val itemSize = ((screenWidth - 24f.dpToPx() * 2 - 16f.dpToPx()) / 2f).toInt() + + adapter = HomeContentAdapter( + onClickItem = { + startActivity( + Intent(this, AudioContentDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) + } + ) + }, + itemSquareSizePx = itemSize + ) + + val spanCount = 2 + val spacingPx = 16f.dpToPx().toInt() + binding.rvContent.layoutManager = GridLayoutManager(this, spanCount) + binding.rvContent.addItemDecoration( + GridSpacingItemDecoration(spanCount, spacingPx, true) + ) + binding.rvContent.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.loadAll( + isFree = if (isFree) true else null, + isPointAvailableOnly = if (isPointOnly) true else null + ) + } + } + }) + + binding.rvContent.adapter = adapter + } + + private fun bindData() { + viewModel.isLoading.observe(this) { + if (it) loadingDialog.show(screenWidth) else loadingDialog.dismiss() + } + + viewModel.toastLiveData.observe(this) { + it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } + } + + viewModel.itemsLiveData.observe(this) { list -> + if (adapter.itemCount > 0 || list.isNotEmpty()) { + binding.rvContent.visibility = View.VISIBLE + binding.llEmpty.visibility = View.GONE + } else { + binding.rvContent.visibility = View.GONE + binding.llEmpty.visibility = View.VISIBLE + } + adapter.appendItems(list) + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentAllViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentAllViewModel.kt new file mode 100644 index 00000000..2817ab38 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentAllViewModel.kt @@ -0,0 +1,67 @@ +package kr.co.vividnext.sodalive.audio_content.all + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +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.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.home.AudioContentMainItem + +class AudioContentAllViewModel( + private val repository: AudioContentRepository +) : BaseViewModel() { + + private val _isLoading = MutableLiveData(false) + val isLoading: LiveData get() = _isLoading + + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData get() = _toastLiveData + + private val _itemsLiveData = MutableLiveData>() + val itemsLiveData: LiveData> get() = _itemsLiveData + + private var page = 1 + private val size = 20 + private var isLast = false + + fun reset() { + page = 1 + isLast = false + } + + fun loadAll( + isFree: Boolean? = null, + isPointAvailableOnly: Boolean? = null + ) { + if (_isLoading.value == true || isLast) return + _isLoading.value = true + + compositeDisposable.add( + repository.getAllAudioContents( + page = page, + size = size, + isFree = isFree, + isPointAvailableOnly = isPointAvailableOnly, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ response -> + val list = response.data ?: emptyList() + if (list.isNotEmpty()) { + page += 1 + } + if (list.size < size) { + isLast = true + } + _itemsLiveData.postValue(list) + _isLoading.value = false + }, { t -> + _isLoading.value = false + _toastLiveData.postValue(t.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + }) + ) + } +} 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 5752ffb8..b6b5c775 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 @@ -67,6 +67,7 @@ object Constants { const val EXTRA_AUDIO_CONTENT_CHANGE_UI = "audio_content_change_ui" const val EXTRA_AUDIO_CONTENT_PROGRESS = "audio_content_progress" const val EXTRA_AUDIO_CONTENT_DURATION = "audio_content_duration" + const val EXTRA_AUDIO_CONTENT_POINT_ONLY = "audio_content_point_only" 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" 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 4ec1deb8..44346947 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 @@ -301,6 +301,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { viewModel { AudioContentNewAllViewModel(get()) } viewModel { AudioContentAllByThemeViewModel(get()) } viewModel { AudioContentRankingAllViewModel(get()) } + viewModel { kr.co.vividnext.sodalive.audio_content.all.AudioContentAllViewModel(get()) } viewModel { RouletteSettingsViewModel(get()) } viewModel { CreatorCommunityAllViewModel(get(), get()) } viewModel { CreatorCommunityCommentListViewModel(get()) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/home/HomeContentAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/home/HomeContentAdapter.kt index 1709db0d..b122d107 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/home/HomeContentAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/home/HomeContentAdapter.kt @@ -78,4 +78,10 @@ class HomeContentAdapter( this.items.addAll(items) notifyDataSetChanged() } + + @SuppressLint("NotifyDataSetChanged") + fun appendItems(items: List) { + this.items.addAll(items) + notifyDataSetChanged() + } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/home/HomeFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/home/HomeFragment.kt index 42420138..1eb58abe 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/home/HomeFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/home/HomeFragment.kt @@ -1111,6 +1111,18 @@ class HomeFragment : BaseFragment(FragmentHomeBinding::infl }) rvContent.adapter = homeFreeContentAdapter + binding.tvFreeContentAll.setOnClickListener { + if (SharedPreferenceManager.token.isNotBlank()) { + startActivity( + Intent(requireContext(), kr.co.vividnext.sodalive.audio_content.all.AudioContentAllActivity::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, true) + } + ) + } else { + (requireActivity() as MainActivity).showLoginActivity() + } + } + viewModel.freeContentListLiveData.observe(viewLifecycleOwner) { if (it.isNotEmpty()) { binding.llFreeContent.visibility = View.VISIBLE @@ -1184,7 +1196,15 @@ class HomeFragment : BaseFragment(FragmentHomeBinding::infl } binding.tvPointContentAll.setOnClickListener { - // TODO: 전체보기 클릭 액션은 추후에 추가 예정 + if (SharedPreferenceManager.token.isNotBlank()) { + startActivity( + Intent(requireContext(), kr.co.vividnext.sodalive.audio_content.all.AudioContentAllActivity::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_POINT_ONLY, true) + } + ) + } else { + (requireActivity() as MainActivity).showLoginActivity() + } } } diff --git a/app/src/main/res/layout/activity_audio_content_all.xml b/app/src/main/res/layout/activity_audio_content_all.xml new file mode 100644 index 00000000..fde77e98 --- /dev/null +++ b/app/src/main/res/layout/activity_audio_content_all.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 5b462388..044d6eda 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -379,15 +379,32 @@ android:orientation="vertical" android:visibility="gone"> - + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingHorizontal="24dp"> + + + + +