diff --git a/Keychy/Keychy/Core/DeepLink/DeepLinkManager.swift b/Keychy/Keychy/Core/DeepLink/DeepLinkManager.swift index 181498ca5..6654f4ffd 100644 --- a/Keychy/Keychy/Core/DeepLink/DeepLinkManager.swift +++ b/Keychy/Keychy/Core/DeepLink/DeepLinkManager.swift @@ -15,12 +15,19 @@ enum DeepLinkType { case notification // 푸시 알림 } +enum DeepLinkError { + case notFound // 존재하지 않는 링크 + case missingType // type 필드 없음 + case typeMismatch // URL 타입과 문서 타입 불일치 +} + @Observable class DeepLinkManager { static let shared = DeepLinkManager() var pendingPostOfficeId: String? var pendingDeepLinkType: DeepLinkType? + var pendingError: DeepLinkError? private init() {} @@ -31,11 +38,14 @@ class DeepLinkManager { // 1. Firestore에서 PostOffice 조회 db.collection("PostOffice").document(postOfficeId).getDocument { snapshot, error in - guard let data = snapshot?.data(), - let documentTypeString = data["type"] as? String, - let documentType = PostOfficeType(rawValue: documentTypeString) else { + // 문서가 존재하지 않거나 에러 발생 + guard error == nil, let data = snapshot?.data() else { print("존재하지 않는 링크입니다") - // TODO: UI 상 처리 필요 + DispatchQueue.main.async { + self.pendingPostOfficeId = postOfficeId + self.pendingDeepLinkType = type + self.pendingError = .notFound + } return } @@ -43,7 +53,11 @@ class DeepLinkManager { guard let documentTypeString = data["type"] as? String, let documentType = PostOfficeType(rawValue: documentTypeString) else { print("type 필드 없음") - // TODO: UI 상 처리 필요 + DispatchQueue.main.async { + self.pendingPostOfficeId = postOfficeId + self.pendingDeepLinkType = type + self.pendingError = .missingType + } return } @@ -52,31 +66,36 @@ class DeepLinkManager { guard isValid else { print("타입 불일치 - URL: \(type), Document: \(documentType)") - // TODO: UI 상 처리 필요 + DispatchQueue.main.async { + self.pendingPostOfficeId = postOfficeId + self.pendingDeepLinkType = type + self.pendingError = .typeMismatch + } return } - self.pendingPostOfficeId = postOfficeId - self.pendingDeepLinkType = type - // 4. 검증 통과 → 정상 처리 DispatchQueue.main.async { self.pendingPostOfficeId = postOfficeId self.pendingDeepLinkType = type + self.pendingError = nil } } } - func consumePendingDeepLink() -> (postOfficeId: String, type: DeepLinkType)? { + func consumePendingDeepLink() -> (postOfficeId: String, type: DeepLinkType, error: DeepLinkError?)? { guard let postOfficeId = pendingPostOfficeId, let type = pendingDeepLinkType else { return nil } + let error = pendingError + self.pendingPostOfficeId = nil self.pendingDeepLinkType = nil + self.pendingError = nil - return (postOfficeId, type) + return (postOfficeId, type, error) } static func createTestReceiveLink(postOfficeId: String) -> URL? { diff --git a/Keychy/Keychy/Presentation/Collection/ViewModels/KeyringCollectViewModel.swift b/Keychy/Keychy/Presentation/Collection/ViewModels/KeyringCollectViewModel.swift index 433dcfd55..78014890e 100644 --- a/Keychy/Keychy/Presentation/Collection/ViewModels/KeyringCollectViewModel.swift +++ b/Keychy/Keychy/Presentation/Collection/ViewModels/KeyringCollectViewModel.swift @@ -19,6 +19,9 @@ class KeyringCollectViewModel { var isAccepting: Bool = false var isAccepted: Bool = false + // DeepLink Error + var hasDeepLinkError: Bool = false + // Alert States var showAcceptCompleteAlert: Bool = false var showInvenFullAlert: Bool = false @@ -27,13 +30,20 @@ class KeyringCollectViewModel { private let postOfficeId: String // MARK: - Init - init(collectionViewModel: CollectionViewModel, postOfficeId: String) { + init(collectionViewModel: CollectionViewModel, postOfficeId: String, deepLinkError: DeepLinkError? = nil) { self.collectionViewModel = collectionViewModel self.postOfficeId = postOfficeId + self.hasDeepLinkError = (deepLinkError != nil) } // MARK: - 데이터 로드 func loadKeyringData() { + // DeepLink 에러가 있으면 바로 종료 (errorView로 연결) + if hasDeepLinkError { + self.isLoading = false + return + } + print("PostOffice 데이터 로드 시작") collectionViewModel.fetchPostOfficeData(postOfficeId: postOfficeId) { postOfficeData in @@ -143,11 +153,7 @@ class KeyringCollectViewModel { false } - var backgroundImageName: ImageResource { - // 로딩 중이 아니고, (이미 수락됨 또는 에러 또는 keyring이 nil) - if !isLoading && (keyring == nil) { - return .whiteBackground - } - return .greenBackground + var shouldShowWhiteBackground: Bool { + hasDeepLinkError || (!isLoading && (keyring == nil)) } } diff --git a/Keychy/Keychy/Presentation/Collection/ViewModels/KeyringReceiveViewModel.swift b/Keychy/Keychy/Presentation/Collection/ViewModels/KeyringReceiveViewModel.swift index 96d4ea943..76985bee7 100644 --- a/Keychy/Keychy/Presentation/Collection/ViewModels/KeyringReceiveViewModel.swift +++ b/Keychy/Keychy/Presentation/Collection/ViewModels/KeyringReceiveViewModel.swift @@ -20,6 +20,9 @@ class KeyringReceiveViewModel { var isAccepted: Bool = false var isAlreadyReceived: Bool = false + // DeepLink Error + var hasDeepLinkError: Bool = false + // Alert States var showAcceptCompleteAlert: Bool = false var showInvenFullAlert: Bool = false @@ -29,13 +32,20 @@ class KeyringReceiveViewModel { private let postOfficeId: String // MARK: - Init - init(collectionViewModel: CollectionViewModel, postOfficeId: String) { + init(collectionViewModel: CollectionViewModel, postOfficeId: String, deepLinkError: DeepLinkError? = nil) { self.collectionViewModel = collectionViewModel self.postOfficeId = postOfficeId + self.hasDeepLinkError = (deepLinkError != nil) } // MARK: - 데이터 로드 func loadKeyringData() { + // DeepLink 에러가 있으면 바로 종료 (errorView로 연결) + if hasDeepLinkError { + self.isLoading = false + return + } + print("PostOffice 데이터 로드 시작") collectionViewModel.fetchPostOfficeData(postOfficeId: postOfficeId) { postOfficeData in @@ -196,11 +206,7 @@ class KeyringReceiveViewModel { false } - var backgroundImageName: String { - // 로딩 중이 아니고, (이미 수락됨 또는 에러 또는 keyring이 nil) - if !isLoading && (isAlreadyReceived || keyring == nil) { - return "WhiteBackground" - } - return "GreenBackground" + var shouldShowWhiteBackground: Bool { + hasDeepLinkError || (!isLoading && (isAlreadyReceived || keyring == nil)) } } diff --git a/Keychy/Keychy/Presentation/Collection/Views/Package/KeyringCollectView.swift b/Keychy/Keychy/Presentation/Collection/Views/Package/KeyringCollectView.swift index b78bcc8f1..63539a965 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Package/KeyringCollectView.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Package/KeyringCollectView.swift @@ -13,11 +13,12 @@ struct KeyringCollectView: View { @State var viewModel: KeyringCollectViewModel @State private var scene: KeyringCellScene? - init(viewModel: CollectionViewModel, postOfficeId: String) { + init(viewModel: CollectionViewModel, postOfficeId: String, deepLinkError: DeepLinkError? = nil) { _viewModel = State( initialValue: KeyringCollectViewModel( collectionViewModel: viewModel, - postOfficeId: postOfficeId + postOfficeId: postOfficeId, + deepLinkError: deepLinkError ) ) } @@ -38,10 +39,15 @@ struct KeyringCollectView: View { alertOverlayView(geometry: geometry) - customNavigationBar - .blur(radius: viewModel.shouldApplyBlur ? 15 : 0) - .adaptiveTopPadding() - .zIndex(0) + // 정상 키링 수신 화면일 때만 네비게이션 바 표시 + if !viewModel.isLoading && + !viewModel.hasDeepLinkError && + viewModel.keyring != nil { + customNavigationBar + .blur(radius: viewModel.shouldApplyBlur ? 15 : 0) + .adaptiveTopPadding() + .zIndex(0) + } } } .ignoresSafeArea() @@ -100,23 +106,34 @@ struct KeyringCollectView: View { private var errorView: some View { VStack(spacing: 20) { - VStack(spacing: 0) { - Image(.emptyViewIcon) + VStack(spacing: 10) { + Image(.noInternetBangMark) .resizable() - .frame(width: 124, height: 111) + .frame(width: 26, height: 48) + .padding(.bottom, 15) - Text("키링을 불러올 수 없습니다.") + Text("키링을 불러 올 수 없습니다") + .typography(.suit18SB) + .foregroundColor(.black100) + + Text("유효하지 않거나\n더 이상 사용할 수 없는 링크입니다") .typography(.suit15R) + .multilineTextAlignment(.center) .foregroundColor(.black100) - .padding(.vertical, 15) + .padding(.bottom, 15) Button { dismiss() } label: { Text("닫기") - .typography(.suit15R) - .foregroundColor(.main500) - .padding(.vertical, 15) + .typography(.suit17B) + .foregroundColor(.black100) + .padding(.vertical, 13.5) + .padding(.horizontal, 20) + .background( + RoundedRectangle(cornerRadius: 15) + .fill(.gray50) + ) } } } @@ -217,10 +234,15 @@ struct KeyringCollectView: View { @ViewBuilder private var backgroundImage: some View { - Image(viewModel.backgroundImageName) - .resizable() - .scaledToFill() - .ignoresSafeArea() + if viewModel.shouldShowWhiteBackground { + Color.white + .ignoresSafeArea() + } else { + Image(.greenBackground) + .resizable() + .scaledToFill() + .ignoresSafeArea() + } } } diff --git a/Keychy/Keychy/Presentation/Collection/Views/Package/KeyringReceiveView.swift b/Keychy/Keychy/Presentation/Collection/Views/Package/KeyringReceiveView.swift index 014a09d4a..9ea57a6a1 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Package/KeyringReceiveView.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Package/KeyringReceiveView.swift @@ -13,11 +13,12 @@ struct KeyringReceiveView: View { @State var viewModel: KeyringReceiveViewModel @State private var scene: KeyringCellScene? - init(viewModel: CollectionViewModel, postOfficeId: String) { + init(viewModel: CollectionViewModel, postOfficeId: String, deepLinkError: DeepLinkError? = nil) { _viewModel = State( initialValue: KeyringReceiveViewModel( collectionViewModel: viewModel, - postOfficeId: postOfficeId + postOfficeId: postOfficeId, + deepLinkError: deepLinkError ) ) } @@ -38,10 +39,16 @@ struct KeyringReceiveView: View { alertOverlayView(geometry: geometry) - customNavigationBar - .blur(radius: viewModel.shouldApplyBlur ? 15 : 0) - .adaptiveTopPadding() - .zIndex(0) + // 정상 키링 수신 화면일 때만 네비게이션 바 표시 + if !viewModel.isLoading && + !viewModel.hasDeepLinkError && + !viewModel.isAlreadyReceived && + viewModel.keyring != nil { + customNavigationBar + .blur(radius: viewModel.shouldApplyBlur ? 15 : 0) + .adaptiveTopPadding() + .zIndex(0) + } } } .ignoresSafeArea() @@ -79,23 +86,33 @@ struct KeyringReceiveView: View { private var alreadyReceivedView: some View { VStack(spacing: 20) { - VStack(spacing: 0) { - Image(.emptyViewIcon) + VStack(spacing: 10) { + Image(.noInternetBangMark) .resizable() - .frame(width: 124, height: 111) + .frame(width: 26, height: 48) + .padding(.bottom, 15) Text("이미 수락된 선물이에요") + .typography(.suit18SB) + .foregroundColor(.black100) + + Text("이 키링은 이미 다른 사람이 수락했어요") .typography(.suit15R) .foregroundColor(.black100) - .padding(.vertical, 15) + .padding(.bottom, 15) Button { dismiss() } label: { Text("닫기") - .typography(.suit15R) - .foregroundColor(.main500) - .padding(.vertical, 15) + .typography(.suit17B) + .foregroundColor(.black100) + .padding(.vertical, 13.5) + .padding(.horizontal, 20) + .background( + RoundedRectangle(cornerRadius: 15) + .fill(.gray50) + ) } } } @@ -128,23 +145,34 @@ struct KeyringReceiveView: View { private var errorView: some View { VStack(spacing: 20) { - VStack(spacing: 0) { - Image(.emptyViewIcon) + VStack(spacing: 10) { + Image(.noInternetBangMark) .resizable() - .frame(width: 124, height: 111) + .frame(width: 26, height: 48) + .padding(.bottom, 15) - Text("키링을 불러올 수 없습니다.") + Text("키링을 불러 올 수 없습니다") + .typography(.suit18SB) + .foregroundColor(.black100) + + Text("유효하지 않거나\n더 이상 사용할 수 없는 링크입니다") .typography(.suit15R) + .multilineTextAlignment(.center) .foregroundColor(.black100) - .padding(.vertical, 15) + .padding(.bottom, 15) Button { dismiss() } label: { Text("닫기") - .typography(.suit15R) - .foregroundColor(.main500) - .padding(.vertical, 15) + .typography(.suit17B) + .foregroundColor(.black100) + .padding(.vertical, 13.5) + .padding(.horizontal, 20) + .background( + RoundedRectangle(cornerRadius: 15) + .fill(.gray50) + ) } } } @@ -245,10 +273,15 @@ struct KeyringReceiveView: View { @ViewBuilder private var backgroundImage: some View { - Image(viewModel.backgroundImageName) - .resizable() - .scaledToFill() - .ignoresSafeArea() + if viewModel.shouldShowWhiteBackground { + Color.white + .ignoresSafeArea() + } else { + Image(.greenBackground) + .resizable() + .scaledToFill() + .ignoresSafeArea() + } } } diff --git a/Keychy/Keychy/Presentation/Tab/ViewModels/MainTabViewModel.swift b/Keychy/Keychy/Presentation/Tab/ViewModels/MainTabViewModel.swift index 5283667c2..1f8016332 100644 --- a/Keychy/Keychy/Presentation/Tab/ViewModels/MainTabViewModel.swift +++ b/Keychy/Keychy/Presentation/Tab/ViewModels/MainTabViewModel.swift @@ -41,6 +41,8 @@ class MainTabViewModel { var showCollectSheet = false var receivedPostOfficeId: String? var collectedPostOfficeId: String? + var receivedDeepLinkError: DeepLinkError? + var collectedDeepLinkError: DeepLinkError? var shouldRefreshCollection = false // Splash @@ -78,8 +80,8 @@ class MainTabViewModel { // MARK: - Private Methods /// 대기 중인 딥링크가 있는지 확인하고 처리 private func checkPendingDeepLink() { - if let (postOfficeId, type) = deepLinkManager.consumePendingDeepLink() { - handleDeepLink(postOfficeId: postOfficeId, type: type) + if let (postOfficeId, type, error) = deepLinkManager.consumePendingDeepLink() { + handleDeepLink(postOfficeId: postOfficeId, type: type, error: error) } } @@ -87,12 +89,12 @@ class MainTabViewModel { /// - Parameters: /// - postOfficeId: 우체국 ID /// - type: 딥링크 타입 (receive, collect, notification) - private func handleDeepLink(postOfficeId: String, type: DeepLinkType) { + private func handleDeepLink(postOfficeId: String, type: DeepLinkType, error: DeepLinkError?) { switch type { case .receive: - handleSheetDeepLink(postOfficeId: postOfficeId, isReceive: true) + handleSheetDeepLink(postOfficeId: postOfficeId, isReceive: true, error: error) case .collect: - handleSheetDeepLink(postOfficeId: postOfficeId, isReceive: false) + handleSheetDeepLink(postOfficeId: postOfficeId, isReceive: false, error: error) case .notification: selectedTab = TabIndex.home.rawValue Task { @MainActor in @@ -106,13 +108,15 @@ class MainTabViewModel { /// - Parameters: /// - postOfficeId: 우체국 ID /// - isReceive: true면 받기 Sheet, false면 모으기 Sheet - private func handleSheetDeepLink(postOfficeId: String, isReceive: Bool) { + private func handleSheetDeepLink(postOfficeId: String, isReceive: Bool, error: DeepLinkError?) { selectedTab = TabIndex.collection.rawValue if isReceive { receivedPostOfficeId = postOfficeId + receivedDeepLinkError = error } else { collectedPostOfficeId = postOfficeId + collectedDeepLinkError = error } Task { @MainActor in diff --git a/Keychy/Keychy/Presentation/Tab/Views/MainTabView.swift b/Keychy/Keychy/Presentation/Tab/Views/MainTabView.swift index 5539bcdb2..1d9762c6d 100644 --- a/Keychy/Keychy/Presentation/Tab/Views/MainTabView.swift +++ b/Keychy/Keychy/Presentation/Tab/Views/MainTabView.swift @@ -130,7 +130,8 @@ extension MainTabView { if let postOfficeId = viewModel.receivedPostOfficeId { KeyringReceiveView( viewModel: viewModel.collectionViewModel, - postOfficeId: postOfficeId + postOfficeId: postOfficeId, + deepLinkError: viewModel.receivedDeepLinkError ) } else { EmptyView() @@ -142,7 +143,8 @@ extension MainTabView { if let postOfficeId = viewModel.collectedPostOfficeId { KeyringCollectView( viewModel: viewModel.collectionViewModel, - postOfficeId: postOfficeId + postOfficeId: postOfficeId, + deepLinkError: viewModel.collectedDeepLinkError ) } else { EmptyView()