라이브 방 - 룰렛 설정 추가

This commit is contained in:
2023-12-01 03:37:23 +09:00
parent b359ca58ba
commit 952df91619
26 changed files with 928 additions and 18 deletions

View File

@@ -29,6 +29,7 @@ object Constants {
const val EXTRA_MESSAGE_BOX = "extra_message_box"
const val EXTRA_TEXT_MESSAGE = "extra_text_message"
const val EXTRA_LIVE_TIME_NOW = "extra_live_time_now"
const val EXTRA_RESULT_ROULETTE = "extra_result_roulette"
const val EXTRA_GO_TO_PREV_PAGE = "extra_go_to_prev_page"
const val EXTRA_SELECT_RECIPIENT = "extra_select_recipient"
const val EXTRA_ROOM_CHANNEL_NAME = "extra_room_channel_name"

View File

@@ -43,6 +43,9 @@ import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageViewMo
import kr.co.vividnext.sodalive.live.room.tag.LiveTagRepository
import kr.co.vividnext.sodalive.live.room.tag.LiveTagViewModel
import kr.co.vividnext.sodalive.live.room.update.LiveRoomEditViewModel
import kr.co.vividnext.sodalive.live.roulette.RouletteRepository
import kr.co.vividnext.sodalive.live.roulette.config.RouletteApi
import kr.co.vividnext.sodalive.live.roulette.config.RouletteSettingsViewModel
import kr.co.vividnext.sodalive.main.MainViewModel
import kr.co.vividnext.sodalive.message.MessageApi
import kr.co.vividnext.sodalive.message.MessageRepository
@@ -145,6 +148,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
single { ApiBuilder().build(get(), AudioContentApi::class.java) }
single { ApiBuilder().build(get(), FaqApi::class.java) }
single { ApiBuilder().build(get(), MemberTagApi::class.java) }
single { ApiBuilder().build(get(), RouletteApi::class.java) }
}
private val viewModelModule = module {
@@ -197,6 +201,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { AudioContentCurationViewModel(get()) }
viewModel { AudioContentNewAllViewModel(get()) }
viewModel { AudioContentRankingAllViewModel(get()) }
viewModel { RouletteSettingsViewModel(get()) }
}
private val repositoryModule = module {
@@ -219,6 +224,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
factory { FaqRepository(get()) }
factory { MemberTagRepository(get()) }
factory { UserProfileFantalkAllViewModel(get(), get()) }
factory { RouletteRepository(get()) }
}
private val moduleList = listOf(

View File

@@ -72,6 +72,7 @@ import kr.co.vividnext.sodalive.live.room.profile.LiveRoomProfileDialog
import kr.co.vividnext.sodalive.live.room.profile.LiveRoomProfileListAdapter
import kr.co.vividnext.sodalive.live.room.profile.LiveRoomUserProfileDialog
import kr.co.vividnext.sodalive.live.room.update.LiveRoomInfoEditDialog
import kr.co.vividnext.sodalive.live.roulette.config.RouletteConfigActivity
import kr.co.vividnext.sodalive.report.ProfileReportDialog
import kr.co.vividnext.sodalive.report.ReportType
import kr.co.vividnext.sodalive.report.UserReportDialog
@@ -179,6 +180,17 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
}
private val rouletteConfigResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
val isActiveRoulette = result.data?.getBooleanExtra(Constants.EXTRA_RESULT_ROULETTE, false)
if (resultCode == RESULT_OK && isActiveRoulette != null) {
// TODO 룰렛 활성화 / 비활성화 설정 리스너에게 알림
}
}
override fun onCreate(savedInstanceState: Bundle?) {
agora = Agora(
context = this,
@@ -660,6 +672,16 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
response.creatorId == SharedPreferenceManager.userId &&
SharedPreferenceManager.role == MemberRole.CREATOR.name
) {
binding.flRoulette.visibility = View.GONE
binding.flRouletteSettings.visibility = View.VISIBLE
binding.flRouletteSettings.setOnClickListener {
rouletteConfigResult.launch(
Intent(
applicationContext,
RouletteConfigActivity::class.java
)
)
}
binding.flDonationMessageList.visibility = View.VISIBLE
binding.flDonationMessageList.setOnClickListener {
LiveRoomDonationMessageDialog(

View File

@@ -0,0 +1,14 @@
package kr.co.vividnext.sodalive.live.roulette
import com.google.gson.annotations.SerializedName
data class GetRouletteResponse(
@SerializedName("can") val can: Int,
@SerializedName("isActive") val isActive: Boolean,
@SerializedName("items") val items: List<RouletteItem>
)
data class RouletteItem(
@SerializedName("title") val title: String,
@SerializedName("weight") val weight: Int
)

View File

@@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.live.roulette
import kr.co.vividnext.sodalive.live.roulette.config.CreateOrUpdateRouletteRequest
import kr.co.vividnext.sodalive.live.roulette.config.RouletteApi
class RouletteRepository(private val api: RouletteApi) {
fun createOrUpdateRoulette(
request: CreateOrUpdateRouletteRequest,
token: String
) = api.createOrUpdateRoulette(
request = request,
authHeader = token
)
fun getRoulette(creatorId: Long, token: String) = api.getRoulette(
creatorId = creatorId,
authHeader = token
)
}

View File

@@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.live.roulette.config
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.live.roulette.RouletteItem
data class CreateOrUpdateRouletteRequest(
@SerializedName("can") val can: Int,
@SerializedName("isActive") val isActive: Boolean,
@SerializedName("items") val items: List<RouletteItem>
)

View File

@@ -0,0 +1,24 @@
package kr.co.vividnext.sodalive.live.roulette.config
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.live.roulette.GetRouletteResponse
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Query
interface RouletteApi {
@POST("/roulette")
fun createOrUpdateRoulette(
@Body request: CreateOrUpdateRouletteRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/roulette")
fun getRoulette(
@Query("creatorId") creatorId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetRouletteResponse>>
}

View File

@@ -0,0 +1,64 @@
package kr.co.vividnext.sodalive.live.roulette.config
import android.os.Bundle
import com.google.android.material.tabs.TabLayout
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.databinding.ActivityRouletteConfigBinding
class RouletteConfigActivity : BaseActivity<ActivityRouletteConfigBinding>(
ActivityRouletteConfigBinding::inflate
) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
changeFragment("settings")
}
override fun setupView() {
binding.toolbar.tvBack.text = "룰렛설정"
binding.toolbar.tvBack.setOnClickListener { finish() }
val tabs = binding.tabs
tabs.addTab(tabs.newTab().setText("룰렛설정").setTag("settings"))
tabs.addTab(tabs.newTab().setText("당첨내역").setTag("winning-details"))
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
val tag = tab.tag as String
changeFragment(tag)
}
override fun onTabUnselected(tab: TabLayout.Tab) {
}
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
}
private fun changeFragment(tag: String) {
val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
val currentFragment = fragmentManager.primaryNavigationFragment
if (currentFragment != null) {
fragmentTransaction.hide(currentFragment)
}
var fragment = fragmentManager.findFragmentByTag(tag)
if (fragment == null) {
fragment = if (tag == "settings") {
RouletteSettingsFragment()
} else {
RouletteWinningDetailsFragment()
}
fragmentTransaction.add(R.id.container, fragment, tag)
} else {
fragmentTransaction.show(fragment)
}
fragmentTransaction.setPrimaryNavigationFragment(fragment)
fragmentTransaction.setReorderingAllowed(true)
fragmentTransaction.commitNow()
}
}

View File

@@ -0,0 +1,3 @@
package kr.co.vividnext.sodalive.live.roulette.config
data class RouletteOption(var title: String, var weight: Int, var percentage: Int = 50)

View File

@@ -0,0 +1,163 @@
package kr.co.vividnext.sodalive.live.roulette.config
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Service
import android.content.Intent
import android.os.Bundle
import android.text.InputFilter
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
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.databinding.FragmentRouletteSettingsBinding
import org.koin.android.ext.android.inject
import java.util.concurrent.TimeUnit
class RouletteSettingsFragment : BaseFragment<FragmentRouletteSettingsBinding>(
FragmentRouletteSettingsBinding::inflate
) {
private val viewModel: RouletteSettingsViewModel by inject()
private lateinit var imm: InputMethodManager
private lateinit var loadingDialog: LoadingDialog
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
bindData()
viewModel.getRoulette()
}
private fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
imm = requireActivity().getSystemService(
Service.INPUT_METHOD_SERVICE
) as InputMethodManager
binding.etSetPrice.filters = arrayOf(InputFilter { source, start, end, _, _, _ ->
// Only allow numeric input
for (i in start until end) {
if (!Character.isDigit(source[i])) {
return@InputFilter ""
}
}
null
})
binding.ivRouletteIsActive.setOnClickListener { viewModel.toggleIsActive() }
binding.ivAddOption.setOnClickListener { addOption() }
binding.tvPreview.setOnClickListener {
}
binding.tvSave.setOnClickListener { _ ->
imm.hideSoftInputFromWindow(view?.windowToken, 0)
viewModel.createOrUpdateRoulette {
val resultIntent = Intent().apply { putExtra(Constants.EXTRA_RESULT_ROULETTE, it) }
requireActivity().setResult(Activity.RESULT_OK, resultIntent)
requireActivity().finish()
}
}
}
private fun bindData() {
viewModel.isActiveLiveData.observe(viewLifecycleOwner) {
binding.ivRouletteIsActive.setImageResource(
if (it) R.drawable.btn_toggle_on_big else R.drawable.btn_toggle_off_big
)
}
viewModel.optionsLiveData.observe(viewLifecycleOwner) { updateOptionUi(it) }
viewModel.toastLiveData.observe(viewLifecycleOwner) { it?.let { showToast(it) } }
viewModel.canLiveData.observe(viewLifecycleOwner) { binding.etSetPrice.setText("$it") }
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
compositeDisposable.add(
binding.etSetPrice.textChanges().skip(1)
.debounce(100, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
if (it.trim().isNotEmpty()) {
viewModel.can = it.toString().toInt()
}
}
)
}
private fun addOption() {
val newOption = RouletteOption("", 1)
viewModel.addOption(newOption)
}
private fun updateOptionUi(options: List<RouletteOption>) {
binding.llRouletteOptionContainer.removeAllViews()
options.forEachIndexed { index, option ->
binding.llRouletteOptionContainer.addView(createOptionView(index, option))
}
}
@SuppressLint("SetTextI18n")
private fun createOptionView(index: Int, option: RouletteOption): View {
val optionView = LayoutInflater
.from(context)
.inflate(
R.layout.layout_roulette_option,
binding.llRouletteOptionContainer,
false
)
val etOption = optionView.findViewById<EditText>(R.id.et_option)
val tvOptionTitle = optionView.findViewById<TextView>(R.id.tv_option_title)
val tvPercentage = optionView.findViewById<TextView>(R.id.tv_option_percentage)
val ivMinus = optionView.findViewById<ImageView>(R.id.iv_minus)
val ivPlus = optionView.findViewById<ImageView>(R.id.iv_plus)
val tvDelete = optionView.findViewById<TextView>(R.id.tv_delete)
etOption.setText(option.title)
tvOptionTitle.text = "옵션 ${index + 1}"
tvPercentage.text = "${option.percentage}%"
ivMinus.setOnClickListener { viewModel.subtractWeight(index) }
ivPlus.setOnClickListener { viewModel.plusWeight(index) }
if (index == 0 || index == 1) {
tvDelete.visibility = View.GONE
} else {
tvDelete.visibility = View.VISIBLE
tvDelete.setOnClickListener { viewModel.deleteOption(index) }
}
compositeDisposable.add(
etOption.textChanges().skip(1)
.debounce(100, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.inputOption(index, it.toString())
}
)
return optionView
}
}

View File

@@ -0,0 +1,201 @@
package kr.co.vividnext.sodalive.live.roulette.config
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.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.live.roulette.RouletteItem
import kr.co.vividnext.sodalive.live.roulette.RouletteRepository
class RouletteSettingsViewModel(private val repository: RouletteRepository) : BaseViewModel() {
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private val _optionsLiveData = MutableLiveData<List<RouletteOption>>(listOf())
val optionsLiveData: LiveData<List<RouletteOption>>
get() = _optionsLiveData
private val _isActiveLiveData = MutableLiveData(false)
val isActiveLiveData: LiveData<Boolean>
get() = _isActiveLiveData
private val _canLiveData = MutableLiveData(5)
val canLiveData: LiveData<Int>
get() = _canLiveData
private val options = mutableListOf<RouletteOption>()
var can = 5
var isActive = false
fun plusWeight(optionIndex: Int) {
val currentOption = options[optionIndex]
options[optionIndex] = currentOption.copy(weight = currentOption.weight + 1)
recalculatePercentages(options)
}
fun subtractWeight(optionIndex: Int) {
if (options[optionIndex].weight > 1) {
val currentOption = options[optionIndex]
options[optionIndex] = currentOption.copy(weight = currentOption.weight - 1)
recalculatePercentages(options)
}
}
fun addOption(newOption: RouletteOption) {
if (options.size >= 6) return
options.add(newOption)
recalculatePercentages(options)
}
fun deleteOption(index: Int) {
val updatedOptions = options.filterIndexed { currentIndex, _ -> currentIndex != index }
removeAllAndAddOptions(updatedOptions)
recalculatePercentages(updatedOptions)
}
fun inputOption(optionIndex: Int, title: String) {
val currentOption = options[optionIndex]
options[optionIndex] = currentOption.copy(title = title)
}
private fun recalculatePercentages(options: List<RouletteOption>) {
val totalWeight = options.sumOf { it.weight }
val updatedOptions = options.asSequence().map { option ->
option.copy(percentage = (option.weight.toDouble() / totalWeight * 100).toInt())
}.toList()
_optionsLiveData.value = updatedOptions
}
fun toggleIsActive() {
isActive = !isActive
_isActiveLiveData.postValue(isActive)
}
fun createOrUpdateRoulette(onSuccess: (Boolean) -> Unit) {
if (!_isLoading.value!!) {
_isLoading.value = true
val items = mutableListOf<RouletteItem>()
for (option in options) {
if (option.title.trim().isEmpty()) {
_toastLiveData.value = "옵션은 빈칸을 할 수 없습니다."
_isLoading.value = false
return
}
items.add(RouletteItem(title = option.title, weight = option.weight))
}
val request = CreateOrUpdateRouletteRequest(
can = can,
isActive = isActive,
items = items
)
compositeDisposable.add(
repository.createOrUpdateRoulette(
request = request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null && it.data is Boolean) {
_toastLiveData.postValue("룰렛이 설정되었습니다.")
onSuccess(it.data)
} 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 getRoulette() {
if (!_isLoading.value!!) {
_isLoading.value = true
compositeDisposable.add(
repository.getRoulette(
creatorId = SharedPreferenceManager.userId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success) {
val data = it.data
if (data != null && data.items.isNotEmpty()) {
_isActiveLiveData.value = data.isActive
_canLiveData.value = data.can
isActive = data.isActive
can = data.can
val options = data.items.asSequence().map { item ->
RouletteOption(title = item.title, weight = item.weight)
}.toList()
removeAllAndAddOptions(options = options)
recalculatePercentages(options)
} else {
_isActiveLiveData.value = false
_canLiveData.value = 5
isActive = false
can = 5
options.add(RouletteOption(title = "", weight = 1))
options.add(RouletteOption(title = "", weight = 1))
recalculatePercentages(options)
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}
private fun removeAllAndAddOptions(options: List<RouletteOption>) {
this.options.clear()
this.options.addAll(options)
}
}

View File

@@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.live.roulette.config
import android.os.Bundle
import android.view.View
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.databinding.FragmentRouletteWinningDetailsBinding
class RouletteWinningDetailsFragment : BaseFragment<FragmentRouletteWinningDetailsBinding>(
FragmentRouletteWinningDetailsBinding::inflate
) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
}
private fun setupView() {
}
}