feat: 메인 홈

- 라이브 UI 추가
This commit is contained in:
2025-07-15 05:04:21 +09:00
parent e3121fc49b
commit 388770889f
30 changed files with 1229 additions and 2 deletions

View File

@@ -81,6 +81,9 @@ import kr.co.vividnext.sodalive.explorer.profile.fantalk.UserProfileFantalkAllVi
import kr.co.vividnext.sodalive.explorer.profile.follow.UserFollowerListViewModel
import kr.co.vividnext.sodalive.following.FollowingCreatorRepository
import kr.co.vividnext.sodalive.following.FollowingCreatorViewModel
import kr.co.vividnext.sodalive.home.HomeApi
import kr.co.vividnext.sodalive.home.HomeRepository
import kr.co.vividnext.sodalive.home.HomeViewModel
import kr.co.vividnext.sodalive.live.LiveApi
import kr.co.vividnext.sodalive.live.LiveRepository
import kr.co.vividnext.sodalive.live.LiveViewModel
@@ -240,6 +243,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
single { ApiBuilder().build(get(), AdTrackingApi::class.java) }
single { ApiBuilder().build(get(), SearchApi::class.java) }
single { ApiBuilder().build(get(), PointStatusApi::class.java) }
single { ApiBuilder().build(get(), HomeApi::class.java) }
}
private val viewModelModule = module {
@@ -336,6 +340,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { AlarmContentAllViewModel(get()) }
viewModel { SearchViewModel(get()) }
viewModel { PointStatusViewModel(get()) }
viewModel { HomeViewModel(get()) }
}
private val repositoryModule = module {
@@ -379,6 +384,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
factory { SearchRepository(get()) }
factory { UserEventRepository(get()) }
factory { PointStatusRepository(get()) }
factory { HomeRepository(get()) }
}
private val moduleList = listOf(

View File

@@ -0,0 +1,14 @@
package kr.co.vividnext.sodalive.home
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class AudioContentMainItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("title") val title: String,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("isPointAvailable") val isPointAvailable: Boolean
)

View File

@@ -0,0 +1,29 @@
package kr.co.vividnext.sodalive.home
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.audio_content.main.v2.GetContentCurationResponse
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.audition.GetAuditionListItem
import kr.co.vividnext.sodalive.explorer.GetExplorerSectionCreatorResponse
import kr.co.vividnext.sodalive.live.GetRoomListResponse
import kr.co.vividnext.sodalive.settings.event.GetEventResponse
@Keep
data class GetHomeResponse(
@SerializedName("liveList") val liveList: List<GetRoomListResponse>,
@SerializedName("creatorRanking") val creatorRanking: List<GetExplorerSectionCreatorResponse>,
@SerializedName("latestContentThemeList") val latestContentThemeList: List<String>,
@SerializedName("latestContentList") val latestContentList: List<AudioContentMainItem>,
@SerializedName("bannerList") val bannerList: List<GetAudioContentBannerResponse>,
@SerializedName("eventBannerList") val eventBannerList: GetEventResponse,
@SerializedName("originalAudioDramaList") val originalAudioDramaList: List<GetSeriesListResponse.SeriesListItem>,
@SerializedName("auditionList") val auditionList: List<GetAuditionListItem>,
@SerializedName("dayOfWeekSeriesList") val dayOfWeekSeriesList: List<GetSeriesListResponse.SeriesListItem>,
@SerializedName("contentRanking") val contentRanking: List<GetAudioContentRankingItem>,
@SerializedName("recommendChannelList") val recommendChannelList: List<RecommendChannelResponse>,
@SerializedName("freeContentList") val freeContentList: List<AudioContentMainItem>,
@SerializedName("curationList") val curationList: List<GetContentCurationResponse>
)

View File

@@ -0,0 +1,35 @@
package kr.co.vividnext.sodalive.home
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.settings.ContentType
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
interface HomeApi {
@GET("/api/home")
fun getHomeData(
@Query("timezone") timezone: String,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetHomeResponse>>
@GET("/api/home/latest-content")
fun getLatestContentByTheme(
@Query("theme") theme: String,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<AudioContentMainItem>>>
@GET("/api/home/day-of-week-series")
fun getDayOfWeekSeriesList(
@Query("dayOfWeek") dayOfWeek: SeriesPublishedDaysOfWeek,
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
@Query("contentType") contentType: ContentType,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
}

View File

@@ -0,0 +1,343 @@
package kr.co.vividnext.sodalive.home
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Rect
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.view.View
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
import kr.co.vividnext.sodalive.audio_content.box.AudioContentBoxActivity
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentHomeBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailFragment
import kr.co.vividnext.sodalive.live.room.dialog.LivePaymentDialog
import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog
import kr.co.vividnext.sodalive.main.MainActivity
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
import kr.co.vividnext.sodalive.settings.notification.MemberRole
import org.koin.android.ext.android.inject
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::inflate) {
private val viewModel: HomeViewModel by inject()
private val liveViewModel: LiveViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var liveAdapter: HomeLiveAdapter
private val handler = Handler(Looper.getMainLooper())
private val preferenceChangeListener =
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
// 특정 키에 대한 값이 변경될 때 UI 업데이트
if (key == Constants.PREF_USER_ROLE) {
if (
sharedPreferences.getString(
key,
MemberRole.USER.name
) == MemberRole.CREATOR.name
) {
binding.llUploadContent.visibility = View.VISIBLE
binding.llUploadContent.setOnClickListener {
startActivity(
Intent(
requireActivity(),
AudioContentUploadActivity::class.java
)
)
}
} else {
binding.llUploadContent.visibility = View.GONE
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
SharedPreferenceManager.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
setupView()
bindData()
viewModel.fetchData()
}
override fun onDestroyView() {
SharedPreferenceManager.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
super.onDestroyView()
}
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
binding.llUploadContent.visibility = View.VISIBLE
binding.llUploadContent.setOnClickListener {
startActivity(
Intent(
requireActivity(),
AudioContentUploadActivity::class.java
)
)
}
} else {
binding.llUploadContent.visibility = View.GONE
}
if (SharedPreferenceManager.token.isNotBlank()) {
binding.llShortIcon.visibility = View.VISIBLE
binding.ivSearch.setOnClickListener {}
binding.ivCharge.setOnClickListener {
startActivity(
Intent(
requireContext(),
CanChargeActivity::class.java
)
)
}
binding.ivStorage.setOnClickListener {
startActivity(
Intent(
requireContext(),
AudioContentBoxActivity::class.java
)
)
}
} else {
binding.llShortIcon.visibility = View.GONE
}
setupLiveView()
}
@OptIn(UnstableApi::class)
private fun setupLiveView() {
val spSectionTitle = SpannableString(binding.tvLiveTitle.text)
spSectionTitle.setSpan(
ForegroundColorSpan(
ContextCompat.getColor(
requireContext(),
R.color.color_3bb9f1
)
),
0,
2,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
binding.tvLiveTitle.text = spSectionTitle
liveAdapter = HomeLiveAdapter {
if (SharedPreferenceManager.token.isNotBlank()) {
val detailFragment = LiveRoomDetailFragment(
it.roomId,
onClickParticipant = { enterLiveRoom(it.roomId) },
onClickReservation = {},
onClickModify = {},
onClickStart = {},
onClickCancel = {}
)
if (detailFragment.isAdded) return@HomeLiveAdapter
detailFragment.show(
requireActivity().supportFragmentManager,
detailFragment.tag
)
} else {
(requireActivity() as MainActivity).showLoginActivity()
}
}
val recyclerView = binding.rvLive
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 = 16f.dpToPx().toInt()
}
liveAdapter.itemCount - 1 -> {
outRect.left = 16f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 16f.dpToPx().toInt()
outRect.right = 16f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = liveAdapter
viewModel.liveListLiveData.observe(viewLifecycleOwner) {
if (it.isNotEmpty()) {
binding.llLive.visibility = View.VISIBLE
liveAdapter.addItems(it)
} else {
binding.llLive.visibility = View.GONE
}
}
}
private fun bindData() {
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
}
@UnstableApi
fun enterLiveRoom(roomId: Long) {
requireContext().startService(
Intent(requireContext(), AudioContentPlayService::class.java).apply {
action = AudioContentPlayService.MusicAction.STOP.name
}
)
requireContext().startService(
Intent(requireContext(), AudioContentPlayerService::class.java).apply {
action = "STOP_SERVICE"
}
)
val onEnterRoomSuccess = {
requireActivity().runOnUiThread {
val intent = Intent(requireContext(), LiveRoomActivity::class.java)
intent.putExtra(Constants.EXTRA_ROOM_ID, roomId)
startActivity(intent)
}
}
liveViewModel.getRoomDetail(roomId) {
if (it.channelName != null) {
if (it.manager.id == SharedPreferenceManager.userId) {
handler.postDelayed({
liveViewModel.enterRoom(roomId, onEnterRoomSuccess)
}, 300)
} else if (it.price == 0 || it.isPaid) {
if (it.isPrivateRoom) {
LiveRoomPasswordDialog(
activity = requireActivity(),
layoutInflater = layoutInflater,
can = 0,
confirmButtonClick = { password ->
liveViewModel.enterRoom(
roomId = roomId,
onSuccess = onEnterRoomSuccess,
password = password
)
}
).show(screenWidth)
} else {
handler.postDelayed({
liveViewModel.enterRoom(roomId, onEnterRoomSuccess)
}, 300)
}
} else {
val beginDateFormat = SimpleDateFormat("yyyy.MM.dd EEE hh:mm a", Locale.ENGLISH)
val beginDate = beginDateFormat.parse(it.beginDateTime)!!
val now = Date()
val dateFormat = SimpleDateFormat("yyyy-MM-dd, HH:mm", Locale.getDefault())
val diffTime: Long = now.time - beginDate.time
val hours = (diffTime / (1000 * 60 * 60)).toInt()
val mins = (diffTime / (1000 * 60)).toInt() % 60
if (it.isPrivateRoom) {
LiveRoomPasswordDialog(
activity = requireActivity(),
layoutInflater = layoutInflater,
can = it.price,
confirmButtonClick = { password ->
handler.postDelayed({
liveViewModel.enterRoom(
roomId = roomId,
onSuccess = onEnterRoomSuccess,
password = password
)
}, 300)
}
).show(screenWidth)
} else {
LivePaymentDialog(
activity = requireActivity(),
layoutInflater = layoutInflater,
title = "유료 라이브 입장",
startDateTime = if (hours >= 1) {
dateFormat.format(beginDate)
} else {
null
},
nowDateTime = if (hours >= 1) {
dateFormat.format(now)
} else {
null
},
desc = "${it.price}캔을 차감하고\n라이브에 입장 하시겠습니까?",
desc2 = if (hours >= 1) {
"라이브를 시작한 지 ${hours}시간 ${mins}분이 지났습니다. 라이브에 입장 후 30분 이내에 라이브가 종료될 수도 있습니다."
} else {
null
},
confirmButtonTitle = "결제 후 입장",
confirmButtonClick = {
handler.postDelayed({
liveViewModel.enterRoom(roomId, onEnterRoomSuccess)
}, 300)
},
cancelButtonTitle = "취소",
cancelButtonClick = {}
).show(screenWidth)
}
}
}
}
}
}

View File

@@ -0,0 +1,62 @@
package kr.co.vividnext.sodalive.home
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CircleCrop
import com.bumptech.glide.request.RequestOptions
import kr.co.vividnext.sodalive.databinding.ItemHomeLiveBinding
import kr.co.vividnext.sodalive.live.GetRoomListResponse
class HomeLiveAdapter(
private val onClick: (GetRoomListResponse) -> Unit
) : RecyclerView.Adapter<HomeLiveAdapter.ViewHolder>() {
var items = mutableListOf<GetRoomListResponse>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemHomeLiveBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetRoomListResponse) {
Glide
.with(context)
.load(item.coverImageUrl)
.apply(
RequestOptions().transform(
CircleCrop()
)
)
.into(binding.ivProfile)
binding.tvTitle.text = item.title
binding.tvNickname.text = item.creatorNickname
binding.root.setOnClickListener { onClick(item) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemHomeLiveBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.count()
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetRoomListResponse>) {
this.items.addAll(items)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,30 @@
package kr.co.vividnext.sodalive.home
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.settings.ContentType
import java.util.TimeZone
class HomeRepository(private val api: HomeApi) {
fun fetchData(token: String) = api.getHomeData(
timezone = TimeZone.getDefault().id,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getLatestContentByTheme(theme: String, token: String) = api.getLatestContentByTheme(
theme = theme,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
fun getDayOfWeekSeriesList(
dayOfWeek: SeriesPublishedDaysOfWeek, token: String
) = api.getDayOfWeekSeriesList(
dayOfWeek = dayOfWeek,
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
contentType = ContentType.values()[SharedPreferenceManager.contentPreference],
authHeader = token
)
}

View File

@@ -0,0 +1,164 @@
package kr.co.vividnext.sodalive.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.main.GetAudioContentBannerResponse
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.audio_content.main.v2.GetContentCurationResponse
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.audition.GetAuditionListItem
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.explorer.GetExplorerSectionCreatorResponse
import kr.co.vividnext.sodalive.live.GetRoomListResponse
class HomeViewModel(private val repository: HomeRepository) : BaseViewModel() {
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _liveListLiveData = MutableLiveData<List<GetRoomListResponse>>()
val liveListLiveData: LiveData<List<GetRoomListResponse>>
get() = _liveListLiveData
private var _creatorRankingLiveData = MutableLiveData<List<GetExplorerSectionCreatorResponse>>()
val creatorRankingLiveData: LiveData<List<GetExplorerSectionCreatorResponse>>
get() = _creatorRankingLiveData
private var _latestContentThemeListLiveData = MutableLiveData<List<String>>()
val latestContentThemeListLiveData: LiveData<List<String>>
get() = _latestContentThemeListLiveData
private var _latestContentListLiveData = MutableLiveData<List<AudioContentMainItem>>()
val latestContentListLiveData: LiveData<List<AudioContentMainItem>>
get() = _latestContentListLiveData
private var _eventBannerListLiveData = MutableLiveData<List<GetAudioContentBannerResponse>>()
val eventBannerListLiveData: LiveData<List<GetAudioContentBannerResponse>>
get() = _eventBannerListLiveData
private var _originalAudioDramaListLiveData =
MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val originalAudioDramaListLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _originalAudioDramaListLiveData
private var _auditionListLiveData = MutableLiveData<List<GetAuditionListItem>>()
val auditionListLiveData: LiveData<List<GetAuditionListItem>>
get() = _auditionListLiveData
private var _dayOfWeekSeriesListLiveData =
MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val dayOfWeekSeriesListLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _dayOfWeekSeriesListLiveData
private var _contentRankingLiveData = MutableLiveData<List<GetAudioContentRankingItem>>()
val contentRankingLiveData: LiveData<List<GetAudioContentRankingItem>>
get() = _contentRankingLiveData
private var _recommendChannelListLiveData = MutableLiveData<List<RecommendChannelResponse>>()
val recommendChannelListLiveData: LiveData<List<RecommendChannelResponse>>
get() = _recommendChannelListLiveData
private var _freeContentListLiveData = MutableLiveData<List<AudioContentMainItem>>()
val freeContentListLiveData: LiveData<List<AudioContentMainItem>>
get() = _freeContentListLiveData
private var _curationListLiveData = MutableLiveData<List<GetContentCurationResponse>>()
val curationListLiveData: LiveData<List<GetContentCurationResponse>>
get() = _curationListLiveData
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) {
_liveListLiveData.value = data.liveList
_creatorRankingLiveData.value = data.creatorRanking
_latestContentThemeListLiveData.value = data.latestContentThemeList
_latestContentListLiveData.value = data.latestContentList
_eventBannerListLiveData.value = data.bannerList
_originalAudioDramaListLiveData.value = data.originalAudioDramaList
_auditionListLiveData.value = data.auditionList
_dayOfWeekSeriesListLiveData.value = data.dayOfWeekSeriesList
_contentRankingLiveData.value = data.contentRanking
} 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 getLatestContentByTheme(theme: String) {
_isLoading.value = true
compositeDisposable.add(
repository.getLatestContentByTheme(
theme,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek) {
_isLoading.value = true
compositeDisposable.add(
repository.getDayOfWeekSeriesList(
dayOfWeek,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@@ -0,0 +1,22 @@
package kr.co.vividnext.sodalive.home
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class RecommendChannelResponse(
@SerializedName("channelId") val channelId: Long,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String,
@SerializedName("contentCount") val contentCount: Long,
@SerializedName("contentList") var contentList: List<RecommendChannelContentItem>
)
@Keep
data class RecommendChannelContentItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String,
@SerializedName("thumbnailImageUrl") val thumbnailImageUrl: String,
@SerializedName("likeCount") val likeCount: Long,
@SerializedName("commentCount") val commentCount: Long
)

View File

@@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.home
enum class SeriesPublishedDaysOfWeek {
SUN, MON, TUE, WED, THU, FRI, SAT, RANDOM
}

View File

@@ -31,7 +31,6 @@ import com.orhanobut.logger.Logger
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.main.v2.home.AudioContentMainTabHomeFragment
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerFragment
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
@@ -43,6 +42,7 @@ import kr.co.vividnext.sodalive.databinding.ActivityMainBinding
import kr.co.vividnext.sodalive.databinding.ItemMainTabBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.home.HomeFragment
import kr.co.vividnext.sodalive.live.LiveFragment
import kr.co.vividnext.sodalive.message.MessageActivity
import kr.co.vividnext.sodalive.mypage.MyPageFragment
@@ -472,7 +472,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
if (fragment == null) {
fragment = when (currentTab) {
MainViewModel.CurrentTab.LIVE -> liveFragment
MainViewModel.CurrentTab.HOME -> AudioContentMainTabHomeFragment()
MainViewModel.CurrentTab.HOME -> HomeFragment()
MainViewModel.CurrentTab.MY -> MyPageFragment()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/color_3bb9f1" />
</shape>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#2A339D" />
<stroke
android:width="1dp"
android:color="@color/white" />
<corners android:radius="999dp" />
</shape>

View File

@@ -14,4 +14,20 @@
android:font="@font/gmarket_sans_medium"
app:font="@font/gmarket_sans_medium" />
<font
android:font="@font/pretendard_bold"
app:font="@font/pretendard_bold" />
<font
android:font="@font/pretendard_light"
app:font="@font/pretendard_light" />
<font
android:font="@font/pretendard_medium"
app:font="@font/pretendard_medium" />
<font
android:font="@font/pretendard_regular"
app:font="@font/pretendard_regular" />
</font-family>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,415 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_131313">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingVertical="13.3dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:contentDescription="@null"
android:src="@drawable/img_text_logo" />
<LinearLayout
android:id="@+id/ll_short_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:orientation="horizontal"
android:visibility="gone">
<ImageView
android:id="@+id/iv_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_search_white" />
<ImageView
android:id="@+id/iv_charge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:contentDescription="@null"
android:src="@drawable/ic_can_circle" />
<ImageView
android:id="@+id/iv_storage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_storage" />
</LinearLayout>
</RelativeLayout>
<LinearLayout
android:id="@+id/ll_live"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/tv_live_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:fontFamily="@font/pretendard_bold"
android:text="지금 라이브 중"
android:textColor="@color/white"
android:textSize="26sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_live"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_famous_creator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/tv_famous_creator_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:fontFamily="@font/pretendard_bold"
android:text="인기 크리에이터"
android:textColor="@color/white"
android:textSize="26sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_famous_creator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_new_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:orientation="vertical"
android:visibility="gone">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_bold"
android:text="최신 콘텐츠"
android:textColor="@color/color_eeeeee"
android:textSize="26sp" />
<TextView
android:id="@+id/tv_new_content_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:fontFamily="@font/pretendard_regular"
android:text="전체보기"
android:textColor="@color/color_607d8b" />
</RelativeLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_new_content_theme"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_new_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:orientation="vertical"
android:visibility="gone">
<com.zhpan.bannerview.BannerViewPager
android:id="@+id/event_banner_slider"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.zhpan.indicator.IndicatorView
android:id="@+id/indicator_event_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="6.7dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_series_original"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/tv_series_original"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:fontFamily="@font/pretendard_bold"
android:text="오직 보이스온에서만"
android:textColor="@color/white"
android:textSize="26sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_series_original"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_audition"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:orientation="vertical"
android:visibility="gone">
<com.zhpan.bannerview.BannerViewPager
android:id="@+id/audition_slider"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.zhpan.indicator.IndicatorView
android:id="@+id/indicator_audition"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="6.7dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_series_day_of_week"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/tv_series_day_of_week"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:fontFamily="@font/pretendard_bold"
android:text="요일별 시리즈"
android:textColor="@color/white"
android:textSize="26sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_series_day_of_week_day"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_series_day_of_week"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_weekly_chart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/tv_weekly_chart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:fontFamily="@font/pretendard_bold"
android:text="보온 주간 차트"
android:textColor="@color/white"
android:textSize="26sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_weekly_chart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_recommend_channel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/tv_recommend_channel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:fontFamily="@font/pretendard_bold"
android:text="추천 채널"
android:textColor="@color/white"
android:textSize="26sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_recommend_channel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_free_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/tv_free_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:fontFamily="@font/pretendard_bold"
android:text="무료 콘텐츠"
android:textColor="@color/white"
android:textSize="26sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_free_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_curation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:visibility="gone" />
<io.github.glailton.expandabletextview.ExpandableTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="48dp"
android:fontFamily="@font/pretendard_regular"
android:text="
- 회사명 : 주식회사 소다라이브\n\n
- 대표자 : 이재형\n\n
- 주소 : 경기도 성남시 분당구 황새울로335번길 10, 5층 563A호\n\n
- 사업자등록번호 : 870-81-03220\n\n
- 통신판매업신고 : 제2024-성남분당B-1012호\n\n
- 고객센터 : 02.2055.1477(이용시간 10:00~19:00)\n\n
- 대표 이메일 : sodalive.official@gmail.com"
android:textColor="@color/color_777777"
android:textSize="11sp"
app:animDuration="500"
app:collapsedLines="5"
app:ellipsizeTextColor="@color/color_777777"
app:expandType="layout"
app:isExpanded="false"
app:readLessText="간략히"
app:readMoreText="더보기"
app:textMode="line" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<LinearLayout
android:id="@+id/ll_upload_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_marginEnd="16.7dp"
android:layout_marginBottom="16.7dp"
android:background="@drawable/bg_round_corner_44_3bb9f1"
android:orientation="horizontal"
android:padding="13.3dp"
android:visibility="gone">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="5dp"
android:contentDescription="@null"
android:src="@drawable/ic_thumb_play" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/pretendard_bold"
android:text="콘텐츠 업로드"
android:textColor="@color/white"
android:textSize="13.3sp" />
</LinearLayout>
</RelativeLayout>

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="282dp"
android:layout_height="wrap_content"
android:background="@drawable/live_button_background"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="14dp"
android:paddingVertical="10dp">
<!-- 프로필 이미지 컨테이너 -->
<FrameLayout
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_marginEnd="16dp"
android:background="@drawable/circle_background">
<!-- 프로필 이미지 -->
<ImageView
android:id="@+id/iv_profile"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@drawable/circle_background"
android:contentDescription="@null"
android:scaleType="centerCrop" />
<!-- LIVE 배지 -->
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:contentDescription="@null"
android:src="@drawable/img_live" />
</FrameLayout>
<!-- 텍스트 컨테이너 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tv_nickname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fontFamily="@font/pretendard_regular"
android:maxLines="2"
android:textColor="@android:color/white"
android:textSize="18sp"
tools:text="네대로 마자로" />
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:fontFamily="@font/pretendard_regular"
android:maxLines="2"
android:textColor="@android:color/white"
android:textSize="16sp"
tools:text="네대로 마자로" />
</LinearLayout>
</LinearLayout>

View File

@@ -5,6 +5,7 @@
<color name="color_80d8ff">#80D8FF</color>
<color name="color_1313bc">#1313BC</color>
<color name="color_131313">#131313</color>
<color name="color_9970ff">#9970FF</color>
<color name="color_eeeeee">#EEEEEE</color>
<color name="color_777777">#777777</color>
@@ -130,4 +131,5 @@
<color name="color_cc777777">#CC777777</color>
<color name="color_ec3aa6">#EC3AA6</color>
<color name="color_7849bc">#7849BC</color>
<color name="color_607d8b">#607D8B</color>
</resources>