feat: 메인 라이브

- 라이브 다시 듣기 UI 추가
This commit is contained in:
2025-07-18 20:43:30 +09:00
parent 964f697466
commit 2f9bace3de
7 changed files with 163 additions and 4 deletions

View File

@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.audio_content
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.audio_content.all.GetNewContentAllResponse
import kr.co.vividnext.sodalive.audio_content.all.by_theme.GetContentByThemeResponse
@@ -56,6 +57,13 @@ interface AudioContentApi {
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentListResponse>>
@GET("/audio-content/replay-live")
fun getAudioContentReplayLiveList(
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Flowable<ApiResponse<List<GetAudioContentMainItem>>>
@GET("/audio-content/theme")
fun getAudioContentThemeList(
@Header("Authorization") authHeader: String

View File

@@ -50,6 +50,12 @@ class AudioContentRepository(
authHeader = token
)
fun getAudioContentReplayLiveList(token: String) = api.getAudioContentReplayLiveList(
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getAudioContentThemeList(token: String) = api.getAudioContentThemeList(token)
fun uploadAudioContent(

View File

@@ -252,7 +252,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { TermsViewModel(get()) }
viewModel { FindPasswordViewModel(get()) }
viewModel { MainViewModel(get(), get(), get(), get(), get()) }
viewModel { LiveViewModel(get(), get(), get(), get()) }
viewModel { LiveViewModel(get(), get(), get(), get(), get()) }
viewModel { MyPageViewModel(get(), get()) }
viewModel { CanStatusViewModel(get()) }
viewModel { CanChargePgViewModel(get()) }

View File

@@ -15,6 +15,7 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.zhpan.bannerview.BaseBannerAdapter
@@ -22,6 +23,7 @@ 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.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
@@ -34,6 +36,8 @@ import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCo
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.following.FollowingCreatorActivity
import kr.co.vividnext.sodalive.home.AudioContentMainItem
import kr.co.vividnext.sodalive.home.HomeContentAdapter
import kr.co.vividnext.sodalive.live.now.LiveNowAdapter
import kr.co.vividnext.sodalive.live.now.all.LiveNowAllActivity
import kr.co.vividnext.sodalive.live.recommend.RecommendLiveAdapter
@@ -162,6 +166,7 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
setupRecommendLive()
setupRecommendChannel()
setupLatestFinishedLiveChannel()
setupLiveReplay()
setupLiveReservation()
}
@@ -403,6 +408,83 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
}
}
private fun setupLiveReplay() {
val adapter = HomeContentAdapter {
if (SharedPreferenceManager.token.isNotBlank()) {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
} else {
(requireActivity() as MainActivity).showLoginActivity()
}
}
val rvContent = binding.rvReplayLive
rvContent.layoutManager = GridLayoutManager(context, 2, RecyclerView.HORIZONTAL, false)
rvContent.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.top = 8f.dpToPx().toInt()
outRect.bottom = 8f.dpToPx().toInt()
val position = parent.getChildAdapterPosition(view)
if (position == 0 || position == 1) {
outRect.left = 0f.dpToPx().toInt()
} else {
outRect.left = 8f.dpToPx().toInt()
}
outRect.right = 8f.dpToPx().toInt()
}
})
rvContent.adapter = adapter
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
if (adapter.itemCount < 2) {
// 1개일 땐 단일 행
rvContent.layoutManager =
LinearLayoutManager(rvContent.context, RecyclerView.HORIZONTAL, false)
} else {
// 2개 이상일 땐 2행 바둑판
rvContent.layoutManager =
GridLayoutManager(rvContent.context, 2, RecyclerView.HORIZONTAL, false)
}
}
})
viewModel.replayContentListLiveData.observe(viewLifecycleOwner) { contentList ->
if (contentList.isNotEmpty()) {
adapter.addItems(
contentList.map {
AudioContentMainItem(
contentId = it.contentId,
creatorId = it.creatorId,
title = it.title,
coverImageUrl = it.coverImageUrl,
creatorNickname = it.creatorNickname,
isPointAvailable = it.isPointAvailable
)
}
)
binding.llReplayLive.visibility = View.VISIBLE
binding.rvReplayLive.visibility = View.VISIBLE
} else {
binding.llReplayLive.visibility = View.GONE
binding.rvReplayLive.visibility = View.GONE
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun setupLiveNow() {
binding

View File

@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.live
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.live.recommend.GetRecommendLiveResponse
import kr.co.vividnext.sodalive.settings.event.GetEventResponse
@@ -17,5 +18,7 @@ data class LiveSummary(
@SerializedName("recommendLive")
val recommendLive: ApiResponse<List<GetRecommendLiveResponse>>,
@SerializedName("latestFinishedLive")
val latestFinishedLive: ApiResponse<List<GetLatestFinishedLiveResponse>>
val latestFinishedLive: ApiResponse<List<GetLatestFinishedLiveResponse>>,
@SerializedName("replayLive")
val replayLive: ApiResponse<List<GetAudioContentMainItem>>
)

View File

@@ -6,10 +6,13 @@ import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse
import kr.co.vividnext.sodalive.home.AudioContentMainItem
import kr.co.vividnext.sodalive.live.recommend.GetRecommendLiveResponse
import kr.co.vividnext.sodalive.live.recommend.LiveRecommendRepository
import kr.co.vividnext.sodalive.live.recommend_channel.GetRecommendChannelResponse
@@ -26,6 +29,7 @@ import kr.co.vividnext.sodalive.settings.event.EventRepository
class LiveViewModel(
private val repository: LiveRepository,
private val eventRepository: EventRepository,
private val contentRepository: AudioContentRepository,
private val liveRecommendRepository: LiveRecommendRepository,
private val creatorCommunityRepository: CreatorCommunityRepository
) : BaseViewModel() {
@@ -61,6 +65,10 @@ class LiveViewModel(
val communityPostItemLiveData: LiveData<List<GetCommunityPostListResponse>>
get() = _communityPostItemLiveData
private var _replayContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val replayContentListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _replayContentListLiveData
private val _latestFinishedLiveListLiveData =
MutableLiveData<List<GetLatestFinishedLiveResponse>>()
val latestFinishedLiveListLiveData: LiveData<List<GetLatestFinishedLiveResponse>>
@@ -166,6 +174,10 @@ class LiveViewModel(
token = "Bearer ${SharedPreferenceManager.token}"
)
val replayLive = contentRepository.getAudioContentReplayLiveList(
token = "Bearer ${SharedPreferenceManager.token}"
)
_isLoading.postValue(true)
compositeDisposable.add(
@@ -174,8 +186,18 @@ class LiveViewModel(
liveReservation,
event,
recommendLive,
latestFinishedLive
) { t1, t2, t3, t4, t5 -> LiveSummary(t1, t2, t3, t4, t5) }
latestFinishedLive,
replayLive
) { t1, t2, t3, t4, t5, t6 ->
LiveSummary(
liveNow = t1,
liveReservation = t2,
event = t3,
recommendLive = t4,
latestFinishedLive = t5,
replayLive = t6
)
}
.subscribeOn(Schedulers.io())
.subscribe(
{
@@ -245,6 +267,12 @@ class LiveViewModel(
_latestFinishedLiveListLiveData.postValue(emptyList())
}
val replayLive = it.replayLive
if (replayLive.success && replayLive.data != null) {
val data = replayLive.data!!
_replayContentListLiveData.postValue(data)
}
_isLoading.postValue(false)
},
{

View File

@@ -134,6 +134,38 @@
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
<LinearLayout
android:id="@+id/ll_replay_live"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_bold"
android:text="라이브 "
android:textColor="@color/color_3bb9f1"
android:textSize="26sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_bold"
android:text="다시 듣기"
android:textColor="@color/white"
android:textSize="26sp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_replay_live"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="48dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
<include
android:id="@+id/layout_recommend_channel"