diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3f0857c8..eb11f769 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -86,13 +86,15 @@
-
-
-
+
+
+
+
-
+ android:path="/result"
+ android:scheme="${URISCHEME}" />
+
(
+ ActivitySeriesMainBinding::inflate
+) {
+ private var currentTab = 0
+
+ override fun setupView() {
+ binding.toolbar.tvBack.text = "시리즈 전체보기"
+ binding.toolbar.tvBack.setOnClickListener { finish() }
+
+ setupTabs()
+ }
+
+ private fun setupTabs() {
+ binding.tabLayout.addTab(binding.tabLayout.newTab().setText("홈"))
+ binding.tabLayout.addTab(binding.tabLayout.newTab().setText("요일별"))
+ binding.tabLayout.addTab(binding.tabLayout.newTab().setText("장르별"))
+
+ // 탭 선택 리스너 설정
+ binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
+ override fun onTabSelected(tab: TabLayout.Tab) {
+ currentTab = tab.position
+ showTabContent(currentTab)
+ }
+
+ override fun onTabUnselected(tab: TabLayout.Tab) {
+ // 필요한 경우 구현
+ }
+
+ override fun onTabReselected(tab: TabLayout.Tab) {
+ // 필요한 경우 구현
+ }
+ })
+
+ // 초기 탭 선택
+ showTabContent(currentTab)
+ }
+
+ private fun showTabContent(position: Int) {
+ val fragmentTransaction = supportFragmentManager.beginTransaction()
+
+ // 기존 프래그먼트 제거
+ supportFragmentManager.fragments.forEach {
+ fragmentTransaction.remove(it)
+ }
+
+ // 선택된 탭에 따라 프래그먼트 표시
+ val fragment = when (position) {
+ 1 -> SeriesMainDayOfWeekFragment()
+ 2 -> SeriesMainByGenreFragment()
+ else -> SeriesMainHomeFragment()
+ }
+
+ fragmentTransaction.add(R.id.fl_container, fragment)
+ fragmentTransaction.commit()
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/SeriesMainApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/SeriesMainApi.kt
new file mode 100644
index 00000000..b1264f63
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/SeriesMainApi.kt
@@ -0,0 +1,55 @@
+package kr.co.vividnext.sodalive.audio_content.series.main
+
+import io.reactivex.rxjava3.core.Single
+import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
+import kr.co.vividnext.sodalive.audio_content.series.main.by_genre.GetSeriesGenreListResponse
+import kr.co.vividnext.sodalive.audio_content.series.main.home.SeriesHomeResponse
+import kr.co.vividnext.sodalive.common.ApiResponse
+import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
+import kr.co.vividnext.sodalive.settings.ContentType
+import retrofit2.http.GET
+import retrofit2.http.Header
+import retrofit2.http.Query
+
+interface SeriesMainApi {
+ @GET("/audio-content/series/main")
+ fun fetchHome(
+ @Query("isAdultContentVisible") isAdultContentVisible: Boolean,
+ @Query("contentType") contentType: ContentType,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @GET("/audio-content/series/main/recommend")
+ fun getRecommendSeriesList(
+ @Query("isAdultContentVisible") isAdultContentVisible: Boolean,
+ @Query("contentType") contentType: ContentType,
+ @Header("Authorization") authHeader: String
+ ): Single>>
+
+ @GET("/audio-content/series/main/day-of-week")
+ fun getDayOfWeekSeriesList(
+ @Query("dayOfWeek") dayOfWeek: SeriesPublishedDaysOfWeek,
+ @Query("isAdultContentVisible") isAdultContentVisible: Boolean,
+ @Query("contentType") contentType: ContentType,
+ @Query("page") page: Int,
+ @Query("size") size: Int,
+ @Header("Authorization") authHeader: String
+ ): Single>>
+
+ @GET("/audio-content/series/main/genre-list")
+ fun getGenreList(
+ @Query("isAdultContentVisible") isAdultContentVisible: Boolean,
+ @Query("contentType") contentType: ContentType,
+ @Header("Authorization") authHeader: String
+ ): Single>>
+
+ @GET("/audio-content/series/main/list-by-genre")
+ fun getSeriesListByGenre(
+ @Query("genreId") genreId: Long,
+ @Query("isAdultContentVisible") isAdultContentVisible: Boolean,
+ @Query("contentType") contentType: ContentType,
+ @Query("page") page: Int,
+ @Query("size") size: Int,
+ @Header("Authorization") authHeader: String
+ ): Single>
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/SeriesMainRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/SeriesMainRepository.kt
new file mode 100644
index 00000000..baf557d6
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/SeriesMainRepository.kt
@@ -0,0 +1,55 @@
+package kr.co.vividnext.sodalive.audio_content.series.main
+
+import kr.co.vividnext.sodalive.common.SharedPreferenceManager
+import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
+import kr.co.vividnext.sodalive.settings.ContentType
+
+class SeriesMainRepository(
+ private val api: SeriesMainApi
+) {
+ fun fetchData(token: String) = api.fetchHome(
+ isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
+ contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
+ authHeader = token
+ )
+
+ fun getRecommendSeriesList(token: String) = api.getRecommendSeriesList(
+ isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
+ contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
+ authHeader = token
+ )
+
+ fun getDayOfWeekSeriesList(
+ dayOfWeek: SeriesPublishedDaysOfWeek,
+ page: Int,
+ size: Int,
+ token: String
+ ) = api.getDayOfWeekSeriesList(
+ dayOfWeek = dayOfWeek,
+ isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
+ contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
+ page = page - 1,
+ size = size,
+ authHeader = token
+ )
+
+ fun getGenreList(token: String) = api.getGenreList(
+ isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
+ contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
+ authHeader = token
+ )
+
+ fun getSeriesListByGenre(
+ genreId: Long,
+ page: Int,
+ size: Int,
+ token: String
+ ) = api.getSeriesListByGenre(
+ genreId = genreId,
+ isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
+ contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
+ page = page - 1,
+ size = size,
+ authHeader = token
+ )
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/by_genre/GetSeriesGenreListResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/by_genre/GetSeriesGenreListResponse.kt
new file mode 100644
index 00000000..16dd3035
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/by_genre/GetSeriesGenreListResponse.kt
@@ -0,0 +1,10 @@
+package kr.co.vividnext.sodalive.audio_content.series.main.by_genre
+
+import androidx.annotation.Keep
+import com.google.gson.annotations.SerializedName
+
+@Keep
+data class GetSeriesGenreListResponse(
+ @SerializedName("id") val id: Long,
+ @SerializedName("genre") val genre: String
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/by_genre/SeriesMainByGenreFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/by_genre/SeriesMainByGenreFragment.kt
new file mode 100644
index 00000000..6119b0f2
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/by_genre/SeriesMainByGenreFragment.kt
@@ -0,0 +1,9 @@
+package kr.co.vividnext.sodalive.audio_content.series.main.by_genre
+
+import kr.co.vividnext.sodalive.base.BaseFragment
+import kr.co.vividnext.sodalive.databinding.FragmentSeriesMainByGenreBinding
+
+class SeriesMainByGenreFragment : BaseFragment(
+ FragmentSeriesMainByGenreBinding::inflate
+) {
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/by_genre/SeriesMainByGenreViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/by_genre/SeriesMainByGenreViewModel.kt
new file mode 100644
index 00000000..7c1a2521
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/by_genre/SeriesMainByGenreViewModel.kt
@@ -0,0 +1,18 @@
+package kr.co.vividnext.sodalive.audio_content.series.main.by_genre
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import kr.co.vividnext.sodalive.audio_content.series.main.SeriesMainRepository
+import kr.co.vividnext.sodalive.base.BaseViewModel
+
+class SeriesMainByGenreViewModel(
+ private val repository: SeriesMainRepository
+) : BaseViewModel() {
+ private var _isLoading = MutableLiveData(false)
+ val isLoading: LiveData
+ get() = _isLoading
+
+ private val _toastLiveData = MutableLiveData()
+ val toastLiveData: LiveData
+ get() = _toastLiveData
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/day_of_week/SeriesMainDayOfWeekFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/day_of_week/SeriesMainDayOfWeekFragment.kt
new file mode 100644
index 00000000..9bc206f2
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/day_of_week/SeriesMainDayOfWeekFragment.kt
@@ -0,0 +1,172 @@
+package kr.co.vividnext.sodalive.audio_content.series.main.day_of_week
+
+import android.content.Intent
+import android.graphics.Rect
+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.series.SeriesListAdapter
+import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
+import kr.co.vividnext.sodalive.base.BaseFragment
+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.FragmentSeriesMainDayOfWeekBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+import kr.co.vividnext.sodalive.home.DayOfWeekAdapter
+import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
+import org.koin.android.ext.android.inject
+import java.util.Calendar
+import kotlin.math.roundToInt
+
+class SeriesMainDayOfWeekFragment : BaseFragment(
+ FragmentSeriesMainDayOfWeekBinding::inflate
+) {
+ private val viewModel: SeriesMainDayOfWeekViewModel by inject()
+
+ private lateinit var loadingDialog: LoadingDialog
+ private lateinit var adapter: SeriesListAdapter
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setupView()
+ observeViewModel()
+ }
+
+ private fun setupView() {
+ loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
+
+ setupDayOfWeekDay()
+ setupSeriesView()
+
+ val dayOfWeeks = listOf(
+ SeriesPublishedDaysOfWeek.RANDOM,
+ SeriesPublishedDaysOfWeek.SUN,
+ SeriesPublishedDaysOfWeek.MON,
+ SeriesPublishedDaysOfWeek.TUE,
+ SeriesPublishedDaysOfWeek.WED,
+ SeriesPublishedDaysOfWeek.THU,
+ SeriesPublishedDaysOfWeek.FRI,
+ SeriesPublishedDaysOfWeek.SAT
+ )
+
+ val calendar = Calendar.getInstance()
+ val dayIndex = calendar.get(Calendar.DAY_OF_WEEK)
+
+ viewModel.dayOfWeek = dayOfWeeks[dayIndex]
+ }
+
+ private fun setupDayOfWeekDay() {
+ val dayOfWeekAdapter = DayOfWeekAdapter(screenWidth = screenWidth) {
+ adapter.clear()
+ viewModel.dayOfWeek = it
+ }
+
+ val rvDayOfWeek = binding.rvSeriesDayOfWeekDay
+ val layoutManager = object : LinearLayoutManager(
+ context,
+ HORIZONTAL,
+ false
+ ) {
+ override fun canScrollVertically() = false
+ override fun canScrollHorizontally() = false
+ }
+ rvDayOfWeek.layoutManager = layoutManager
+
+ rvDayOfWeek.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.left = 0
+ outRect.right = 2.5f.dpToPx().toInt()
+ }
+
+ dayOfWeekAdapter.itemCount - 1 -> {
+ outRect.left = 2.5f.dpToPx().toInt()
+ outRect.right = 0
+ }
+
+ else -> {
+ outRect.left = 2.5f.dpToPx().toInt()
+ outRect.right = 2.5f.dpToPx().toInt()
+ }
+ }
+ }
+ })
+ rvDayOfWeek.adapter = dayOfWeekAdapter
+ }
+
+ private fun setupSeriesView() {
+ adapter = SeriesListAdapter(
+ itemWidth = ((screenWidth - 24f.dpToPx() * 2 - 16f.dpToPx()) / 2f).roundToInt(),
+ onClickItem = {
+ startActivity(
+ Intent(
+ requireContext(),
+ SeriesDetailActivity::class.java
+ ).apply {
+ putExtra(Constants.EXTRA_SERIES_ID, it)
+ }
+ )
+ },
+ onClickCreator = {},
+ isVisibleCreator = false
+ )
+
+ val spanCount = 2
+ val spacingPx = 16f.dpToPx().toInt()
+ val recyclerView = binding.rvSeriesDayOfWeek
+ recyclerView.layoutManager = GridLayoutManager(requireContext(), spanCount)
+
+ recyclerView.addItemDecoration(
+ GridSpacingItemDecoration(spanCount, spacingPx, false)
+ )
+
+ 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.getSeriesList()
+ }
+ }
+ })
+
+ recyclerView.adapter = adapter
+
+ viewModel.seriesListLiveData.observe(viewLifecycleOwner) {
+ adapter.addItems(it)
+ }
+ }
+
+ private fun observeViewModel() {
+ viewModel.isLoading.observe(viewLifecycleOwner) {
+ if (it) {
+ loadingDialog.show(screenWidth)
+ } else {
+ loadingDialog.dismiss()
+ }
+ }
+
+ viewModel.toastLiveData.observe(viewLifecycleOwner) {
+ it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() }
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/day_of_week/SeriesMainDayOfWeekViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/day_of_week/SeriesMainDayOfWeekViewModel.kt
new file mode 100644
index 00000000..032a0528
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/day_of_week/SeriesMainDayOfWeekViewModel.kt
@@ -0,0 +1,85 @@
+package kr.co.vividnext.sodalive.audio_content.series.main.day_of_week
+
+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.series.GetSeriesListResponse
+import kr.co.vividnext.sodalive.audio_content.series.main.SeriesMainRepository
+import kr.co.vividnext.sodalive.base.BaseViewModel
+import kr.co.vividnext.sodalive.common.SharedPreferenceManager
+import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
+
+class SeriesMainDayOfWeekViewModel(
+ private val repository: SeriesMainRepository
+) : BaseViewModel() {
+ private var _isLoading = MutableLiveData(false)
+ val isLoading: LiveData
+ get() = _isLoading
+
+ private val _toastLiveData = MutableLiveData()
+ val toastLiveData: LiveData
+ get() = _toastLiveData
+
+ private val _seriesListLiveData = MutableLiveData>()
+ val seriesListLiveData: LiveData>
+ get() = _seriesListLiveData
+
+ private var page = 1
+ private var isLast = false
+ private val pageSize = 20
+
+ var dayOfWeek = SeriesPublishedDaysOfWeek.RANDOM
+ set(newValue) {
+ if (field != newValue) {
+ page = 1
+ isLast = false
+ field = newValue
+ getSeriesList()
+ }
+ }
+
+ fun getSeriesList() {
+ if (isLast) return
+ _isLoading.value = true
+
+ compositeDisposable.add(
+ repository.getDayOfWeekSeriesList(
+ dayOfWeek = dayOfWeek,
+ page = page,
+ size = pageSize,
+ token = "Bearer ${SharedPreferenceManager.token}"
+ )
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ _isLoading.value = false
+
+ val data = it.data
+ if (it.success && data != null) {
+ page += 1
+
+ if (data.isNotEmpty()) {
+ _seriesListLiveData.value = data
+ } else {
+ isLast = true
+ }
+ } else {
+ if (it.message != null) {
+ _toastLiveData.value = it.message
+ } else {
+ _toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ }
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ }
+ )
+ )
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/home/SeriesBannerAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/home/SeriesBannerAdapter.kt
new file mode 100644
index 00000000..2e50562a
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/home/SeriesBannerAdapter.kt
@@ -0,0 +1,53 @@
+package kr.co.vividnext.sodalive.audio_content.series.main.home
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.widget.FrameLayout
+import android.widget.ImageView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.target.CustomTarget
+import com.bumptech.glide.request.transition.Transition
+import com.zhpan.bannerview.BaseBannerAdapter
+import com.zhpan.bannerview.BaseViewHolder
+import kr.co.vividnext.sodalive.R
+
+class SeriesBannerAdapter(
+ private val context: Context,
+ private val itemWidth: Int,
+ private val itemHeight: Int,
+ private val onClick: (SeriesBannerResponse) -> Unit
+) : BaseBannerAdapter() {
+ override fun bindData(
+ holder: BaseViewHolder,
+ data: SeriesBannerResponse,
+ position: Int,
+ pageSize: Int
+ ) {
+ val ivBanner = holder.findViewById(R.id.iv_recommend_live)
+ val layoutParams = ivBanner.layoutParams as FrameLayout.LayoutParams
+
+ layoutParams.width = itemWidth
+ layoutParams.height = itemHeight
+
+ Glide
+ .with(context)
+ .asBitmap()
+ .load(data.imagePath)
+ .into(object : CustomTarget() {
+ override fun onResourceReady(resource: Bitmap, transition: Transition?) {
+ ivBanner.setImageBitmap(resource)
+ ivBanner.layoutParams = layoutParams
+ }
+
+ override fun onLoadCleared(placeholder: Drawable?) {
+ }
+ })
+
+ ivBanner.setOnClickListener { onClick(data) }
+ }
+
+ override fun getLayoutId(viewType: Int): Int {
+ return R.layout.item_recommend_live
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/home/SeriesHomeResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/home/SeriesHomeResponse.kt
new file mode 100644
index 00000000..f6ac770b
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/home/SeriesHomeResponse.kt
@@ -0,0 +1,23 @@
+package kr.co.vividnext.sodalive.audio_content.series.main.home
+
+import androidx.annotation.Keep
+import com.google.gson.annotations.SerializedName
+import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
+
+@Keep
+data class SeriesHomeResponse(
+ @SerializedName("banners")
+ val banners: List,
+
+ @SerializedName("completedSeriesList")
+ val completedSeriesList: List,
+
+ @SerializedName("recommendSeriesList")
+ val recommendSeriesList: List
+)
+
+@Keep
+data class SeriesBannerResponse(
+ @SerializedName("seriesId") val seriesId: Long,
+ @SerializedName("imagePath") val imagePath: String
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/home/SeriesMainHomeFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/home/SeriesMainHomeFragment.kt
new file mode 100644
index 00000000..ee8371ce
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/home/SeriesMainHomeFragment.kt
@@ -0,0 +1,233 @@
+package kr.co.vividnext.sodalive.audio_content.series.main.home
+
+import android.content.Intent
+import android.graphics.Rect
+import android.os.Bundle
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.Toast
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.zhpan.bannerview.BaseBannerAdapter
+import com.zhpan.indicator.enums.IndicatorSlideMode
+import com.zhpan.indicator.enums.IndicatorStyle
+import kr.co.vividnext.sodalive.R
+import kr.co.vividnext.sodalive.audio_content.series.SeriesListAdapter
+import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
+import kr.co.vividnext.sodalive.base.BaseFragment
+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.common.SharedPreferenceManager
+import kr.co.vividnext.sodalive.databinding.FragmentSeriesMainHomeBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+import kr.co.vividnext.sodalive.home.HomeSeriesAdapter
+import kr.co.vividnext.sodalive.main.MainActivity
+import org.koin.android.ext.android.inject
+import kotlin.math.roundToInt
+
+class SeriesMainHomeFragment : BaseFragment(
+ FragmentSeriesMainHomeBinding::inflate
+) {
+ private val viewModel: SeriesMainHomeViewModel by inject()
+
+ private lateinit var loadingDialog: LoadingDialog
+
+ private lateinit var bannerAdapter: SeriesBannerAdapter
+ private lateinit var completedAdapter: HomeSeriesAdapter
+ private lateinit var recommendAdapter: SeriesListAdapter
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setupView()
+ observeViewModel()
+
+ viewModel.fetchData()
+ }
+
+ private fun setupView() {
+ loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
+
+ setupBanner()
+ setupCompletedSeriesView()
+ setupRecommendSeriesView()
+ }
+
+ private fun setupBanner() {
+ val layoutParams = binding
+ .bannerSlider
+ .layoutParams as LinearLayout.LayoutParams
+
+ val pagerWidth = screenWidth
+ val pagerHeight = pagerWidth * 198 / 352
+ layoutParams.width = pagerWidth
+ layoutParams.height = pagerHeight
+
+ bannerAdapter = SeriesBannerAdapter(
+ requireContext(),
+ pagerWidth,
+ pagerHeight
+ ) {
+ startActivity(
+ Intent(
+ requireContext(),
+ SeriesDetailActivity::class.java
+ ).apply {
+ putExtra(Constants.EXTRA_SERIES_ID, it.seriesId)
+ }
+ )
+ }
+
+ binding
+ .bannerSlider
+ .layoutParams = layoutParams
+
+ binding.bannerSlider.apply {
+ adapter = bannerAdapter as BaseBannerAdapter
+
+ setLifecycleRegistry(lifecycle)
+ setScrollDuration(1000)
+ setInterval(4 * 1000)
+ }.create()
+
+ binding
+ .bannerSlider
+ .setIndicatorView(binding.indicatorBanner)
+ .setIndicatorStyle(IndicatorStyle.ROUND_RECT)
+ .setIndicatorSlideMode(IndicatorSlideMode.SMOOTH)
+ .setIndicatorVisibility(View.GONE)
+ .setIndicatorSliderColor(
+ ContextCompat.getColor(requireContext(), R.color.color_909090),
+ ContextCompat.getColor(requireContext(), R.color.color_3bb9f1)
+ )
+ .setIndicatorSliderWidth(10f.dpToPx().toInt(), 10f.dpToPx().toInt())
+ .setIndicatorHeight(10f.dpToPx().toInt())
+
+ viewModel.bannerListLiveData.observe(viewLifecycleOwner) {
+ if (it.isNotEmpty()) {
+ binding.llBanner.visibility = View.VISIBLE
+ binding.bannerSlider.refreshData(it)
+ } else {
+ binding.llBanner.visibility = View.GONE
+ }
+ }
+ }
+
+ private fun setupCompletedSeriesView() {
+ completedAdapter = HomeSeriesAdapter {
+ if (SharedPreferenceManager.token.isNotBlank()) {
+ startActivity(
+ Intent(requireContext(), SeriesDetailActivity::class.java).apply {
+ putExtra(Constants.EXTRA_SERIES_ID, it)
+ }
+ )
+ } else {
+ (requireActivity() as MainActivity).showLoginActivity()
+ }
+ }
+
+ val recyclerView = binding.rvCompletedSeries
+ recyclerView.layoutManager = LinearLayoutManager(
+ context,
+ LinearLayoutManager.HORIZONTAL,
+ false
+ )
+
+ recyclerView.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.left = 0
+ outRect.right = 8f.dpToPx().toInt()
+ }
+
+ completedAdapter.itemCount - 1 -> {
+ outRect.left = 8f.dpToPx().toInt()
+ outRect.right = 0
+ }
+
+ else -> {
+ outRect.left = 8f.dpToPx().toInt()
+ outRect.right = 8f.dpToPx().toInt()
+ }
+ }
+ }
+ })
+ recyclerView.adapter = completedAdapter
+
+ viewModel.completedSeriesLiveData.observe(viewLifecycleOwner) {
+ if (it.isNotEmpty()) {
+ binding.llCompletedSeries.visibility = View.VISIBLE
+ completedAdapter.addItems(it)
+ } else {
+ binding.llCompletedSeries.visibility = View.GONE
+ }
+ }
+ }
+
+ private fun setupRecommendSeriesView() {
+ recommendAdapter = SeriesListAdapter(
+ itemWidth = ((screenWidth - 24f.dpToPx() * 2 - 16f.dpToPx()) / 2f).roundToInt(),
+ onClickItem = {
+ startActivity(
+ Intent(
+ requireContext(),
+ SeriesDetailActivity::class.java
+ ).apply {
+ putExtra(Constants.EXTRA_SERIES_ID, it)
+ }
+ )
+ },
+ onClickCreator = {},
+ isVisibleCreator = false
+ )
+
+ val spanCount = 2
+ val spacingPx = 16f.dpToPx().toInt()
+ val recyclerView = binding.rvRecommendSeries
+ recyclerView.layoutManager = GridLayoutManager(requireContext(), spanCount)
+
+ recyclerView.addItemDecoration(
+ GridSpacingItemDecoration(spanCount, spacingPx, false)
+ )
+
+ recyclerView.adapter = recommendAdapter
+
+ binding.ivRecommendRefresh.setOnClickListener {
+ viewModel.getRecommendSeriesList()
+ }
+
+ viewModel.recommendSeriesLiveData.observe(viewLifecycleOwner) {
+ if (it.isNotEmpty()) {
+ binding.llRecommendSeries.visibility = View.VISIBLE
+ recommendAdapter.clear()
+ recommendAdapter.addItems(it)
+ } else {
+ binding.llRecommendSeries.visibility = View.GONE
+ }
+ }
+ }
+
+ private fun observeViewModel() {
+ viewModel.isLoading.observe(viewLifecycleOwner) {
+ if (it) {
+ loadingDialog.show(screenWidth)
+ } else {
+ loadingDialog.dismiss()
+ }
+ }
+
+ viewModel.toastLiveData.observe(viewLifecycleOwner) {
+ it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() }
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/home/SeriesMainHomeViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/home/SeriesMainHomeViewModel.kt
new file mode 100644
index 00000000..fd585ce4
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/main/home/SeriesMainHomeViewModel.kt
@@ -0,0 +1,101 @@
+package kr.co.vividnext.sodalive.audio_content.series.main.home
+
+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.series.GetSeriesListResponse
+import kr.co.vividnext.sodalive.audio_content.series.main.SeriesMainRepository
+import kr.co.vividnext.sodalive.base.BaseViewModel
+import kr.co.vividnext.sodalive.common.SharedPreferenceManager
+
+class SeriesMainHomeViewModel(
+ private val repository: SeriesMainRepository
+) : BaseViewModel() {
+ private var _isLoading = MutableLiveData(false)
+ val isLoading: LiveData
+ get() = _isLoading
+
+ private val _toastLiveData = MutableLiveData()
+ val toastLiveData: LiveData
+ get() = _toastLiveData
+
+ private var _bannerListLiveData = MutableLiveData>()
+ val bannerListLiveData: LiveData>
+ get() = _bannerListLiveData
+
+ private var _completedSeriesLiveData =
+ MutableLiveData>()
+ val completedSeriesLiveData: LiveData>
+ get() = _completedSeriesLiveData
+
+ private var _recommendSeriesLiveData =
+ MutableLiveData>()
+ val recommendSeriesLiveData: LiveData>
+ get() = _recommendSeriesLiveData
+
+ fun fetchData() {
+ _isLoading.value = true
+
+ compositeDisposable.add(
+ repository.fetchData(token = "Bearer ${SharedPreferenceManager.token}")
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ _isLoading.value = false
+
+ val data = it.data
+ if (it.success && data != null) {
+ _bannerListLiveData.value = data.banners
+ _completedSeriesLiveData.value = data.completedSeriesList
+ _recommendSeriesLiveData.value = data.recommendSeriesList
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+
+ fun getRecommendSeriesList() {
+ _isLoading.value = true
+
+ compositeDisposable.add(
+ repository.getRecommendSeriesList(token = "Bearer ${SharedPreferenceManager.token}")
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ _isLoading.value = false
+ if (it.success && it.data != null) {
+ _recommendSeriesLiveData.value = it.data
+ } else {
+ if (it.message != null) {
+ _toastLiveData.value = it.message
+ } else {
+ _toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ }
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ }
+ )
+ )
+ }
+}
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 44346947..5f8ed03c 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
@@ -32,6 +32,11 @@ import kr.co.vividnext.sodalive.audio_content.series.SeriesListAllViewModel
import kr.co.vividnext.sodalive.audio_content.series.SeriesRepository
import kr.co.vividnext.sodalive.audio_content.series.content.SeriesContentAllViewModel
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailViewModel
+import kr.co.vividnext.sodalive.audio_content.series.main.SeriesMainApi
+import kr.co.vividnext.sodalive.audio_content.series.main.SeriesMainRepository
+import kr.co.vividnext.sodalive.audio_content.series.main.by_genre.SeriesMainByGenreViewModel
+import kr.co.vividnext.sodalive.audio_content.series.main.day_of_week.SeriesMainDayOfWeekViewModel
+import kr.co.vividnext.sodalive.audio_content.series.main.home.SeriesMainHomeViewModel
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadViewModel
import kr.co.vividnext.sodalive.audio_content.upload.theme.AudioContentThemeViewModel
import kr.co.vividnext.sodalive.audition.AuditionApi
@@ -224,6 +229,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
single { ApiBuilder().build(get(), TermsApi::class.java) }
single { ApiBuilder().build(get(), EventApi::class.java) }
single { ApiBuilder().build(get(), SeriesApi::class.java) }
+ single { ApiBuilder().build(get(), SeriesMainApi::class.java) }
single { ApiBuilder().build(get(), ReportApi::class.java) }
single { ApiBuilder().build(get(), LiveRecommendApi::class.java) }
single { ApiBuilder().build(get(), ExplorerApi::class.java) }
@@ -333,6 +339,9 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { NewCharactersAllViewModel(get()) }
viewModel { OriginalWorkViewModel(get()) }
viewModel { OriginalWorkDetailViewModel(get()) }
+ viewModel { SeriesMainHomeViewModel(get()) }
+ viewModel { SeriesMainByGenreViewModel(get()) }
+ viewModel { SeriesMainDayOfWeekViewModel(get()) }
}
private val repositoryModule = module {
@@ -376,6 +385,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
factory { CharacterCommentRepository(get()) }
factory { NewCharactersRepository(get()) }
factory { OriginalWorkRepository(get()) }
+ factory { SeriesMainRepository(get()) }
}
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 5879abae..99bf6122 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
@@ -26,13 +26,16 @@ import com.zhpan.indicator.enums.IndicatorSlideMode
import com.zhpan.indicator.enums.IndicatorStyle
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
+import kr.co.vividnext.sodalive.audio_content.all.AudioContentAllActivity
import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllActivity
import kr.co.vividnext.sodalive.audio_content.box.AudioContentBoxActivity
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
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.player.AudioContentPlayerService
+import kr.co.vividnext.sodalive.audio_content.series.SeriesListAllActivity
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
+import kr.co.vividnext.sodalive.audio_content.series.main.SeriesMainActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
import kr.co.vividnext.sodalive.audition.AuditionActivity
import kr.co.vividnext.sodalive.base.BaseFragment
@@ -646,7 +649,10 @@ class HomeFragment : BaseFragment(FragmentHomeBinding::infl
// ‘오직 보이스온에서만’ 전체보기: isOriginal=true로 시리즈 전체보기 화면 진입
if (SharedPreferenceManager.token.isNotBlank()) {
startActivity(
- Intent(requireContext(), kr.co.vividnext.sodalive.audio_content.series.SeriesListAllActivity::class.java).apply {
+ Intent(
+ requireContext(),
+ SeriesListAllActivity::class.java
+ ).apply {
putExtra(kr.co.vividnext.sodalive.common.Constants.EXTRA_IS_ORIGINAL, true)
}
)
@@ -791,7 +797,7 @@ class HomeFragment : BaseFragment(FragmentHomeBinding::infl
outRect.right = 2.5f.dpToPx().toInt()
}
- seriesDayOfWeekAdapter.itemCount - 1 -> {
+ dayOfWeekAdapter.itemCount - 1 -> {
outRect.left = 2.5f.dpToPx().toInt()
outRect.right = 0
}
@@ -804,6 +810,21 @@ class HomeFragment : BaseFragment(FragmentHomeBinding::infl
}
})
rvDayOfWeek.adapter = dayOfWeekAdapter
+
+ binding.tvSeriesDayOfWeekAll.setOnClickListener {
+ if (SharedPreferenceManager.token.isNotBlank()) {
+ startActivity(
+ Intent(
+ requireContext(),
+ SeriesMainActivity::class.java
+ ).apply {
+ putExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, true)
+ }
+ )
+ } else {
+ (requireActivity() as MainActivity).showLoginActivity()
+ }
+ }
}
private fun setupPopularCharacters() {
@@ -1127,7 +1148,7 @@ class HomeFragment : BaseFragment(FragmentHomeBinding::infl
binding.tvFreeContentAll.setOnClickListener {
if (SharedPreferenceManager.token.isNotBlank()) {
startActivity(
- Intent(requireContext(), kr.co.vividnext.sodalive.audio_content.all.AudioContentAllActivity::class.java).apply {
+ Intent(requireContext(), AudioContentAllActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, true)
}
)
@@ -1211,7 +1232,10 @@ class HomeFragment : BaseFragment(FragmentHomeBinding::infl
binding.tvPointContentAll.setOnClickListener {
if (SharedPreferenceManager.token.isNotBlank()) {
startActivity(
- Intent(requireContext(), kr.co.vividnext.sodalive.audio_content.all.AudioContentAllActivity::class.java).apply {
+ Intent(
+ requireContext(),
+ AudioContentAllActivity::class.java
+ ).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_POINT_ONLY, true)
}
)
@@ -1229,7 +1253,10 @@ class HomeFragment : BaseFragment(FragmentHomeBinding::infl
recommendContentAdapter = HomeContentAdapter(onClickItem = {
if (SharedPreferenceManager.token.isNotBlank()) {
startActivity(
- Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
+ Intent(
+ requireContext(),
+ AudioContentDetailActivity::class.java
+ ).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
diff --git a/app/src/main/res/layout/activity_series_main.xml b/app/src/main/res/layout/activity_series_main.xml
new file mode 100644
index 00000000..54dce6cf
--- /dev/null
+++ b/app/src/main/res/layout/activity_series_main.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index 5e5d1a8b..ef32e611 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -253,15 +253,32 @@
android:layout_marginBottom="48dp"
android:orientation="vertical">
-
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:paddingHorizontal="24dp">
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_series_main_day_of_week.xml b/app/src/main/res/layout/fragment_series_main_day_of_week.xml
new file mode 100644
index 00000000..4dc82d80
--- /dev/null
+++ b/app/src/main/res/layout/fragment_series_main_day_of_week.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_series_main_home.xml b/app/src/main/res/layout/fragment_series_main_home.xml
new file mode 100644
index 00000000..91729c6f
--- /dev/null
+++ b/app/src/main/res/layout/fragment_series_main_home.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+