From 0b3042d29e242273a07c5867fbaf690189350d32 Mon Sep 17 00:00:00 2001 From: klaus Date: Wed, 15 Jul 2026 21:27:44 +0900 Subject: [PATCH] =?UTF-8?q?refactor(onair):=20=EB=9D=BC=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=20=EC=9E=85=EC=9E=A5=20=ED=9D=90=EB=A6=84=EC=9D=84=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=ED=99=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v2/live/onair/HomeOnAirLiveActivity.kt | 135 +------- .../onair/model/HomeOnAirLiveEntryPolicy.kt | 7 - .../action/LiveActionCoordinatorSourceTest.kt | 297 ++++++++++++++++++ .../onair/HomeOnAirLiveEntryPolicyTest.kt | 76 +---- 4 files changed, 318 insertions(+), 197 deletions(-) delete mode 100644 app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveEntryPolicy.kt create mode 100644 app/src/test/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinatorSourceTest.kt diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt index c55c7498..1ba6c7e9 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt @@ -9,32 +9,16 @@ 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.player.AudioContentPlayerService import kr.co.vividnext.sodalive.base.BaseActivity -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.common.ToastMessage import kr.co.vividnext.sodalive.databinding.ActivityHomeOnAirLiveBinding import kr.co.vividnext.sodalive.live.LiveViewModel -import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse -import kr.co.vividnext.sodalive.live.room.LiveRoomActivity -import kr.co.vividnext.sodalive.live.room.dialog.LivePaymentDialog -import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog -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 kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinator import kr.co.vividnext.sodalive.v2.live.onair.model.HomeOnAirLivePageUiState -import kr.co.vividnext.sodalive.v2.live.onair.model.canEnterHomeOnAirLiveRoom import kr.co.vividnext.sodalive.v2.live.onair.ui.HomeOnAirLiveAdapter import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.TimeZone @UnstableApi class HomeOnAirLiveActivity : BaseActivity( @@ -42,6 +26,16 @@ class HomeOnAirLiveActivity : BaseActivity( ) { private val viewModel: HomeOnAirLiveViewModel by viewModel() private val liveViewModel: LiveViewModel by inject() + private val liveActionCoordinator: LiveActionCoordinator by lazy { + LiveActionCoordinator( + activity = this, + layoutInflater = layoutInflater, + fragmentManager = supportFragmentManager, + liveViewModel = liveViewModel, + screenWidthProvider = { screenWidth }, + refreshHome = {} + ) + } private val loadingDialog: LoadingDialog by lazy { LoadingDialog(this, layoutInflater) } private val adapter = HomeOnAirLiveAdapter { enterLiveRoom(it.roomId) } private var isPageLoading = false @@ -110,114 +104,11 @@ class HomeOnAirLiveActivity : BaseActivity( } private fun enterLiveRoom(roomId: Long) { - ensureV2Access(AccessRequirement.Login) { - liveViewModel.getRoomDetail(roomId) { roomDetail -> - if (!canEnterHomeOnAirLiveRoom(roomDetail)) { - Toast.makeText(applicationContext, R.string.common_error_unknown, Toast.LENGTH_LONG).show() - return@getRoomDetail - } - - ensureV2Access(AccessRequirement.forAdultContent(roomDetail.isAdult)) { - enterLiveRoom(roomId, roomDetail) - } - } + liveActionCoordinator.enterLiveRoom(roomId) { + Toast.makeText(applicationContext, R.string.common_error_unknown, Toast.LENGTH_LONG).show() } } - private fun enterLiveRoom(roomId: Long, roomDetail: GetRoomDetailResponse) { - startService( - Intent(applicationContext, AudioContentPlayService::class.java).apply { - action = AudioContentPlayService.MusicAction.STOP.name - } - ) - startService( - Intent(applicationContext, AudioContentPlayerService::class.java).apply { - action = "STOP_SERVICE" - } - ) - - val onEnterRoomSuccess = { - runOnUiThread { - startActivity( - Intent(applicationContext, LiveRoomActivity::class.java).apply { - putExtra(Constants.EXTRA_ROOM_ID, roomId) - } - ) - } - } - - if (roomDetail.manager.id == SharedPreferenceManager.userId) { - liveViewModel.enterRoom(roomId, onEnterRoomSuccess) - } else if (roomDetail.price == 0 || roomDetail.isPaid) { - if (roomDetail.isPrivateRoom) { - showPasswordDialog(roomId, can = 0, onEnterRoomSuccess = onEnterRoomSuccess) - } else { - liveViewModel.enterRoom(roomId, onEnterRoomSuccess) - } - } else { - showPaidLiveEntryDialog( - roomId = roomId, - beginDateTimeUtc = roomDetail.beginDateTimeUtc, - price = roomDetail.price, - isPrivateRoom = roomDetail.isPrivateRoom, - onEnterRoomSuccess = onEnterRoomSuccess - ) - } - } - - private fun showPasswordDialog(roomId: Long, can: Int, onEnterRoomSuccess: () -> Unit) { - LiveRoomPasswordDialog( - activity = this, - layoutInflater = layoutInflater, - can = can, - confirmButtonClick = { password -> - liveViewModel.enterRoom( - roomId = roomId, - onSuccess = onEnterRoomSuccess, - password = password - ) - } - ).show(screenWidth) - } - - private fun showPaidLiveEntryDialog( - roomId: Long, - beginDateTimeUtc: String, - price: Int, - isPrivateRoom: Boolean, - onEnterRoomSuccess: () -> Unit - ) { - if (isPrivateRoom) { - showPasswordDialog(roomId, can = price, onEnterRoomSuccess = onEnterRoomSuccess) - return - } - - val locale = Locale(LanguageManager.getEffectiveLanguage(this)) - val wrappedContext = LocaleHelper.wrap(this) - 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 = this, - 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(screenWidth) - } - private fun showToast(toastMessage: ToastMessage) { toastMessage.message?.let { message -> showToast(message) } ?: toastMessage.resId?.let { resId -> showToast(getString(resId)) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveEntryPolicy.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveEntryPolicy.kt deleted file mode 100644 index c5430e4e..00000000 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/model/HomeOnAirLiveEntryPolicy.kt +++ /dev/null @@ -1,7 +0,0 @@ -package kr.co.vividnext.sodalive.v2.live.onair.model - -import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse - -fun canEnterHomeOnAirLiveRoom(roomDetail: GetRoomDetailResponse): Boolean { - return roomDetail.channelName.isNullOrBlank().not() -} diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinatorSourceTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinatorSourceTest.kt new file mode 100644 index 00000000..12d23971 --- /dev/null +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinatorSourceTest.kt @@ -0,0 +1,297 @@ +package kr.co.vividnext.sodalive.v2.live.action + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.File + +class LiveActionCoordinatorSourceTest { + + @Test + fun `Live coordinator는 공통 정책과 기존 레거시 동작을 연결한다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt" + ).readText() + + assertTrue(source.contains("class LiveActionCoordinator(")) + assertTrue(source.contains("LiveEntryPolicy.decide(")) + assertTrue(source.contains("AccessRequirement.Login")) + assertTrue(source.contains("val requiresAdultAccess =")) + assertTrue(source.contains("requiresAdultContentAccess == true || roomDetail.isAdult")) + assertTrue(source.contains("AccessRequirement.forAdultContent(requiresAdultAccess)")) + assertTrue(source.contains("AudioContentPlayService.MusicAction.STOP.name")) + assertTrue(source.contains("AudioContentPlayerService::class.java")) + assertTrue(source.contains("LiveRoomPasswordDialog(")) + assertTrue(source.contains("LivePaymentDialog(")) + assertTrue(source.contains("LiveRoomDetailFragment(")) + assertTrue(source.contains("LiveRoomActivity::class.java")) + assertFalse(source.contains("kr.co.vividnext.sodalive.v2.creator")) + } + + @Test + fun `Live coordinator 기본 lifecycle predicate는 종료된 Activity 결과 처리를 막는다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt" + ).readText() + + assertTrue( + source.contains( + "private val canHandleResult: () -> Boolean = " + + "{ !activity.isFinishing && !activity.isDestroyed }" + ) + ) + } + + @Test + fun `Live coordinator는 입장 성공 callback 직전에 lifecycle을 다시 검사한다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt" + ).readText() + val entrySource = sourceSection( + source, + "fun enterLiveRoom(", + "private fun stopAudioPlayback()" + ) + val successSource = sourceSection( + entrySource, + "val onEnterRoomSuccess = {", + "handleLiveEntry(roomId, roomDetail, decision, onEnterRoomSuccess)" + ) + + assertTrue(successSource.contains("activity.runOnUiThread")) + assertTrue(successSource.contains("runLiveActionUiSideEffect(::canHandleUiResult)")) + assertBefore( + successSource, + "runLiveActionUiSideEffect(::canHandleUiResult)", + "openLiveRoom(roomId)" + ) + } + + @Test + fun `Live coordinator는 시작 성공 navigation 직전에도 lifecycle을 다시 검사한다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt" + ).readText() + val startSource = sourceSection( + source, + "private fun startLive(roomId: Long)", + "private fun cancelLive(roomId: Long)" + ) + + assertTrue(startSource.contains("activity.runOnUiThread")) + assertBefore( + startSource, + "runLiveActionUiSideEffect(::canHandleUiResult)", + "openLiveRoom(roomId)" + ) + } + + @Test + fun `Live coordinator는 Activity stateSaved 호출 화면 predicate를 결합해 UI 결과를 검사한다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt" + ).readText() + val predicateSource = sourceSection( + source, + "private fun canHandleUiResult(): Boolean", + "private companion object" + ) + + assertTrue(predicateSource.contains("isActivityFinishing = activity.isFinishing")) + assertTrue(predicateSource.contains("isActivityDestroyed = activity.isDestroyed")) + assertTrue(predicateSource.contains("isStateSaved = fragmentManager.isStateSaved")) + assertTrue(predicateSource.contains("canHandleResult = canHandleResult")) + } + + @Test + fun `Live coordinator는 안정적인 tag와 state로 상세 중복 표시를 막는다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt" + ).readText() + val detailSource = sourceSection( + source, + "fun showLiveRoomDetail(roomId: Long)", + "fun enterLiveRoom(" + ) + + assertTrue(source.contains("LIVE_ROOM_DETAIL_TAG")) + assertTrue(detailSource.contains("fragmentManager.findFragmentByTag(LIVE_ROOM_DETAIL_TAG)")) + assertTrue(detailSource.contains("canShowLiveRoomDetail(")) + assertTrue(detailSource.contains("detailFragment.showNow(fragmentManager, LIVE_ROOM_DETAIL_TAG)")) + assertBefore(detailSource, "canShowLiveRoomDetail(", "LiveRoomDetailFragment(") + assertBefore( + detailSource, + "fragmentManager.findFragmentByTag(LIVE_ROOM_DETAIL_TAG)", + "LiveRoomDetailFragment(" + ) + assertFalse(detailSource.contains("detailFragment.isAdded")) + assertFalse(detailSource.contains("detailFragment.tag")) + } + + @Test + fun `Live coordinator 공개 상세 진입점은 로그인과 성인 접근을 확인한 뒤 표시한다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt" + ).readText() + val publicDetailSource = sourceSection( + source, + "fun showLiveRoomDetail(roomId: Long)", + "private fun showLiveRoomDetailContent(roomId: Long)" + ) + + assertTrue(publicDetailSource.contains("AccessRequirement.Login")) + assertTrue(publicDetailSource.contains("liveViewModel.getRoomDetail(roomId)")) + assertTrue(publicDetailSource.contains("AccessRequirement.forAdultContent(roomDetail.isAdult)")) + assertTrue(publicDetailSource.contains("showLiveRoomDetailContent(roomId)")) + assertBefore(publicDetailSource, "AccessRequirement.Login", "liveViewModel.getRoomDetail(roomId)") + assertBefore( + publicDetailSource, + "AccessRequirement.forAdultContent(roomDetail.isAdult)", + "showLiveRoomDetailContent(roomId)" + ) + } + + @Test + fun `Home Creator Channel On-air는 동일한 Live coordinator를 사용한다`() { + val home = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt" + ).readText() + val creator = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt" + ).readText() + val onAir = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt" + ).readText() + + assertTrue(home.contains("private val liveActionCoordinator: LiveActionCoordinator by lazy")) + assertTrue(home.contains("liveActionCoordinator.enterLiveRoom(item.roomId)")) + assertFalse(home.contains("liveViewModel.getRoomDetail(item.roomId)")) + + assertTrue(creator.contains("private val liveActionCoordinator: LiveActionCoordinator by lazy")) + assertTrue( + creator.contains( + "liveActionCoordinator.enterLiveRoom(live.liveId, requiresAdultContentAccess = live.isAdult)" + ) + ) + + assertTrue(onAir.contains("private val liveActionCoordinator: LiveActionCoordinator by lazy")) + assertTrue(onAir.contains("liveActionCoordinator.enterLiveRoom(roomId")) + assertFalse(onAir.contains("private fun enterLiveRoom(roomId: Long, roomDetail:")) + assertFalse(onAir.contains("private fun showPaidLiveEntryDialog(")) + } + + @Test + fun `Creator Channel은 라이브 생성 결과를 단일 handler에서 처리한다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt" + ).readText() + val handlerStartIndex = source.indexOf( + "private fun handleLiveCreationResult(result: LiveCreationResult)" + ) + val handlerEndIndex = source.indexOf( + "override val shouldApplySystemBarTopInset", + handlerStartIndex + ) + + assertTrue(source.contains("resolveLiveCreationResult(")) + assertTrue(handlerStartIndex >= 0) + assertTrue(handlerEndIndex > handlerStartIndex) + + val handlerSource = source.substring(handlerStartIndex, handlerEndIndex) + + assertTrue(handlerSource.contains("LiveCreationResult.Ignored -> Unit")) + assertTrue( + handlerSource.contains( + "LiveCreationResult.RefreshOnly -> homeActionDelegate?.refreshHome()" + ) + ) + assertTrue(handlerSource.contains("LiveCreationResult.Created ->")) + assertTrue(handlerSource.contains("is LiveCreationResult.Enter ->")) + } + + @Test + fun `Live coordinator는 unavailable을 adult guard와 오디오 중단보다 먼저 처리한다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt" + ).readText() + val flowSource = sourceSection( + source, + "runLiveEntryDecisionFlow(", + "val loadRoomDetail =" + ) + val unavailableIndex = flowSource.indexOf( + "handleUnavailableLive(roomId, requiresAdultAccess, onUnavailable)" + ) + val adultGuardIndex = flowSource.indexOf( + "AccessRequirement.forAdultContent(requiresAdultAccess)" + ) + val stopAudioIndex = flowSource.indexOf("stopAudioPlayback()") + + assertTrue(unavailableIndex >= 0) + assertTrue(adultGuardIndex >= 0) + assertTrue(stopAudioIndex >= 0) + assertTrue(unavailableIndex < adultGuardIndex) + assertTrue(adultGuardIndex < stopAudioIndex) + assertFalse(source.contains("AccessRequirement.forAdultContent(requiresAdultContentAccess),")) + } + + @Test + fun `Live coordinator는 unavailable 상세 fallback만 adult guard 뒤에 표시한다`() { + val source = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt" + ).readText() + + val effectiveAdultAccess = "requiresAdultContentAccess == true || roomDetail.isAdult" + val fallbackStartIndex = source.indexOf("private fun handleUnavailableLive(") + val fallbackEndIndex = source.indexOf("private fun reservationRoom(", fallbackStartIndex) + + assertTrue(source.contains("val requiresAdultAccess = $effectiveAdultAccess")) + assertTrue(source.indexOf(effectiveAdultAccess) == source.lastIndexOf(effectiveAdultAccess)) + assertTrue( + source.contains("handleUnavailableLive(roomId, requiresAdultAccess, onUnavailable)") + ) + assertTrue(fallbackStartIndex >= 0) + assertTrue(fallbackEndIndex > fallbackStartIndex) + + val fallbackSource = source.substring(fallbackStartIndex, fallbackEndIndex) + val fallbackGuardIndex = fallbackSource.indexOf( + "AccessRequirement.forAdultContent(requiresAdultAccess)" + ) + val callbackIndex = fallbackSource.indexOf("onUnavailable()") + val callbackReturnIndex = fallbackSource.indexOf("return", callbackIndex) + val showDetailIndex = fallbackSource.indexOf("showLiveRoomDetailContent(roomId)") + + assertTrue(fallbackSource.contains("requiresAdultAccess: Boolean")) + assertTrue(fallbackSource.contains("if (onUnavailable != null)")) + assertFalse(fallbackSource.contains("roomDetail.isAdult")) + assertTrue(callbackIndex >= 0) + assertTrue(callbackReturnIndex >= 0) + assertTrue(fallbackGuardIndex >= 0) + assertTrue(showDetailIndex >= 0) + assertTrue(callbackIndex < callbackReturnIndex) + assertTrue(callbackReturnIndex < fallbackGuardIndex) + assertTrue(fallbackGuardIndex < showDetailIndex) + } + + private fun projectFile(relativePath: String): File { + val candidates = listOf(File(relativePath), File("../$relativePath")) + return candidates.firstOrNull { it.exists() } + ?: error("Project file not found: $relativePath") + } + + private fun sourceSection(source: String, startMarker: String, endMarker: String): String { + val startIndex = source.indexOf(startMarker) + assertTrue("Missing start marker: $startMarker", startIndex >= 0) + val endIndex = source.indexOf(endMarker, startIndex + startMarker.length) + assertTrue("Missing end marker: $endMarker", endIndex > startIndex) + return source.substring(startIndex, endIndex) + } + + private fun assertBefore(source: String, expectedBefore: String, expectedAfter: String) { + val beforeIndex = source.indexOf(expectedBefore) + val afterIndex = source.indexOf(expectedAfter) + assertTrue("Missing expected source: $expectedBefore", beforeIndex >= 0) + assertTrue("Missing expected source: $expectedAfter", afterIndex >= 0) + assertTrue("Expected '$expectedBefore' before '$expectedAfter'", beforeIndex < afterIndex) + } +} diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveEntryPolicyTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveEntryPolicyTest.kt index 10a9a9df..43f25e95 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveEntryPolicyTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveEntryPolicyTest.kt @@ -1,9 +1,5 @@ package kr.co.vividnext.sodalive.v2.live.onair -import kr.co.vividnext.sodalive.live.room.GenderRestriction -import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailManager -import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse -import kr.co.vividnext.sodalive.v2.live.onair.model.canEnterHomeOnAirLiveRoom import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test @@ -12,72 +8,24 @@ import java.io.File class HomeOnAirLiveEntryPolicyTest { @Test - fun `channelName이 있으면 입장 분기를 허용한다`() { - assertTrue(canEnterHomeOnAirLiveRoom(roomDetail(channelName = "channel-a"))) - } - - @Test - fun `channelName이 null이면 입장 분기를 중단한다`() { - assertFalse(canEnterHomeOnAirLiveRoom(roomDetail(channelName = null))) - } - - @Test - fun `channelName이 blank이면 입장 분기를 중단한다`() { - assertFalse(canEnterHomeOnAirLiveRoom(roomDetail(channelName = " "))) - } - - @Test - fun `라이브 입장은 로그인 상세 조회 성인 가드 기존 입장 순서를 유지한다`() { + fun `On-air 라이브 입장은 공통 Live Action과 unavailable Toast를 연결한다`() { val source = projectFile( "app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt" ).readText() val entrySource = sourceSection( source = source, startMarker = "private fun enterLiveRoom(roomId: Long) {", - endMarker = "private fun enterLiveRoom(roomId: Long, roomDetail: GetRoomDetailResponse)" + endMarker = "private fun showToast(toastMessage: ToastMessage)" ) - val loginGuard = "ensureV2Access(AccessRequirement.Login)" - val roomDetailQuery = "liveViewModel.getRoomDetail(roomId) { roomDetail ->" - val adultGuard = "ensureV2Access(AccessRequirement.forAdultContent(roomDetail.isAdult))" - val enterRoom = "enterLiveRoom(roomId, roomDetail)" - assertBefore(entrySource, loginGuard, roomDetailQuery) - assertBefore(entrySource, roomDetailQuery, adultGuard) - assertBefore(entrySource, adultGuard, enterRoom) - assertFalse(source.contains("private fun ensureLoginAndAdultAuth")) + assertTrue(source.contains("private val liveActionCoordinator: LiveActionCoordinator by lazy")) + assertTrue(entrySource.contains("liveActionCoordinator.enterLiveRoom(roomId)")) + assertTrue(entrySource.contains("R.string.common_error_unknown")) + assertFalse(source.contains("private fun enterLiveRoom(roomId: Long, roomDetail:")) + assertFalse(source.contains("private fun showPaidLiveEntryDialog(")) + assertFalse(source.contains("private fun showPasswordDialog(")) } - private fun roomDetail(channelName: String?) = GetRoomDetailResponse( - roomId = 1L, - price = 0, - title = "title", - notice = "notice", - isPaid = false, - isAdult = false, - genderRestriction = GenderRestriction.ALL, - isPrivateRoom = false, - password = null, - tags = emptyList(), - channelName = channelName, - beginDateTimeUtc = "2026-06-26T12:00:00", - isNotification = false, - numberOfParticipants = 1, - numberOfParticipantsTotal = 10, - manager = GetRoomDetailManager( - id = 100L, - nickname = "creator", - introduce = "", - youtubeUrl = null, - instagramUrl = null, - kakaoOpenChatUrl = null, - fancimmUrl = null, - xUrl = null, - profileImageUrl = "", - isCreator = true - ), - participatingUsers = emptyList() - ) - private fun sourceSection(source: String, startMarker: String, endMarker: String): String { val start = source.indexOf(startMarker) val end = source.indexOf(endMarker, start) @@ -88,14 +36,6 @@ class HomeOnAirLiveEntryPolicyTest { return source.substring(start, end) } - private fun assertBefore(source: String, expectedBefore: String, expectedAfter: String) { - val beforeIndex = source.indexOf(expectedBefore) - val afterIndex = source.indexOf(expectedAfter, beforeIndex) - - assertTrue("missing source: $expectedBefore", beforeIndex >= 0) - assertTrue("missing source after $expectedBefore: $expectedAfter", afterIndex > beforeIndex) - } - private fun projectFile(relativePath: String): File { val candidates = listOf(File(relativePath), File("../$relativePath")) return candidates.firstOrNull { it.exists() }