fix(navigation): 라이브 재생 중 외부 이동을 확인 후 처리한다

This commit is contained in:
Yu Sung
2026-03-06 18:56:49 +09:00
parent 33f9ddfd12
commit cab9795557
5 changed files with 219 additions and 23 deletions

View File

@@ -747,6 +747,13 @@ enum I18n {
static var quitTitle: String { pick(ko: "라이브 나가기", en: "Leave live", ja: "ライブを退出") }
static var quitDesc: String { pick(ko: "라이브에서 나가시겠습니까?", en: "Do you want to leave the live?", ja: "ライブから退出しますか?") }
static var leaveLiveForNavigationDesc: String {
pick(
ko: "다른 페이지로 이동시 현재 라이브에서 나가게 됩니다.",
en: "Moving to another page will leave the current live.",
ja: "別のページに移動すると、現在のライブから退出します。"
)
}
static var endTitle: String { pick(ko: "라이브 종료", en: "End live", ja: "ライブ終了") }
static var endDesc: String {

View File

@@ -862,6 +862,9 @@ struct LiveRoomViewV2: View {
waterProgress = 0
}
}
.onReceive(NotificationCenter.default.publisher(for: .requestLiveRoomQuitForExternalNavigation)) { _ in
viewModel.quitRoom()
}
.ignoresSafeArea(.keyboard)
.edgesIgnoringSafeArea(keyboardHandler.keyboardHeight > 0 ? .bottom : .init())
.sheet(

View File

@@ -36,6 +36,9 @@ struct HomeView: View {
@State private var isShowAuthView: Bool = false
@State private var isShowAuthConfirmView: Bool = false
@State private var pendingAction: (() -> Void)? = nil
@State private var isShowLeaveLiveNavigationDialog: Bool = false
@State private var pendingExternalNavigationAction: (() -> Void)? = nil
@State private var pendingExternalNavigationCancelAction: (() -> Void)? = nil
@State private var payload = Payload()
var body: some View {
@@ -265,6 +268,21 @@ struct HomeView: View {
if appState.isShowPlayer {
LiveRoomViewV2()
}
if isShowLeaveLiveNavigationDialog {
SodaDialog(
title: I18n.Common.alertTitle,
desc: I18n.LiveRoom.leaveLiveForNavigationDesc,
confirmButtonTitle: I18n.Common.confirm,
confirmButtonAction: {
confirmExternalNavigation()
},
cancelButtonTitle: I18n.Common.cancel,
cancelButtonAction: {
cancelExternalNavigation()
}
)
}
}
.edgesIgnoringSafeArea(.bottom)
.fullScreenCover(isPresented: $isShowAuthView) {
@@ -296,47 +314,124 @@ struct HomeView: View {
}
}
.valueChanged(value: appState.pushRoomId) { value in
DispatchQueue.main.async {
appState.setAppStep(step: .main)
if value > 0 {
liveViewModel.enterLiveRoom(roomId: value)
guard value > 0 else {
return
}
let roomId = value
appState.pushRoomId = 0
DispatchQueue.main.async {
handleExternalNavigationRequest(
value: roomId,
navigationAction: {
appState.setAppStep(step: .main)
liveViewModel.enterLiveRoom(roomId: roomId)
},
cancelAction: {
appState.pushRoomId = 0
}
)
}
}
.valueChanged(value: appState.pushChannelId) { value in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if value > 0 {
appState.setAppStep(step: .main)
appState.setAppStep(step: .creatorDetail(userId: value))
guard value > 0 else {
return
}
let channelId = value
appState.pushChannelId = 0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
handleExternalNavigationRequest(
value: channelId,
navigationAction: {
appState.setAppStep(step: .main)
appState.setAppStep(step: .creatorDetail(userId: channelId))
},
cancelAction: {
appState.pushChannelId = 0
}
)
}
}
.valueChanged(value: appState.pushMessageId) { value in
guard value > 0 else {
return
}
let messageId = value
appState.pushMessageId = 0
DispatchQueue.main.async {
handleExternalNavigationRequest(
value: messageId,
navigationAction: {
appState.setAppStep(step: .main)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if value > 0 {
appState.setAppStep(step: .message)
}
},
cancelAction: {
appState.pushMessageId = 0
}
)
}
}
.valueChanged(value: appState.pushAudioContentId) { value in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if value > 0 {
appState.setAppStep(step: .main)
appState.setAppStep(step: .contentDetail(contentId: value))
guard value > 0 else {
return
}
let contentId = value
appState.pushAudioContentId = 0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
handleExternalNavigationRequest(
value: contentId,
navigationAction: {
appState.setAppStep(step: .main)
appState.setAppStep(step: .contentDetail(contentId: contentId))
},
cancelAction: {
appState.pushAudioContentId = 0
}
)
}
}
.valueChanged(value: appState.pushSeriesId) { value in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if value > 0 {
appState.setAppStep(step: .main)
appState.setAppStep(step: .seriesDetail(seriesId: value))
guard value > 0 else {
return
}
let seriesId = value
appState.pushSeriesId = 0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
handleExternalNavigationRequest(
value: seriesId,
navigationAction: {
appState.setAppStep(step: .main)
appState.setAppStep(step: .seriesDetail(seriesId: seriesId))
},
cancelAction: {
appState.pushSeriesId = 0
}
)
}
}
.valueChanged(value: appState.isShowPlayer) { isShowPlayer in
guard !isShowPlayer,
let pendingExternalNavigationAction = pendingExternalNavigationAction else {
return
}
self.pendingExternalNavigationAction = nil
self.pendingExternalNavigationCancelAction = nil
DispatchQueue.main.async {
pendingExternalNavigationAction()
}
}
.onAppear {
@@ -378,6 +473,42 @@ struct HomeView: View {
)
}
private func handleExternalNavigationRequest(
value: Int,
navigationAction: @escaping () -> Void,
cancelAction: @escaping () -> Void
) {
guard value > 0 else {
return
}
if appState.isShowPlayer {
pendingExternalNavigationAction = navigationAction
pendingExternalNavigationCancelAction = cancelAction
isShowLeaveLiveNavigationDialog = true
return
}
navigationAction()
}
private func confirmExternalNavigation() {
guard pendingExternalNavigationAction != nil else {
isShowLeaveLiveNavigationDialog = false
return
}
isShowLeaveLiveNavigationDialog = false
NotificationCenter.default.post(name: .requestLiveRoomQuitForExternalNavigation, object: nil)
}
private func cancelExternalNavigation() {
isShowLeaveLiveNavigationDialog = false
pendingExternalNavigationAction = nil
pendingExternalNavigationCancelAction?()
pendingExternalNavigationCancelAction = nil
}
private func pushTokenUpdate() {
let pushToken = UserDefaults.string(forKey: .pushToken)
if !pushToken.trimmingCharacters(in: .whitespaces).isEmpty {
@@ -386,6 +517,10 @@ struct HomeView: View {
}
}
extension Notification.Name {
static let requestLiveRoomQuitForExternalNavigation = Notification.Name("REQUEST_LIVE_ROOM_QUIT_FOR_EXTERNAL_NAVIGATION")
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()

View File

@@ -0,0 +1,29 @@
# 20260306 라이브룸 외부 이동 확인 다이얼로그 구현
## 작업 목표
- 라이브룸(`LiveRoomViewV2`)에 입장한 상태에서 딥링크/푸시로 다른 페이지 이동 요청이 들어오면 즉시 이동하지 않는다.
- `SodaDialog`로 확인/취소 다이얼로그를 노출하고, 확인을 눌렀을 때만 이동한다.
- 다이얼로그 문구는 국제화(`I18n`)를 적용한다.
## 구현 체크리스트
- [x] 딥링크/푸시 이동 트리거 지점 확인 (`HomeView`)
- [x] 라이브룸 상태에서 이동 요청 보류 및 확인 다이얼로그 노출
- [x] 확인 시 라이브룸 종료 트리거 후 보류된 이동 실행
- [x] 취소 시 보류된 이동 취소 및 push/deeplink 값 정리
- [x] 다이얼로그 문구 국제화 키 추가 및 적용
- [x] 진단/빌드 검증 수행
## 검증 기록
- 무엇/왜/어떻게: 딥링크/푸시 이동 처리 지점(`HomeView``push*` `valueChanged`)을 조사하고, 라이브룸 재생 중에는 이동 액션을 보류한 뒤 `SodaDialog` 확인 시에만 `LiveRoomViewV2`로 종료 요청(Notification) -> 라이브 종료 후 보류 액션 실행 흐름으로 변경했다.
- 실행 명령: 백그라운드 탐색 `bg_781ddd35`, `bg_38f4cad5`
- 결과: 딥링크/푸시 진입 경로(`AppDelegate` -> `AppState.push*` -> `HomeView`/`SplashView`)와 I18n 패턴(`I18n.Common`/`I18n.LiveRoom`)을 확인했다.
- 실행 명령: `lsp_diagnostics` (`SodaLive/Sources/Main/Home/HomeView.swift`, `SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift`, `SodaLive/Sources/I18n/I18n.swift`)
- 결과: `LiveRoomViewV2.swift`는 진단 오류 없음. `HomeView.swift`/`I18n.swift`는 SourceKit 인덱싱 컨텍스트에서 외부 모듈/심볼 미해석 오탐이 발생했고, 실제 유효성은 빌드로 확인했다.
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`
- 결과: `** BUILD SUCCEEDED **`
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
- 결과: `** BUILD SUCCEEDED **`
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test`
- 결과: `Scheme SodaLive is not currently configured for the test action.`
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
- 결과: `Scheme SodaLive-dev is not currently configured for the test action.`

View File

@@ -0,0 +1,22 @@
# 20260306 홈 푸시 이동 트리거 보정
## 작업 목표
- `HomeView`에서 푸시 탭 후 `pushRoomId` 외 경로(`pushChannelId`, `pushMessageId`, `pushAudioContentId`, `pushSeriesId`)가 누락되는 현상을 보정한다.
## 구현 체크리스트
- [x] `push*` `valueChanged` 트리거 누락 원인 확인
- [x] `HomeView`의 푸시 처리 로직 보정
- [x] 진단/빌드/테스트 검증
## 검증 기록
- 무엇/왜/어떻게: `HomeView`의 푸시 처리에서 `pushChannelId`, `pushMessageId`, `pushAudioContentId`, `pushSeriesId`(및 `pushRoomId`) 값을 소비하지 않으면 동일 ID 재수신 시 `onChange`가 재발화되지 않아 이동 누락이 발생할 수 있어, 각 `valueChanged` 시작 시 로컬 변수에 보관 후 즉시 해당 `push*` 값을 `0`으로 초기화하도록 수정했다.
- 실행 명령: `lsp_diagnostics` (`SodaLive/Sources/Main/Home/HomeView.swift`)
- 결과: SourceKit 컨텍스트에서 `No such module 'Firebase'` 오탐이 발생했고, 실제 컴파일 유효성은 빌드로 검증했다.
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`
- 결과: 병렬 빌드 시 1회 `build.db` lock 실패 후 단독 재실행에서 `** BUILD SUCCEEDED **`.
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
- 결과: `** BUILD SUCCEEDED **`.
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test`
- 결과: `Scheme SodaLive is not currently configured for the test action.`
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
- 결과: `Scheme SodaLive-dev is not currently configured for the test action.`