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">
+
+
+
+
+