diff --git a/Atcha-iOS.xcodeproj/project.pbxproj b/Atcha-iOS.xcodeproj/project.pbxproj index 5f03f507..862e7052 100644 --- a/Atcha-iOS.xcodeproj/project.pbxproj +++ b/Atcha-iOS.xcodeproj/project.pbxproj @@ -2516,7 +2516,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 23SCTLK482; FRAMEWORK_SEARCH_PATHS = ( @@ -2539,7 +2539,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.9.5; + MARKETING_VERSION = 1.9.6; PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2563,7 +2563,7 @@ CODE_SIGN_ENTITLEMENTS = "Atcha-iOS/Atcha-iOS.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 23SCTLK482; EXCLUDED_ARCHS = ""; FRAMEWORK_SEARCH_PATHS = ( @@ -2586,7 +2586,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.9.5; + MARKETING_VERSION = 1.9.6; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2611,7 +2611,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 23SCTLK482; FRAMEWORK_SEARCH_PATHS = ( @@ -2634,7 +2634,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.9.5; + MARKETING_VERSION = 1.9.6; PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Atcha-iOS/App/AppFlowCoordinator.swift b/Atcha-iOS/App/AppFlowCoordinator.swift index dd2d147a..613ccccc 100644 --- a/Atcha-iOS/App/AppFlowCoordinator.swift +++ b/Atcha-iOS/App/AppFlowCoordinator.swift @@ -25,7 +25,7 @@ class AppFlowCoordinator { self.container = container } - func startApp() { + func startApp(launchType: LaunchType = .main) { let navigationController = UINavigationController() window.rootViewController = navigationController window.makeKeyAndVisible() @@ -35,11 +35,14 @@ class AppFlowCoordinator { DispatchQueue.main.async { AppDIContainer.shared.tokenStorage.clearAllTokens() UserDefaultsWrapper.shared.set(false, forKey: UserDefaultsWrapper.Key.hasSeenIntro.rawValue) - self?.startApp() + self?.startApp(launchType: .fast) } } - let splashCoordinator = container.makeSplashCoordinator(navigationController: navigationController) + let splashCoordinator = container.makeSplashCoordinator( + navigationController: navigationController, + launchType: launchType + ) splashCoordinator.routerHandler = { [weak self] router in guard let self = self else { return } switch router { @@ -114,7 +117,7 @@ class AppFlowCoordinator { mainCoordinator.withdrawFinish = { [weak self] in DispatchQueue.main.async { // 앱 데이터를 다 지웠으니, 스플래시부터 앱을 아예 새로 시작(리부팅)합니다! - self?.startApp() + self?.startApp(launchType: .fast) } } diff --git a/Atcha-iOS/App/DIContainer/AppCompositionRoot.swift b/Atcha-iOS/App/DIContainer/AppCompositionRoot.swift index adc27d04..fb4299fd 100644 --- a/Atcha-iOS/App/DIContainer/AppCompositionRoot.swift +++ b/Atcha-iOS/App/DIContainer/AppCompositionRoot.swift @@ -57,8 +57,8 @@ extension AppCompositionRoot: SplashCoordinatorFactory, MainCoordinatorFactory, LockScreenCoordinatorFactory, IntroCoordinatorFactory { - func makeSplashCoordinator(navigationController: UINavigationController) -> SplashCoordinator { - return splashDIContainer.makeSplashCoordinator(navigationController: navigationController) + func makeSplashCoordinator(navigationController: UINavigationController, launchType: LaunchType) -> SplashCoordinator { + return splashDIContainer.makeSplashCoordinator(navigationController: navigationController, launchType: launchType) } func makeLoginCoordinator(navigationController: UINavigationController) -> LoginCoordinator { diff --git a/Atcha-iOS/App/DIContainer/CoordinatorFactory.swift b/Atcha-iOS/App/DIContainer/CoordinatorFactory.swift index 43ed90b9..2573e102 100644 --- a/Atcha-iOS/App/DIContainer/CoordinatorFactory.swift +++ b/Atcha-iOS/App/DIContainer/CoordinatorFactory.swift @@ -13,7 +13,7 @@ import Foundation /// to avoid service locator style lookups and to enable constructor injection of dependencies. protocol SplashCoordinatorFactory { - func makeSplashCoordinator(navigationController: UINavigationController) -> SplashCoordinator + func makeSplashCoordinator(navigationController: UINavigationController, launchType: LaunchType) -> SplashCoordinator } protocol LoginCoordinatorFactory { @@ -45,8 +45,8 @@ extension AppDIContainer: SplashCoordinatorFactory, MainCoordinatorFactory, LockScreenCoordinatorFactory, IntroCoordinatorFactory { - func makeSplashCoordinator(navigationController: UINavigationController) -> SplashCoordinator { - return splashDIContainer.makeSplashCoordinator(navigationController: navigationController) + func makeSplashCoordinator(navigationController: UINavigationController, launchType: LaunchType) -> SplashCoordinator { + return splashDIContainer.makeSplashCoordinator(navigationController: navigationController, launchType: launchType) } func makeLoginCoordinator(navigationController: UINavigationController) -> LoginCoordinator { diff --git a/Atcha-iOS/App/DIContainer/Splash/SplashDIContainer.swift b/Atcha-iOS/App/DIContainer/Splash/SplashDIContainer.swift index c523b766..2132b7b2 100644 --- a/Atcha-iOS/App/DIContainer/Splash/SplashDIContainer.swift +++ b/Atcha-iOS/App/DIContainer/Splash/SplashDIContainer.swift @@ -32,12 +32,13 @@ final class SplashDIContainer { return UpdateAppVersionUseCaseImpl(repository: repository) } - func makeSplashViewModel() -> SplashViewModel { + func makeSplashViewModel(launchType: LaunchType) -> SplashViewModel { return SplashViewModel( fetchUserUseCase: makeFetchUserUseCase(), checkAppVersionUseCase: makeCheckAppVersionUseCase(), updateAppVersionUseCase: makeUpdateAppVersionUseCase(), - tokenStorage: tokenStorage + tokenStorage: tokenStorage, + launchType: launchType ) } @@ -45,8 +46,9 @@ final class SplashDIContainer { return SplashViewController(viewModel: viewModel) } - func makeSplashCoordinator(navigationController: UINavigationController) -> SplashCoordinator { + func makeSplashCoordinator(navigationController: UINavigationController, launchType: LaunchType) -> SplashCoordinator { SplashCoordinator(navigationController: navigationController, - diContainer: self) + diContainer: self, + launchType: launchType) } } diff --git a/Atcha-iOS/App/SceneDelegate.swift b/Atcha-iOS/App/SceneDelegate.swift index 6c61134c..939cc36b 100644 --- a/Atcha-iOS/App/SceneDelegate.swift +++ b/Atcha-iOS/App/SceneDelegate.swift @@ -17,9 +17,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } let window = UIWindow(windowScene: windowScene) + var launchType: LaunchType = .main appFlowCoordinator = AppFlowCoordinator(window: window, container: diContainer) - appFlowCoordinator?.startApp() + appFlowCoordinator?.startApp(launchType: launchType) self.window = window } diff --git a/Atcha-iOS/Base.lproj/LaunchScreen.storyboard b/Atcha-iOS/Base.lproj/LaunchScreen.storyboard index 82c0f0b6..cb2b988e 100644 --- a/Atcha-iOS/Base.lproj/LaunchScreen.storyboard +++ b/Atcha-iOS/Base.lproj/LaunchScreen.storyboard @@ -1,9 +1,9 @@ - + - + diff --git a/Atcha-iOS/Core/Network/Token/SessionController.swift b/Atcha-iOS/Core/Network/Token/SessionController.swift index 3d82a51e..1b306b7d 100644 --- a/Atcha-iOS/Core/Network/Token/SessionController.swift +++ b/Atcha-iOS/Core/Network/Token/SessionController.swift @@ -26,6 +26,8 @@ final class SessionController { storage.clearRefreshToken() UserDefaultsWrapper.shared.removeAll() AppDIContainer.shared.locationStateHolder.clear() + UserDefaultsWrapper.shared.set(true, forKey: UserDefaultsWrapper.Key.isGuest.rawValue) + UserDefaultsWrapper.shared.set(false, forKey: UserDefaultsWrapper.Key.hasSeenIntro.rawValue) // 로그인 화면으로 DispatchQueue.main.async { [weak self] in diff --git a/Atcha-iOS/Core/Network/Token/TokenInterceptor.swift b/Atcha-iOS/Core/Network/Token/TokenInterceptor.swift index 97361f57..4d8c69f0 100644 --- a/Atcha-iOS/Core/Network/Token/TokenInterceptor.swift +++ b/Atcha-iOS/Core/Network/Token/TokenInterceptor.swift @@ -77,8 +77,6 @@ final class TokenInterceptor: RequestInterceptor, @unchecked Sendable { return } - let actualHeaders = request.request?.allHTTPHeaderFields ?? [:] - guard let refreshToken = tokenStorage.refreshToken else { SessionController.shared.expireAndRouteToLogin() completion(.doNotRetry) @@ -109,24 +107,7 @@ final class TokenInterceptor: RequestInterceptor, @unchecked Sendable { waiters.forEach { $0(.doNotRetry) } return } - - let successBody = [ - "newAccessToken": p.accessToken, - "newRefreshToken": p.refreshToken - ] - - DiscordWebhookManager.shared.sendErrorLog( - baseURL: NetworkConstant.baseURL, - statusCode: 200, - method: "GET", - path: "/auth/reissue", - responseCode: "REISSUE_SUCCESS", - message: "토큰 재발급에 성공하여 새로운 토큰을 수신했습니다.", - requestHeaders: actualHeaders, - requestBody: successBody, // 여기서 받은 토큰 정보를 보냅니다. - requestParameters: nil - ) - + self.tokenStorage.accessToken = p.accessToken self.tokenStorage.refreshToken = p.refreshToken diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Onboarding/setting_home_mark.imageset/Contents.json b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Onboarding/setting_home_mark.imageset/Contents.json new file mode 100644 index 00000000..e334e205 --- /dev/null +++ b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Onboarding/setting_home_mark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "setting_home_mark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "setting_home_mark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "setting_home_mark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Onboarding/setting_home_mark.imageset/setting_home_mark.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Onboarding/setting_home_mark.imageset/setting_home_mark.png new file mode 100644 index 00000000..91b6e675 Binary files /dev/null and b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Onboarding/setting_home_mark.imageset/setting_home_mark.png differ diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Onboarding/setting_home_mark.imageset/setting_home_mark@2x.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Onboarding/setting_home_mark.imageset/setting_home_mark@2x.png new file mode 100644 index 00000000..0d03202e Binary files /dev/null and b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Onboarding/setting_home_mark.imageset/setting_home_mark@2x.png differ diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Onboarding/setting_home_mark.imageset/setting_home_mark@3x.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Onboarding/setting_home_mark.imageset/setting_home_mark@3x.png new file mode 100644 index 00000000..24f4a8f8 Binary files /dev/null and b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Onboarding/setting_home_mark.imageset/setting_home_mark@3x.png differ diff --git a/Atcha-iOS/Presentation/Intro/IntroViewController.swift b/Atcha-iOS/Presentation/Intro/IntroViewController.swift index 77855fc2..2f823535 100644 --- a/Atcha-iOS/Presentation/Intro/IntroViewController.swift +++ b/Atcha-iOS/Presentation/Intro/IntroViewController.swift @@ -178,11 +178,6 @@ final class IntroViewController: BaseViewController { label.snp.makeConstraints { make in make.center.equalToSuperview() } - - button.snp.makeConstraints { make in - make.horizontalEdges.equalToSuperview().inset(15) - make.height.equalTo(56) - } } } diff --git a/Atcha-iOS/Presentation/Location/MainViewController.swift b/Atcha-iOS/Presentation/Location/MainViewController.swift index 87ceab62..c5a7d096 100644 --- a/Atcha-iOS/Presentation/Location/MainViewController.swift +++ b/Atcha-iOS/Presentation/Location/MainViewController.swift @@ -78,6 +78,7 @@ final class MainViewController: BaseViewController, if viewModel.isGuest { return false } return viewModel.isGuideActiveInSession } + private var guestTapCount = 0 // MARK: - Life Cycle @@ -152,6 +153,12 @@ final class MainViewController: BaseViewController, } } } + + self.guestTapCount = 0 + if viewModel.isGuest { + ballonView.isHidden = true + ballonView.alpha = 0 + } } override func viewDidAppear(_ animated: Bool) { @@ -229,6 +236,8 @@ final class MainViewController: BaseViewController, flagImageView.image = UIImage.settingLocationMark atchaImageView.isUserInteractionEnabled = true atchaImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleBallonTap))) + ballonView.isUserInteractionEnabled = true + ballonView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleBallonTap))) ballonView.isHidden = true ballonView.alpha = 0 @@ -309,6 +318,7 @@ extension MainViewController { observeArrival() observeScheduledArrivalTimeout() observeAlarmTimeout() + observeLoginDismissal() } private func bindPermissionAlert() { @@ -956,6 +966,13 @@ extension MainViewController { } @objc private func handleBallonTap() { + safeStartJump() + + if viewModel.isGuest { + handleGuestBallonTap() + return + } + // 알람 등록 후(departure 상태)일 때만 반응 guard viewModel.bottomType == .departure else { return } @@ -1014,6 +1031,31 @@ extension MainViewController { postAlarmTapIndex += 1 } } + + private func handleGuestBallonTap() { + if guestTapCount == 0 { + // 첫 번째 터치: "궁금하면 로그인 해봐요!" + guestTapCount = 1 + + ballonView.layer.removeAllAnimations() + ballonView.isHidden = false + ballonView.alpha = 1 + + ballonView.setupTitle(topMessage: nil, bottomMessage: "궁금하면 로그인 해봐요!") + ballonView.animateStaggered(secondaryDelay: 0, fade: 0.25) + + } else { + // 두 번째 터치: 로그인 시트 노출 + guestTapCount = 0 // 카운트 리셋 + + // [중요] 말풍선을 즉시 숨김 상태로 만들어야 시트가 내려간 뒤 다시 Persistent(???원) 메시지가 나타납니다. + ballonView.layer.removeAllAnimations() + ballonView.isHidden = true + ballonView.alpha = 0 + + presentLoginAlert() + } + } } // MARK: - Map Delegate & Gesture @@ -1233,6 +1275,10 @@ extension MainViewController { private func showOrUpdatePersistentBalloon(isFirstVisit: Bool, isServiceRegion: Bool?, fareStr: String?) { guard !isShowingToast else { return } + if viewModel.isGuest && guestTapCount == 1 { + return + } + // [수정] 우리가 정의한 로그인 기반 가이드 로직 적용 let showGuideLine = shouldShowMapGuide let topText: String? = showGuideLine ? "지도를 움직여 출발지를 설정해요" : nil @@ -1242,7 +1288,7 @@ extension MainViewController { } else { if viewModel.isGuest { // 비로그인: 가이드 없이 ???원만 노출 - ballonView.separationTitle(grayMessage: "여기서 막차 놓치면 택시비 ", whiteMessage: "약 ???원", showTopLine: false) + ballonView.separationTitle(grayMessage: "여기서 막차 놓치면 택시비 ", whiteMessage: "???원", showTopLine: false) } else { // 로그인 상태 if let fare = fareStr { @@ -1315,4 +1361,25 @@ extension MainViewController { balloonHideWorkItem = workItem DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: workItem) } + + private func observeLoginDismissal() { + NotificationCenter.default.publisher(for: NSNotification.Name("LoginSheetDismissed")) + .receive(on: RunLoop.main) + .sink { [weak self] _ in + guard let self = self else { return } + + // 로그인 시트가 내려갔으니 guestTapCount도 초기화해주는 게 자연스러워요 + self.guestTapCount = 0 + + // 현재 검색 모드라면 다시 고정 말풍선 노출 + if self.viewModel.bottomType == .search || self.viewModel.bottomType == nil { + self.showOrUpdatePersistentBalloon( + isFirstVisit: self.isFirstVisit, + isServiceRegion: self.latestIsServiceRegion, + fareStr: self.latestFareString + ) + } + } + .store(in: &cancellables) + } } diff --git a/Atcha-iOS/Presentation/Login/LoginViewController.swift b/Atcha-iOS/Presentation/Login/LoginViewController.swift index a4c63712..a1b4c52f 100644 --- a/Atcha-iOS/Presentation/Login/LoginViewController.swift +++ b/Atcha-iOS/Presentation/Login/LoginViewController.swift @@ -245,6 +245,7 @@ extension LoginViewController { self.containerView.transform = CGAffineTransform(translationX: 0, y: self.sheetHeight) }) { _ in self.dismiss(animated: false) { + NotificationCenter.default.post(name: NSNotification.Name("LoginSheetDismissed"), object: nil) self.viewModel.loginCancelled?() } } diff --git a/Atcha-iOS/Presentation/Popup/AtchaPopupInfo.swift b/Atcha-iOS/Presentation/Popup/AtchaPopupInfo.swift index 14f80ec8..75c1a8e8 100644 --- a/Atcha-iOS/Presentation/Popup/AtchaPopupInfo.swift +++ b/Atcha-iOS/Presentation/Popup/AtchaPopupInfo.swift @@ -18,6 +18,8 @@ enum AtcahPopuInfo { case arrive case scheduledArrive case serverError + case update_essential + case update_recommended var title: String { switch self { @@ -31,6 +33,8 @@ enum AtcahPopuInfo { case .arrive: return "목적지 부근에 도착해\n안내를 종료합니다" case .scheduledArrive: return "예정된 도착 시간이 지나\n알람이 자동으로 종료됐어요" case .serverError: return "잠시 후 다시 시도해주세요\n앗차팀에서 확인 및 대응 중입니다" + case .update_essential: return "더 좋아진 앗차를 사용하기 위해\n업데이트가 필요해요" + case .update_recommended: return "더 좋아진 앗차를 사용하기 위해\n업데이트를 권장해요" } } @@ -46,6 +50,8 @@ enum AtcahPopuInfo { case .arrive: return "확인" case .scheduledArrive: return "닫기" case .serverError: return "확인" + case .update_essential: return "업데이트" + case .update_recommended: return "업데이트" } } @@ -68,6 +74,7 @@ enum AtcahPopuInfo { switch self { case .alarm, .re_register: return "돌아가기" case .course: return "돌아가기" + case .update_recommended: return "나중에" default: return "취소" } } diff --git a/Atcha-iOS/Presentation/Popup/AtchaPopupViewController.swift b/Atcha-iOS/Presentation/Popup/AtchaPopupViewController.swift index ed0b20d2..10d0cd7d 100644 --- a/Atcha-iOS/Presentation/Popup/AtchaPopupViewController.swift +++ b/Atcha-iOS/Presentation/Popup/AtchaPopupViewController.swift @@ -80,7 +80,7 @@ final class AtchaPopupViewController: BaseViewController { confirmButton.setAttributedTitle(confirmAttr, for: .normal) confirmButton.backgroundColor = info.confrimBackgroundColor - if info != .alarmTimeout || info != .arrive || info != .serverError || info != .scheduledArrive { + if info != .alarmTimeout || info != .arrive || info != .serverError || info != .scheduledArrive || info != .update_essential { let cancelAttr = AtchaFont.B5_SB_14(info.cancelTitle, color: info.cancelForegroundColor) cancelButton.setAttributedTitle(cancelAttr, for: .normal) cancelButton.backgroundColor = info.cancelBackgroundColor @@ -93,7 +93,7 @@ final class AtchaPopupViewController: BaseViewController { $0.removeFromSuperview() } - if info == .alarmTimeout || info == .arrive || info == .serverError || info == .scheduledArrive { + if info == .alarmTimeout || info == .arrive || info == .serverError || info == .scheduledArrive || info == .update_essential { buttonStackView.addArrangedSubview(confirmButton) } else { buttonStackView.addArrangedSubview(cancelButton) diff --git a/Atcha-iOS/Presentation/Setting/PushAlarmSheet/PushAlarmSheetViewController.swift b/Atcha-iOS/Presentation/Setting/PushAlarmSheet/PushAlarmSheetViewController.swift index 92d09f0e..77a50c59 100644 --- a/Atcha-iOS/Presentation/Setting/PushAlarmSheet/PushAlarmSheetViewController.swift +++ b/Atcha-iOS/Presentation/Setting/PushAlarmSheet/PushAlarmSheetViewController.swift @@ -203,13 +203,10 @@ final class PushAlarmSheetViewController: BaseViewController Void)? - init(navigationController: UINavigationController, diContainer: SplashDIContainer) { + init(navigationController: UINavigationController, diContainer: SplashDIContainer, launchType: LaunchType) { self.navigationController = navigationController self.diContainer = diContainer + self.launchType = launchType } func start() { - let viewModel = diContainer.makeSplashViewModel() + let viewModel = diContainer.makeSplashViewModel(launchType: launchType) let viewController = diContainer.makeSplashViewController(viewModel: viewModel) viewModel.routerHandler = { [weak self] router in self?.routerHandler?(router) diff --git a/Atcha-iOS/Presentation/Splash/SplashViewController.swift b/Atcha-iOS/Presentation/Splash/SplashViewController.swift index 412c0578..f91f003b 100644 --- a/Atcha-iOS/Presentation/Splash/SplashViewController.swift +++ b/Atcha-iOS/Presentation/Splash/SplashViewController.swift @@ -26,7 +26,6 @@ final class SplashViewController: BaseViewController { setupUI() setupBindings() - viewModel.makeInitialFlow() } private func setupUI() { @@ -41,29 +40,12 @@ final class SplashViewController: BaseViewController { make.edges.equalToSuperview() } logoStackView.snp.makeConstraints { make in - make.centerX.equalTo(view.safeAreaLayoutGuide) - make.centerY.equalTo(view.safeAreaLayoutGuide) + make.centerX.equalToSuperview() + make.centerY.equalToSuperview() } } private func setupBindings() { - viewModel.$isLoading - .receive(on: DispatchQueue.main) - .sink { isLoading in - // 로딩 UI 표시/숨김 - print("isLoading: \(isLoading)") - } - .store(in: &cancellables) - - viewModel.$errorMessage - .compactMap { $0 } - .receive(on: DispatchQueue.main) - .sink { message in - // Alert 띄우기 - print("Error: \(message)") - } - .store(in: &cancellables) - viewModel.$appVersionInfo .compactMap { $0 } .receive(on: DispatchQueue.main) @@ -75,15 +57,16 @@ final class SplashViewController: BaseViewController { } private func updateAppVersion(_ version: String) { - let severVersion: String = version - let appVersion: String = AppInfoProvider.versionWithV - - if isVersion(severVersion, lessThan: appVersion) { - viewModel.updateAppVersion(version: appVersion) - } else { - print("강제 업데이트 표출해주세요!!") + let serverVersion = version + let appVersion = AppInfoProvider.versionWithV + + // 서버 버전이 앱 버전보다 높다면 업데이트가 필요한 상황 + if isVersion(appVersion, lessThan: serverVersion) { + showUpdatePopup(isEssential: false) + } else { + viewModel.makeInitialFlow() + } } - } /// lhs < rhs 인지 비교 (서버 버전 < 앱 버전?) private func isVersion(_ lhs: String, lessThan rhs: String) -> Bool { @@ -99,4 +82,33 @@ final class SplashViewController: BaseViewController { } return false } + + private func showUpdatePopup(isEssential: Bool) { + // 1. 팝업 뷰모델 생성 (info 타입은 프로젝트 정의에 맞게 조절하세요) + let popupVM = AtchaPopupViewModel(info: isEssential ? .update_essential : .update_recommended) + let popupVC = AtchaPopupViewController(viewModel: popupVM) + + // 2. [업데이트하기] 버튼 로직 + popupVC.confirmButton.addAction(UIAction { _ in + let appID = "6747877903" + if let url = URL(string: "itms-apps://itunes.apple.com/app/id\(appID)"), + UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + }, for: .touchUpInside) + + // 3. [닫기/취소] 버튼 로직 + popupVC.cancelButton.addAction(UIAction { [weak self, weak popupVC] _ in + popupVC?.dismiss(animated: false) + + if isEssential { + print("필수 업데이트입니다. 진행할 수 없습니다.") + } else { + self?.viewModel.makeInitialFlow() + } + }, for: .touchUpInside) + + popupVC.modalPresentationStyle = .overFullScreen + present(popupVC, animated: false) + } } diff --git a/Atcha-iOS/Presentation/Splash/SplashViewModel.swift b/Atcha-iOS/Presentation/Splash/SplashViewModel.swift index a346a080..e9d1cfd4 100644 --- a/Atcha-iOS/Presentation/Splash/SplashViewModel.swift +++ b/Atcha-iOS/Presentation/Splash/SplashViewModel.swift @@ -7,9 +7,21 @@ import Foundation +enum LaunchType { + case main // 일반 진입 (1.5초 대기) + case fast // 빠른 진입 (0초 대기) - 푸시 알림 등 + + var minimumDelay: Double { + switch self { + case .main: return 1.2 + case .fast: return 0.8 + } + } +} + final class SplashViewModel: BaseViewModel { @Published private(set) var appVersionInfo: String? - + private let launchType: LaunchType private let fetchUserUseCase: FetchUserUseCase private let checkAppVersionUseCase: CheckAppVersionUseCase private let updateAppVersionUseCase: UpdateAppVersionUseCase @@ -20,24 +32,34 @@ final class SplashViewModel: BaseViewModel { init(fetchUserUseCase: FetchUserUseCase, checkAppVersionUseCase: CheckAppVersionUseCase, updateAppVersionUseCase: UpdateAppVersionUseCase, - tokenStorage: TokenStorage) { + tokenStorage: TokenStorage, + launchType: LaunchType) { self.fetchUserUseCase = fetchUserUseCase self.checkAppVersionUseCase = checkAppVersionUseCase self.updateAppVersionUseCase = updateAppVersionUseCase self.tokenStorage = tokenStorage + self.launchType = launchType super.init() self.checkAppVersion() } func checkAppVersion() { - print(#function) Task { - setLoading(true) - defer { self.setLoading(false) } + let startTime = Date() + let minDelay = launchType.minimumDelay + do { let versionInfo = try await checkAppVersionUseCase.execute() - appVersionInfo = versionInfo + let elapsed = Date().timeIntervalSince(startTime) + if elapsed < minDelay { + let remaining = minDelay - elapsed + try? await Task.sleep(nanoseconds: UInt64(remaining * 1_000_000_000)) + } + + await MainActor.run { + self.appVersionInfo = versionInfo + } } catch { handleError(error) } diff --git a/Atcha-iOS/Presentation/User/Home/HomeFindViewController.swift b/Atcha-iOS/Presentation/User/Home/HomeFindViewController.swift index 542f9f9c..ccc41a43 100644 --- a/Atcha-iOS/Presentation/User/Home/HomeFindViewController.swift +++ b/Atcha-iOS/Presentation/User/Home/HomeFindViewController.swift @@ -51,7 +51,7 @@ final class HomeFindViewController: BaseViewController, backButton) mapContainerView.delegate = self - flagImageView.image = UIImage.settingLocationMark + flagImageView.image = UIImage.settingHomeMark backButton.setImage(UIImage.chevronLeft, for: .normal) backButton.tintColor = .white backButton.backgroundColor = .black diff --git a/Atcha-iOS/Presentation/User/Withdraw/WithdrawViewModel.swift b/Atcha-iOS/Presentation/User/Withdraw/WithdrawViewModel.swift index 9ea0f983..d91516f5 100644 --- a/Atcha-iOS/Presentation/User/Withdraw/WithdrawViewModel.swift +++ b/Atcha-iOS/Presentation/User/Withdraw/WithdrawViewModel.swift @@ -37,6 +37,7 @@ final class WithdrawViewModel: BaseViewModel { tokenStorage.clearAllTokens() UserDefaultsWrapper.shared.removeAll() locationStateHolder.clear() + UserDefaultsWrapper.shared.set(true, forKey: UserDefaultsWrapper.Key.isGuest.rawValue) UserDefaultsWrapper.shared.set(false, forKey: UserDefaultsWrapper.Key.hasSeenIntro.rawValue) signOutFinish?() } catch {