fix(live-room): 종료 경합의 중복 재조회와 오류 토스트를 막는다
This commit is contained in:
@@ -170,6 +170,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
private var isSpeakerMute = false
|
||||
private var isMicrophoneMute = false
|
||||
private var isSpeaker = false
|
||||
private var hasKnownHostAbsence = false
|
||||
|
||||
private var isCapturePrivacyMuted = false
|
||||
private var isScreenRecordingActive = false
|
||||
@@ -2304,13 +2305,26 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
override fun onUserOffline(uid: Int, reason: Int) {
|
||||
super.onUserOffline(uid, reason)
|
||||
Logger.e("onUserOffline - uid: $uid")
|
||||
if (viewModel.isEqualToHostId(uid)) {
|
||||
|
||||
val offlineAction = resolveLiveRoomOfflineAction(
|
||||
isHostOffline = viewModel.isEqualToHostId(uid),
|
||||
hasKnownHostAbsence = hasKnownHostAbsence
|
||||
)
|
||||
|
||||
if (offlineAction.shouldMarkHostAbsence) {
|
||||
hasKnownHostAbsence = true
|
||||
}
|
||||
|
||||
if (offlineAction.shouldFinishRoom) {
|
||||
handler.post {
|
||||
showToast(getString(R.string.screen_live_room_closed))
|
||||
finish()
|
||||
}
|
||||
} else {
|
||||
viewModel.getRoomInfo(roomId)
|
||||
return
|
||||
}
|
||||
|
||||
if (offlineAction.shouldRefreshRoomInfo) {
|
||||
viewModel.getRoomInfo(roomId, suppressRoomNotFoundError = true)
|
||||
speakerListAdapter.muteSpeakers.remove(uid)
|
||||
}
|
||||
}
|
||||
@@ -2669,7 +2683,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||
}
|
||||
} else if (eventType == RtmConstants.RtmPresenceEventType.REMOTE_LEAVE) {
|
||||
if (!viewModel.isEqualToHostId(memberId.toInt())) {
|
||||
viewModel.getRoomInfo(roomId)
|
||||
viewModel.getRoomInfo(roomId, suppressRoomNotFoundError = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package kr.co.vividnext.sodalive.live.room
|
||||
|
||||
internal data class LiveRoomOfflineAction(
|
||||
val shouldMarkHostAbsence: Boolean,
|
||||
val shouldFinishRoom: Boolean,
|
||||
val shouldRefreshRoomInfo: Boolean
|
||||
)
|
||||
|
||||
internal fun resolveLiveRoomOfflineAction(
|
||||
isHostOffline: Boolean,
|
||||
hasKnownHostAbsence: Boolean
|
||||
): LiveRoomOfflineAction {
|
||||
if (hasKnownHostAbsence) {
|
||||
return LiveRoomOfflineAction(
|
||||
shouldMarkHostAbsence = false,
|
||||
shouldFinishRoom = false,
|
||||
shouldRefreshRoomInfo = false
|
||||
)
|
||||
}
|
||||
|
||||
if (isHostOffline) {
|
||||
return LiveRoomOfflineAction(
|
||||
shouldMarkHostAbsence = true,
|
||||
shouldFinishRoom = true,
|
||||
shouldRefreshRoomInfo = false
|
||||
)
|
||||
}
|
||||
|
||||
return LiveRoomOfflineAction(
|
||||
shouldMarkHostAbsence = false,
|
||||
shouldFinishRoom = false,
|
||||
shouldRefreshRoomInfo = true
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package kr.co.vividnext.sodalive.live.room
|
||||
|
||||
private val ignorableLiveRoomNotFoundMessages = setOf(
|
||||
"라이브 정보가 없습니다.",
|
||||
"해당하는 라이브의 정보가 없습니다.",
|
||||
"Live session information not found.",
|
||||
"該当するライブの情報がありません。"
|
||||
)
|
||||
|
||||
internal fun shouldSuppressLiveRoomInfoError(
|
||||
message: String?,
|
||||
suppressRoomNotFoundError: Boolean
|
||||
): Boolean {
|
||||
if (!suppressRoomNotFoundError) {
|
||||
return false
|
||||
}
|
||||
|
||||
val normalizedMessage = message?.trim().orEmpty()
|
||||
if (normalizedMessage.isBlank()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return normalizedMessage in ignorableLiveRoomNotFoundMessages
|
||||
}
|
||||
@@ -232,7 +232,12 @@ class LiveRoomViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
fun getRoomInfo(roomId: Long, userId: Int = 0, onSuccess: (String) -> Unit = {}) {
|
||||
fun getRoomInfo(
|
||||
roomId: Long,
|
||||
userId: Int = 0,
|
||||
suppressRoomNotFoundError: Boolean = false,
|
||||
onSuccess: (String) -> Unit = {}
|
||||
) {
|
||||
compositeDisposable.add(
|
||||
repository.getRoomInfo(roomId, "Bearer ${SharedPreferenceManager.token}")
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -266,6 +271,10 @@ class LiveRoomViewModel(
|
||||
onSuccess(nickname)
|
||||
}
|
||||
} else {
|
||||
if (shouldSuppressLiveRoomInfoError(it.message, suppressRoomNotFoundError)) {
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package kr.co.vividnext.sodalive.live.room
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class LiveRoomOfflineActionPolicyTest {
|
||||
|
||||
@Test
|
||||
fun `방장 offline이 처음 감지되면 종료만 수행한다`() {
|
||||
val action = resolveLiveRoomOfflineAction(
|
||||
isHostOffline = true,
|
||||
hasKnownHostAbsence = false
|
||||
)
|
||||
|
||||
assertTrue(action.shouldMarkHostAbsence)
|
||||
assertTrue(action.shouldFinishRoom)
|
||||
assertFalse(action.shouldRefreshRoomInfo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `방장이 아직 남아있으면 비방장 offline에서 방 정보를 재조회한다`() {
|
||||
val action = resolveLiveRoomOfflineAction(
|
||||
isHostOffline = false,
|
||||
hasKnownHostAbsence = false
|
||||
)
|
||||
|
||||
assertFalse(action.shouldMarkHostAbsence)
|
||||
assertFalse(action.shouldFinishRoom)
|
||||
assertTrue(action.shouldRefreshRoomInfo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `방장 부재가 이미 확정되면 후속 offline에서는 아무 동작도 하지 않는다`() {
|
||||
val action = resolveLiveRoomOfflineAction(
|
||||
isHostOffline = false,
|
||||
hasKnownHostAbsence = true
|
||||
)
|
||||
|
||||
assertFalse(action.shouldMarkHostAbsence)
|
||||
assertFalse(action.shouldFinishRoom)
|
||||
assertFalse(action.shouldRefreshRoomInfo)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package kr.co.vividnext.sodalive.live.room
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class LiveRoomRoomInfoErrorPolicyTest {
|
||||
|
||||
@Test
|
||||
fun `leave 재조회에서 라이브 정보가 없습니다 메시지는 숨긴다`() {
|
||||
assertTrue(
|
||||
shouldSuppressLiveRoomInfoError(
|
||||
message = "라이브 정보가 없습니다.",
|
||||
suppressRoomNotFoundError = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `suppress 플래그가 없으면 같은 메시지도 숨기지 않는다`() {
|
||||
assertFalse(
|
||||
shouldSuppressLiveRoomInfoError(
|
||||
message = "라이브 정보가 없습니다.",
|
||||
suppressRoomNotFoundError = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `관련 없는 오류 메시지는 그대로 노출한다`() {
|
||||
assertFalse(
|
||||
shouldSuppressLiveRoomInfoError(
|
||||
message = "네트워크 오류가 발생했습니다.",
|
||||
suppressRoomNotFoundError = true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
47
docs/20260413_라이브룸방장부재중복조회방지.md
Normal file
47
docs/20260413_라이브룸방장부재중복조회방지.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# 20260413 라이브룸 방장 부재 중복 조회 방지
|
||||
|
||||
## 작업 체크리스트
|
||||
- [x] `LiveRoomActivity.onUserOffline`와 방장 부재 판별 흐름을 기준으로 중복 `getRoomInfo` 호출 조건을 확정한다.
|
||||
QA: 방장 offline 감지 후 후속 user offline 콜백에서는 방 정보 재조회 조건이 false여야 한다.
|
||||
- [x] 방장 부재가 확정된 뒤에는 추가 `getRoomInfo(roomId)`를 호출하지 않도록 최소 가드를 반영한다.
|
||||
QA: 비방장 offline은 기존처럼 재조회하되, 방장 offline 이후에는 재조회가 차단되어야 한다.
|
||||
- [x] 변경 파일 검증과 결과 기록을 남긴다.
|
||||
QA: 관련 단위 테스트, `:app:testDebugUnitTest`, `:app:assembleDebug` 결과를 문서 하단에 기록한다.
|
||||
- [x] leave/offline 신호로 발생한 `getRoomInfo`에서 종료 race의 room-not-found 메시지만 숨길 범위를 확정한다.
|
||||
QA: leave/offline 재조회에서만 room-not-found suppress가 적용되고, 다른 `getRoomInfo` 실패는 기존처럼 노출되어야 한다.
|
||||
- [x] 종료 race에서 내려오는 `라이브 정보가 없습니다.` 메시지를 사용자에게 노출하지 않도록 최소 변경을 반영한다.
|
||||
QA: leave/offline 기반 `getRoomInfo`가 room-not-found를 받아도 토스트가 발생하지 않아야 한다.
|
||||
|
||||
## 검증 기록
|
||||
- 2026-04-13
|
||||
- 무엇: `shouldSuppressLiveRoomInfoError` 허용 목록에 실제 테스트에서 사용한 한국어 room-not-found 문구(`라이브 정보가 없습니다.`)를 추가했다.
|
||||
- 왜: 정책 함수는 다국어/문구 변형을 지원하도록 의도됐지만 한국어 변형 1종이 누락돼 `LiveRoomRoomInfoErrorPolicyTest`가 실패했다.
|
||||
- 어떻게:
|
||||
- 수정 파일: `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomRoomInfoErrorPolicy.kt`
|
||||
- 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug`
|
||||
- 결과: `BUILD SUCCESSFUL`
|
||||
- 2026-04-13
|
||||
- 무엇: `LiveRoomActivity.onUserOffline`에서 방장 부재가 한 번 확인되면 후속 offline 콜백에서는 방 정보 재조회를 하지 않도록 `hasKnownHostAbsence` 가드와 `resolveLiveRoomOfflineAction` 정책 함수를 추가했다.
|
||||
- 왜: 여러 사용자가 동시에 나가는 상황에서 방장 offline 이후에도 비방장 offline 콜백이 이어지면 `viewModel.getRoomInfo(roomId)`가 중복 호출될 수 있어, 방장이 없는 상태에서는 재조회를 막아야 했다.
|
||||
- 어떻게:
|
||||
- 수정 파일: `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt`
|
||||
- 추가 파일: `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomOfflineActionPolicy.kt`
|
||||
- 추가 파일: `app/src/test/java/kr/co/vividnext/sodalive/live/room/LiveRoomOfflineActionPolicyTest.kt`
|
||||
- 실행 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.live.room.LiveRoomOfflineActionPolicyTest"`
|
||||
- 결과: 최초 실행은 `Unresolved reference 'resolveLiveRoomOfflineAction'`로 실패했고, 정책 함수 추가 후 `BUILD SUCCESSFUL`로 통과했다.
|
||||
- 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug`
|
||||
- 결과: `BUILD SUCCESSFUL`
|
||||
- 진단 도구: Kotlin(`.kt`)용 LSP 서버 미구성으로 `lsp_diagnostics` 실행 불가 확인
|
||||
- 2026-04-13
|
||||
- 무엇: leave/offline 신호로 발생한 `getRoomInfo`에만 `suppressRoomNotFoundError` 플래그를 추가하고, 종료 race에서 내려오는 `라이브 정보가 없습니다.`/room-not-found 메시지는 토스트로 노출하지 않도록 `shouldSuppressLiveRoomInfoError` 정책 함수를 연결했다.
|
||||
- 왜: 일반 유저 leave/offline 신호가 방장 종료 신호보다 먼저 도착하면 이미 종료된 방에 대한 재조회가 먼저 실행될 수 있고, 이때의 room-not-found는 실제 장애가 아니라 종료 경합에 따른 기대된 실패이기 때문이다.
|
||||
- 어떻게:
|
||||
- 수정 파일: `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt`
|
||||
- 수정 파일: `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt`
|
||||
- 추가 파일: `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomRoomInfoErrorPolicy.kt`
|
||||
- 추가 파일: `app/src/test/java/kr/co/vividnext/sodalive/live/room/LiveRoomRoomInfoErrorPolicyTest.kt`
|
||||
- 실행 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.live.room.LiveRoomRoomInfoErrorPolicyTest"`
|
||||
- 결과: 최초 실행은 `Unresolved reference 'shouldSuppressLiveRoomInfoError'`로 실패했고, 정책 함수 추가 후 `BUILD SUCCESSFUL`로 통과했다.
|
||||
- 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug`
|
||||
- 결과: `BUILD SUCCESSFUL`
|
||||
- 진단 도구: Kotlin(`.kt`)용 LSP 서버 미구성으로 `lsp_diagnostics` 실행 불가 확인
|
||||
Reference in New Issue
Block a user