diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c3ff5e7..bd5338e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -154,6 +154,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 6989bef..061e8c8 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
@@ -283,6 +283,13 @@ interface AudioContentApi {
@Header("Authorization") authHeader: String
): Single>>
+ @GET("/v2/audio-content/main/series/completed-monthly-rank")
+ fun getCompletedSeries(
+ @Query("page") page: Int,
+ @Query("size") size: Int,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
@GET("/v2/audio-content/main/content")
fun getContentMainContent(
@Header("Authorization") authHeader: String
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/series/AudioContentMainTabSeriesFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/series/AudioContentMainTabSeriesFragment.kt
index 4283399..657988a 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/series/AudioContentMainTabSeriesFragment.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/series/AudioContentMainTabSeriesFragment.kt
@@ -18,6 +18,7 @@ import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.main.AudioContentBannerType
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.ContentRankCreatorAdapter
+import kr.co.vividnext.sodalive.audio_content.main.v2.series.completed.CompletedSeriesActivity
import kr.co.vividnext.sodalive.audio_content.main.v2.series.curation.AudioContentMainSeriesCurationAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.series.new_series.AudioContentMainNewSeriesAdapter
import kr.co.vividnext.sodalive.audio_content.main.v2.series.origianl_audio_drama.AudioContentMainTabSeriesOriginalAudioDramaAdapter
@@ -456,6 +457,15 @@ class AudioContentMainTabSeriesFragment : BaseFragment(
+ ActivityCompletedSeriesBinding::inflate
+) {
+
+ private val viewModel: CompletedSeriesViewModel by inject()
+
+ private lateinit var loadingDialog: LoadingDialog
+ private lateinit var adapter: SeriesListAdapter
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ bindData()
+
+ viewModel.getCompletedSeries()
+ }
+
+ override fun setupView() {
+ loadingDialog = LoadingDialog(this, layoutInflater)
+ binding.toolbar.tvBack.text = "완결 시리즈"
+ binding.toolbar.tvBack.setOnClickListener { finish() }
+
+ setupCompletedSeriesListView()
+ }
+
+ private fun setupCompletedSeriesListView() {
+ val spacing = 13.3f.dpToPx().roundToInt()
+
+ adapter = SeriesListAdapter(
+ itemWidth = ((screenWidth - spacing * 3) / 2f).roundToInt(),
+ onClickItem = {
+ startActivity(
+ Intent(applicationContext, SeriesDetailActivity::class.java).apply {
+ putExtra(Constants.EXTRA_SERIES_ID, it)
+ }
+ )
+ },
+ onClickCreator = {},
+ isVisibleCreator = false
+ )
+
+ val spanCount = 2
+ val recyclerView = binding.rvSeries
+ recyclerView.layoutManager = GridLayoutManager(this, spanCount)
+
+ recyclerView.addItemDecoration(
+ DifferentSpacingItemDecoration(
+ spanCount = spanCount,
+ horizontalSpacing = spacing,
+ verticalSpacing = spacing,
+ includeEdge = true
+ )
+ )
+
+ recyclerView.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.getCompletedSeries()
+ }
+ }
+ })
+
+ recyclerView.adapter = adapter
+ }
+
+ private fun bindData() {
+ viewModel.toastLiveData.observe(this) {
+ it?.let { showToast(it) }
+ }
+
+ viewModel.isLoading.observe(this) {
+ if (it) {
+ loadingDialog.show(screenWidth)
+ } else {
+ loadingDialog.dismiss()
+ }
+ }
+
+ viewModel.completedSeriesLiveData.observe(this) {
+ adapter.addItems(it)
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/series/completed/CompletedSeriesViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/series/completed/CompletedSeriesViewModel.kt
new file mode 100644
index 0000000..f59fd80
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/series/completed/CompletedSeriesViewModel.kt
@@ -0,0 +1,75 @@
+package kr.co.vividnext.sodalive.audio_content.main.v2.series.completed
+
+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.main.v2.series.AudioContentMainTabSeriesRepository
+import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
+import kr.co.vividnext.sodalive.base.BaseViewModel
+import kr.co.vividnext.sodalive.common.SharedPreferenceManager
+
+class CompletedSeriesViewModel(
+ private val repository: AudioContentMainTabSeriesRepository
+) : BaseViewModel() {
+ private val _toastLiveData = MutableLiveData()
+ val toastLiveData: LiveData
+ get() = _toastLiveData
+
+ private var _isLoading = MutableLiveData(false)
+ val isLoading: LiveData
+ get() = _isLoading
+
+ private var _completedSeriesLiveData =
+ MutableLiveData>()
+ val completedSeriesLiveData: LiveData>
+ get() = _completedSeriesLiveData
+
+ var isLast = false
+ var page = 1
+ private val size = 20
+
+ fun getCompletedSeries() {
+ if (!_isLoading.value!! && !isLast) {
+ _isLoading.value = true
+
+ compositeDisposable.add(
+ repository.getCompletedSeries(
+ page = page,
+ size = size,
+ token = "Bearer ${SharedPreferenceManager.token}"
+ )
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.success && it.data != null) {
+ page += 1
+
+ if (it.data.items.isNotEmpty()) {
+ _completedSeriesLiveData.value = it.data.items
+ } else {
+ isLast = true
+ }
+ } else {
+ if (it.message != null) {
+ _toastLiveData.value = it.message
+ } else {
+ _toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+
+ }
+ }
+
+ _isLoading.value = false
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+ }
+}
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 112e705..88b053c 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
@@ -38,6 +38,7 @@ import kr.co.vividnext.sodalive.audio_content.main.v2.replay.AudioContentMainTab
import kr.co.vividnext.sodalive.audio_content.main.v2.replay.AudioContentMainTabReplayViewModel
import kr.co.vividnext.sodalive.audio_content.main.v2.series.AudioContentMainTabSeriesRepository
import kr.co.vividnext.sodalive.audio_content.main.v2.series.AudioContentMainTabSeriesViewModel
+import kr.co.vividnext.sodalive.audio_content.main.v2.series.completed.CompletedSeriesViewModel
import kr.co.vividnext.sodalive.audio_content.main.v2.series.origianl_audio_drama.OriginalAudioDramaContentAllRepository
import kr.co.vividnext.sodalive.audio_content.main.v2.series.origianl_audio_drama.OriginalAudioDramaContentAllViewModel
import kr.co.vividnext.sodalive.audio_content.modify.AudioContentModifyViewModel
@@ -316,6 +317,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { AudioContentMainTabFreeViewModel(get()) }
viewModel { OriginalAudioDramaContentAllViewModel(get()) }
viewModel { IntroduceCreatorViewModel(get()) }
+ viewModel { CompletedSeriesViewModel(get()) }
}
private val repositoryModule = module {
diff --git a/app/src/main/res/layout/activity_completed_series.xml b/app/src/main/res/layout/activity_completed_series.xml
new file mode 100644
index 0000000..b0f2ea1
--- /dev/null
+++ b/app/src/main/res/layout/activity_completed_series.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+