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

@@ -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
}