From d1509d4359b7f6fa5af0fb2047bd71afb35ece81 Mon Sep 17 00:00:00 2001 From: klaus Date: Wed, 15 Jul 2026 21:27:23 +0900 Subject: [PATCH] =?UTF-8?q?refactor(live):=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20=EC=BD=94=EB=94=94=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v2/live/action/LiveActionCoordinator.kt | 392 ++++++++++++++++++ 1 file changed, 392 insertions(+) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt new file mode 100644 index 00000000..19a4b0ec --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt @@ -0,0 +1,392 @@ +package kr.co.vividnext.sodalive.v2.live.action + +import android.content.Intent +import android.view.LayoutInflater +import android.widget.Toast +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService +import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.extensions.moneyFormat +import kr.co.vividnext.sodalive.live.LiveViewModel +import kr.co.vividnext.sodalive.live.reservation.complete.LiveReservationCompleteActivity +import kr.co.vividnext.sodalive.live.room.LiveRoomActivity +import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse +import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailFragment +import kr.co.vividnext.sodalive.live.room.dialog.LiveCancelDialog +import kr.co.vividnext.sodalive.live.room.dialog.LivePaymentDialog +import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog +import kr.co.vividnext.sodalive.live.room.update.LiveRoomEditActivity +import kr.co.vividnext.sodalive.settings.language.LanguageManager +import kr.co.vividnext.sodalive.settings.language.LocaleHelper +import kr.co.vividnext.sodalive.v2.access.AccessRequirement +import kr.co.vividnext.sodalive.v2.access.ensureV2Access +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +class LiveActionCoordinator( + private val activity: FragmentActivity, + private val layoutInflater: LayoutInflater, + private val fragmentManager: FragmentManager, + private val liveViewModel: LiveViewModel, + private val screenWidthProvider: () -> Int, + private val refreshHome: () -> Unit, + private val canHandleResult: () -> Boolean = { !activity.isFinishing && !activity.isDestroyed } +) { + fun showLiveRoomDetail(roomId: Long) { + activity.ensureV2Access(AccessRequirement.Login) { + liveViewModel.getRoomDetail(roomId) { roomDetail -> + if (!canHandleUiResult()) return@getRoomDetail + + activity.ensureV2Access(AccessRequirement.forAdultContent(roomDetail.isAdult)) { + showLiveRoomDetailContent(roomId) + } + } + } + } + + private fun showLiveRoomDetailContent(roomId: Long) { + if (!canShowLiveRoomDetail( + canHandleUiResult = ::canHandleUiResult, + existingDetailFragment = fragmentManager.findFragmentByTag(LIVE_ROOM_DETAIL_TAG) + ) + ) { + return + } + + val detailFragment = LiveRoomDetailFragment( + roomId, + onClickParticipant = {}, + onClickReservation = { reservationRoom(roomId) }, + onClickModify = { roomDetailResponse -> modifyLive(roomDetailResponse) }, + onClickStart = { startLive(roomId) }, + onClickCancel = { cancelLive(roomId) } + ) + + detailFragment.showNow(fragmentManager, LIVE_ROOM_DETAIL_TAG) + } + + fun enterLiveRoom( + roomId: Long, + requiresAdultContentAccess: Boolean? = null, + onUnavailable: (() -> Unit)? = null + ) { + val handleRoomDetail: (GetRoomDetailResponse) -> Unit = handleRoomDetail@{ roomDetail -> + if (!canHandleUiResult()) return@handleRoomDetail + + val decision = decideLiveEntry(roomDetail) + val requiresAdultAccess = requiresAdultContentAccess == true || roomDetail.isAdult + + runLiveEntryDecisionFlow( + decision = decision, + onShowDetail = { + handleUnavailableLive(roomId, requiresAdultAccess, onUnavailable) + }, + ensureAdultAccess = { onAllowed -> + activity.ensureV2Access( + AccessRequirement.forAdultContent(requiresAdultAccess), + onAllowed + ) + }, + onAllowedEntry = { + if (canHandleUiResult()) { + stopAudioPlayback() + val onEnterRoomSuccess = { + activity.runOnUiThread { + runLiveActionUiSideEffect(::canHandleUiResult) { + openLiveRoom(roomId) + } + } + } + handleLiveEntry(roomId, roomDetail, decision, onEnterRoomSuccess) + } + } + ) + } + val loadRoomDetail = { liveViewModel.getRoomDetail(roomId, handleRoomDetail) } + + activity.ensureV2Access(AccessRequirement.Login, loadRoomDetail) + } + + private fun stopAudioPlayback() { + activity.startService( + Intent(activity, AudioContentPlayService::class.java).apply { + action = AudioContentPlayService.MusicAction.STOP.name + } + ) + activity.startService( + Intent(activity, AudioContentPlayerService::class.java).apply { + action = "STOP_SERVICE" + } + ) + } + + private fun handleLiveEntry( + roomId: Long, + roomDetail: GetRoomDetailResponse, + decision: LiveEntryDecision, + onEnterRoomSuccess: () -> Unit + ) { + if (!canHandleUiResult()) return + + when (decision) { + LiveEntryDecision.ShowDetail -> Unit + LiveEntryDecision.Enter -> liveViewModel.enterRoom(roomId, onEnterRoomSuccess) + is LiveEntryDecision.RequestPassword -> { + LiveRoomPasswordDialog( + activity = activity, + layoutInflater = layoutInflater, + can = decision.can, + confirmButtonClick = { password -> + liveViewModel.enterRoom( + roomId = roomId, + onSuccess = onEnterRoomSuccess, + password = password + ) + } + ).show(screenWidthProvider()) + } + + LiveEntryDecision.RequestPayment -> { + showPaidLiveEntryDialog( + roomId = roomId, + beginDateTimeUtc = roomDetail.beginDateTimeUtc, + price = roomDetail.price, + onEnterRoomSuccess = onEnterRoomSuccess + ) + } + } + } + + private fun decideLiveEntry(roomDetail: GetRoomDetailResponse): LiveEntryDecision { + return LiveEntryPolicy.decide( + LiveEntryContext( + channelName = roomDetail.channelName, + managerId = roomDetail.manager.id, + currentUserId = SharedPreferenceManager.userId, + price = roomDetail.price, + isPaid = roomDetail.isPaid, + isPrivateRoom = roomDetail.isPrivateRoom + ) + ) + } + + private fun handleUnavailableLive( + roomId: Long, + requiresAdultAccess: Boolean, + onUnavailable: (() -> Unit)? + ) { + if (onUnavailable != null) { + onUnavailable() + return + } + activity.ensureV2Access(AccessRequirement.forAdultContent(requiresAdultAccess)) { + showLiveRoomDetailContent(roomId) + } + } + + private fun reservationRoom(roomId: Long) { + liveViewModel.getRoomDetail(roomId) { + if (!canHandleUiResult()) return@getRoomDetail + + if (it.manager.id == SharedPreferenceManager.userId) { + Toast.makeText( + activity, + activity.getString(R.string.screen_live_reservation_self_block), + Toast.LENGTH_LONG + ).show() + return@getRoomDetail + } + + if (it.isPrivateRoom) { + LiveRoomPasswordDialog( + activity = activity, + layoutInflater = layoutInflater, + can = if (it.isPaid) 0 else it.price, + confirmButtonClick = { password -> processLiveReservation(roomId, password) } + ).show(screenWidthProvider()) + } else if (it.price == 0 || it.isPaid) { + processLiveReservation(roomId) + } else { + LivePaymentDialog( + activity = activity, + layoutInflater = layoutInflater, + title = activity.getString( + R.string.screen_live_reservation_pay_title, + it.price.moneyFormat() + ), + desc = activity.getString(R.string.screen_live_reservation_pay_desc, it.title), + confirmButtonTitle = activity.getString(R.string.screen_live_reservation_confirm), + confirmButtonClick = { processLiveReservation(roomId) }, + cancelButtonTitle = activity.getString(R.string.cancel), + cancelButtonClick = {} + ).show(screenWidthProvider()) + } + } + } + + private fun processLiveReservation(roomId: Long, password: String? = null) { + liveViewModel.reservationRoom(roomId, password) { + if (!canHandleUiResult()) return@reservationRoom + + refreshHome() + activity.startActivity( + Intent(activity, LiveReservationCompleteActivity::class.java).apply { + putExtra(Constants.EXTRA_LIVE_RESERVATION_RESPONSE, it) + } + ) + } + } + + private fun modifyLive(roomDetail: GetRoomDetailResponse) { + if (!canHandleUiResult()) return + + activity.startActivity( + Intent(activity, LiveRoomEditActivity::class.java).apply { + putExtra(Constants.EXTRA_ROOM_DETAIL, roomDetail) + } + ) + } + + private fun startLive(roomId: Long) { + liveViewModel.startLive(roomId) { + if (!canHandleUiResult()) return@startLive + + refreshHome() + activity.runOnUiThread { + runLiveActionUiSideEffect(::canHandleUiResult) { + openLiveRoom(roomId) + } + } + } + } + + private fun cancelLive(roomId: Long) { + if (!canHandleUiResult()) return + + LiveCancelDialog( + activity = activity, + layoutInflater = layoutInflater, + title = activity.getString(R.string.screen_live_cancel_title), + hint = activity.getString(R.string.screen_live_cancel_hint), + confirmButtonTitle = activity.getString(R.string.screen_live_cancel_confirm), + confirmButtonClick = { reason -> + liveViewModel.cancelLive(roomId, reason) { + if (!canHandleUiResult()) return@cancelLive + + Toast.makeText( + activity, + activity.getString(R.string.screen_live_cancel_success), + Toast.LENGTH_LONG + ).show() + refreshHome() + } + }, + cancelButtonTitle = activity.getString(R.string.dialog_close), + cancelButtonClick = {} + ).show(screenWidthProvider()) + } + + private fun showPaidLiveEntryDialog( + roomId: Long, + beginDateTimeUtc: String, + price: Int, + onEnterRoomSuccess: () -> Unit + ) { + if (!canHandleUiResult()) return + + val locale = Locale(LanguageManager.getEffectiveLanguage(activity)) + val wrappedContext = LocaleHelper.wrap(activity) + val beginDate = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).apply { + timeZone = TimeZone.getTimeZone("UTC") + }.parse(beginDateTimeUtc) ?: return + val now = Date() + val dateFormat = SimpleDateFormat("yyyy-MM-dd, HH:mm", locale) + val diffTime = now.time - beginDate.time + val hours = (diffTime / (1000 * 60 * 60)).toInt() + val mins = (diffTime / (1000 * 60)).toInt() % 60 + + LivePaymentDialog( + activity = activity, + layoutInflater = layoutInflater, + title = wrappedContext.getString(R.string.live_paid_title), + startDateTime = if (hours >= 1) dateFormat.format(beginDate) else null, + nowDateTime = if (hours >= 1) dateFormat.format(now) else null, + desc = wrappedContext.getString(R.string.live_paid_desc, price), + desc2 = if (hours >= 1) { + wrappedContext.getString(R.string.live_paid_warning, hours, mins) + } else { + null + }, + confirmButtonTitle = wrappedContext.getString(R.string.live_paid_confirm), + confirmButtonClick = { liveViewModel.enterRoom(roomId, onEnterRoomSuccess) }, + cancelButtonTitle = wrappedContext.getString(R.string.cancel), + cancelButtonClick = {} + ).show(screenWidthProvider()) + } + + private fun openLiveRoom(roomId: Long) { + activity.startActivity( + Intent(activity, LiveRoomActivity::class.java).apply { + putExtra(Constants.EXTRA_ROOM_ID, roomId) + } + ) + } + + private fun canHandleUiResult(): Boolean { + return canHandleLiveActionUiResult( + isActivityFinishing = activity.isFinishing, + isActivityDestroyed = activity.isDestroyed, + isStateSaved = fragmentManager.isStateSaved, + canHandleResult = canHandleResult + ) + } + + private companion object { + const val LIVE_ROOM_DETAIL_TAG = "LiveRoomDetailFragment" + } +} + +internal fun runLiveActionUiSideEffect( + canHandleResult: () -> Boolean, + sideEffect: () -> Unit +): Boolean { + if (!canHandleResult()) return false + sideEffect() + return true +} + +internal fun runLiveEntryDecisionFlow( + decision: LiveEntryDecision, + onShowDetail: () -> Unit, + ensureAdultAccess: (onAllowed: () -> Unit) -> Unit, + onAllowedEntry: () -> Unit +) { + if (decision == LiveEntryDecision.ShowDetail) { + onShowDetail() + return + } + + ensureAdultAccess(onAllowedEntry) +} + +internal fun canHandleLiveActionUiResult( + isActivityFinishing: Boolean, + isActivityDestroyed: Boolean, + isStateSaved: Boolean, + canHandleResult: () -> Boolean +): Boolean { + return !isActivityFinishing && !isActivityDestroyed && !isStateSaved && canHandleResult() +} + +internal fun canShowLiveRoomDetail( + canHandleUiResult: () -> Boolean, + existingDetailFragment: Any? +): Boolean { + return canHandleUiResult() && existingDetailFragment == null +}