From cab979555744119d36b94df73a089bb12ebefe79 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Fri, 6 Mar 2026 18:56:49 +0900 Subject: [PATCH] =?UTF-8?q?fix(navigation):=20=EB=9D=BC=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=20=EC=9E=AC=EC=83=9D=20=EC=A4=91=20=EC=99=B8=EB=B6=80=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=EC=9D=84=20=ED=99=95=EC=9D=B8=20=ED=9B=84=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SodaLive/Sources/I18n/I18n.swift | 7 + .../Sources/Live/Room/V2/LiveRoomViewV2.swift | 3 + SodaLive/Sources/Main/Home/HomeView.swift | 181 +++++++++++++++--- ...20260306_라이브룸외부이동확인다이얼로그.md | 29 +++ docs/20260306_홈푸시이동트리거보정.md | 22 +++ 5 files changed, 219 insertions(+), 23 deletions(-) create mode 100644 docs/20260306_라이브룸외부이동확인다이얼로그.md create mode 100644 docs/20260306_홈푸시이동트리거보정.md diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index b07f3dc..9dd234d 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -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 { diff --git a/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift b/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift index 154ccf2..f39f65a 100644 --- a/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift +++ b/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift @@ -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( diff --git a/SodaLive/Sources/Main/Home/HomeView.swift b/SodaLive/Sources/Main/Home/HomeView.swift index 5943006..9f0cf9d 100644 --- a/SodaLive/Sources/Main/Home/HomeView.swift +++ b/SodaLive/Sources/Main/Home/HomeView.swift @@ -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 + guard value > 0 else { + return + } + + let roomId = value + appState.pushRoomId = 0 + DispatchQueue.main.async { - appState.setAppStep(step: .main) - - if value > 0 { - liveViewModel.enterLiveRoom(roomId: value) - } + handleExternalNavigationRequest( + value: roomId, + navigationAction: { + appState.setAppStep(step: .main) + liveViewModel.enterLiveRoom(roomId: roomId) + }, + cancelAction: { + appState.pushRoomId = 0 + } + ) } } .valueChanged(value: appState.pushChannelId) { value in + guard value > 0 else { + return + } + + let channelId = value + appState.pushChannelId = 0 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - if value > 0 { - appState.setAppStep(step: .main) - appState.setAppStep(step: .creatorDetail(userId: value)) - } + 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 { - appState.setAppStep(step: .main) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - if value > 0 { - appState.setAppStep(step: .message) + handleExternalNavigationRequest( + value: messageId, + navigationAction: { + appState.setAppStep(step: .main) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + appState.setAppStep(step: .message) + } + }, + cancelAction: { + appState.pushMessageId = 0 } - } + ) } } .valueChanged(value: appState.pushAudioContentId) { value in + guard value > 0 else { + return + } + + let contentId = value + appState.pushAudioContentId = 0 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - if value > 0 { - appState.setAppStep(step: .main) - appState.setAppStep(step: .contentDetail(contentId: value)) - } + handleExternalNavigationRequest( + value: contentId, + navigationAction: { + appState.setAppStep(step: .main) + appState.setAppStep(step: .contentDetail(contentId: contentId)) + }, + cancelAction: { + appState.pushAudioContentId = 0 + } + ) } } .valueChanged(value: appState.pushSeriesId) { value in + guard value > 0 else { + return + } + + let seriesId = value + appState.pushSeriesId = 0 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - if value > 0 { - appState.setAppStep(step: .main) - appState.setAppStep(step: .seriesDetail(seriesId: value)) - } + 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 { @@ -377,6 +472,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) @@ -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() diff --git a/docs/20260306_라이브룸외부이동확인다이얼로그.md b/docs/20260306_라이브룸외부이동확인다이얼로그.md new file mode 100644 index 0000000..cfb4800 --- /dev/null +++ b/docs/20260306_라이브룸외부이동확인다이얼로그.md @@ -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.` diff --git a/docs/20260306_홈푸시이동트리거보정.md b/docs/20260306_홈푸시이동트리거보정.md new file mode 100644 index 0000000..b3b2c6d --- /dev/null +++ b/docs/20260306_홈푸시이동트리거보정.md @@ -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.`