fix(navigation): 라이브 재생 중 외부 이동을 확인 후 처리한다
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
29
docs/20260306_라이브룸외부이동확인다이얼로그.md
Normal file
29
docs/20260306_라이브룸외부이동확인다이얼로그.md
Normal 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.`
|
||||
22
docs/20260306_홈푸시이동트리거보정.md
Normal file
22
docs/20260306_홈푸시이동트리거보정.md
Normal 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.`
|
||||
Reference in New Issue
Block a user