diff --git a/.gitignore b/.gitignore index 0896441e..cc5cc875 100644 --- a/.gitignore +++ b/.gitignore @@ -114,4 +114,6 @@ StageConfig.xcconfig LiveConfig.xcconfig BaseConfig.xcconfig +.claude + Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Onboarding/.DS_Store diff --git a/Atcha-iOS.xcodeproj/project.pbxproj b/Atcha-iOS.xcodeproj/project.pbxproj index 16314675..353cec7a 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 = 11; + 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.6; + MARKETING_VERSION = 1.9.7; 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 = 11; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 23SCTLK482; EXCLUDED_ARCHS = ""; FRAMEWORK_SEARCH_PATHS = ( @@ -2586,7 +2586,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.9.6; + MARKETING_VERSION = 1.9.7; 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 = 11; + 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.6; + MARKETING_VERSION = 1.9.7; PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Lock/alarm_cancel.imageset/Contents.json b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Lock/alarm_cancel.imageset/Contents.json new file mode 100644 index 00000000..7ef96f32 --- /dev/null +++ b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Lock/alarm_cancel.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "alarm_cancel.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "alarm_cancel@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "alarm_cancel@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Lock/alarm_cancel.imageset/alarm_cancel.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Lock/alarm_cancel.imageset/alarm_cancel.png new file mode 100644 index 00000000..2c95d12d Binary files /dev/null and b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Lock/alarm_cancel.imageset/alarm_cancel.png differ diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Lock/alarm_cancel.imageset/alarm_cancel@2x.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Lock/alarm_cancel.imageset/alarm_cancel@2x.png new file mode 100644 index 00000000..7549f6b6 Binary files /dev/null and b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Lock/alarm_cancel.imageset/alarm_cancel@2x.png differ diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Lock/alarm_cancel.imageset/alarm_cancel@3x.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Lock/alarm_cancel.imageset/alarm_cancel@3x.png new file mode 100644 index 00000000..f7316985 Binary files /dev/null and b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Lock/alarm_cancel.imageset/alarm_cancel@3x.png differ diff --git a/Atcha-iOS/Presentation/Lock/LockViewController.swift b/Atcha-iOS/Presentation/Lock/LockViewController.swift index d90fbce8..927cfb46 100644 --- a/Atcha-iOS/Presentation/Lock/LockViewController.swift +++ b/Atcha-iOS/Presentation/Lock/LockViewController.swift @@ -15,6 +15,7 @@ final class LockViewController: BaseViewController { private let titleLabel: UILabel = UILabel() private let taxiFareLabel: UILabel = UILabel() private let startButton: AtchaButton = AtchaButton(text: "출발하기", size: .h52, style: .filled(.primary)) + private let cancelImageView: UIImageView = UIImageView() private let detailRouteButton: AtchaButton = AtchaButton(text: "더 늦은 경로 확인하기", size: .h52, style: .filled(.opacity)) private let bottomStack: UIStackView = UIStackView() private var lottieAnimationView: LottieAnimationView = LottieAnimationView(name: "Alarm") @@ -71,7 +72,7 @@ final class LockViewController: BaseViewController { // MARK: - Lock UI private func setupUI() { - view.addSubViews(backgroundImageView, lottieAnimationView, gradientView, logoImageView, titleLabel, taxiFareLabel, bottomStack) + view.addSubViews(backgroundImageView, lottieAnimationView, gradientView, logoImageView, titleLabel, taxiFareLabel, bottomStack, cancelImageView) backgroundImageView.image = UIImage.lockBackground gradient.colors = [ @@ -96,6 +97,9 @@ final class LockViewController: BaseViewController { startButton.addTarget(self, action: #selector(startTapped), for: .touchUpInside) + cancelImageView.image = UIImage.alarmCancel + cancelImageView.isUserInteractionEnabled = true + cancelImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cancelAlarmTapped))) detailRouteButton.addTarget(self, action: #selector(detailRouteTapped), @@ -137,6 +141,12 @@ final class LockViewController: BaseViewController { make.leading.equalToSuperview().offset(20) make.trailing.equalToSuperview().inset(20) } + + cancelImageView.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide.snp.top).inset(18) + make.trailing.equalToSuperview().inset(16) + make.size.equalTo(24) + } } @objc private func startTapped() { @@ -175,6 +185,10 @@ final class LockViewController: BaseViewController { ) } + @objc private func cancelAlarmTapped() { + showAlarmCancelPopup() + } + private func observeAlarmTimeout() { NotificationCenter.default.publisher(for: NSNotification.Name("alarmDidTimeout")) .receive(on: RunLoop.main) @@ -184,4 +198,34 @@ final class LockViewController: BaseViewController { } .store(in: &cancellables) } + + private func showAlarmCancelPopup() { + AlarmManager.shared.stopAlarm() + + let popupVM = AtchaPopupViewModel(info: .alarm_cancel) + let popupVC = AtchaPopupViewController(viewModel: popupVM) + + popupVC.cancelButton.addAction(UIAction { [weak popupVC] _ in + popupVC?.dismiss(animated: false) + }, for: .touchUpInside) + + popupVC.confirmButton.addAction(UIAction { [weak self, weak popupVC] _ in + guard let self else { return } + popupVC?.dismiss(animated: false) + + self.viewModel.cancelLockScreenTimer() + AlarmManager.shared.stopAlarm() + AlarmManager.shared.removeAllAlarmNotificationsExceptAutoStop() + + UserDefaultsWrapper.shared.set( + false, + forKey: UserDefaultsWrapper.Key.departureAlarmDidFire.rawValue + ) + + viewModel.routerHandler?(.dismissLockScreen) + }, for: .touchUpInside) + + popupVC.modalPresentationStyle = .overFullScreen + present(popupVC, animated: false) + } } diff --git a/Atcha-iOS/Presentation/Main/MainCoordinator.swift b/Atcha-iOS/Presentation/Main/MainCoordinator.swift index 8c5e379d..0bca15ec 100644 --- a/Atcha-iOS/Presentation/Main/MainCoordinator.swift +++ b/Atcha-iOS/Presentation/Main/MainCoordinator.swift @@ -320,7 +320,12 @@ final class MainCoordinator: NSObject { context: .afterReigster)) } case .dismissLockScreen: - self?.navigationController.dismiss(animated: true) + self?.mainViewModel?.stopAlarmTimeoutTimer() + self?.navigationController.dismiss(animated: true) { [weak self] in + self?.mainViewModel?.alarmDelete() + self?.mainViewModel?.bottomType = .search + self?.mainViewModel?.removeLegInfoAndAddress() + } default: do {} } } diff --git a/Atcha-iOS/Presentation/Popup/AtchaPopupInfo.swift b/Atcha-iOS/Presentation/Popup/AtchaPopupInfo.swift index 75c1a8e8..3c1c1ff1 100644 --- a/Atcha-iOS/Presentation/Popup/AtchaPopupInfo.swift +++ b/Atcha-iOS/Presentation/Popup/AtchaPopupInfo.swift @@ -20,6 +20,7 @@ enum AtcahPopuInfo { case serverError case update_essential case update_recommended + case alarm_cancel var title: String { switch self { @@ -35,6 +36,7 @@ enum AtcahPopuInfo { case .serverError: return "잠시 후 다시 시도해주세요\n앗차팀에서 확인 및 대응 중입니다" case .update_essential: return "더 좋아진 앗차를 사용하기 위해\n업데이트가 필요해요" case .update_recommended: return "더 좋아진 앗차를 사용하기 위해\n업데이트를 권장해요" + case .alarm_cancel: return "알람을 종료할까요?" } } @@ -52,12 +54,13 @@ enum AtcahPopuInfo { case .serverError: return "확인" case .update_essential: return "업데이트" case .update_recommended: return "업데이트" + case .alarm_cancel: return "종료하기" } } var confrimBackgroundColor: UIColor { switch self { - case .alarm, .re_register, .course, .arrive: return .main + case .alarm, .re_register, .course, .arrive, .alarm_cancel: return .main case .alarmTimeout, .serverError, .scheduledArrive: return .gray910 default: return .white } @@ -73,7 +76,7 @@ enum AtcahPopuInfo { var cancelTitle: String { switch self { case .alarm, .re_register: return "돌아가기" - case .course: return "돌아가기" + case .course, .alarm_cancel: return "돌아가기" case .update_recommended: return "나중에" default: return "취소" } diff --git a/Atcha-iOS/Presentation/Splash/SplashViewController.swift b/Atcha-iOS/Presentation/Splash/SplashViewController.swift index f91f003b..8c6551f9 100644 --- a/Atcha-iOS/Presentation/Splash/SplashViewController.swift +++ b/Atcha-iOS/Presentation/Splash/SplashViewController.swift @@ -46,27 +46,20 @@ final class SplashViewController: BaseViewController { } private func setupBindings() { - viewModel.$appVersionInfo - .compactMap { $0 } - .receive(on: DispatchQueue.main) - .sink { [weak self] versionInfo in - guard let self else { return } - updateAppVersion(versionInfo) - } - .store(in: &cancellables) + fetchAppStoreVersion() } - private func updateAppVersion(_ version: String) { - let serverVersion = version - let appVersion = AppInfoProvider.versionWithV - - // 서버 버전이 앱 버전보다 높다면 업데이트가 필요한 상황 - if isVersion(appVersion, lessThan: serverVersion) { - showUpdatePopup(isEssential: false) - } else { - viewModel.makeInitialFlow() - } - } +// private func updateAppVersion(_ version: String) { +// 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 { @@ -84,31 +77,54 @@ final class SplashViewController: BaseViewController { } private func showUpdatePopup(isEssential: Bool) { - // 1. 팝업 뷰모델 생성 (info 타입은 프로젝트 정의에 맞게 조절하세요) - let popupVM = AtchaPopupViewModel(info: isEssential ? .update_essential : .update_recommended) - let popupVC = AtchaPopupViewController(viewModel: popupVM) + // 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) - // 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) + if isEssential { + print("필수 업데이트입니다. 진행할 수 없습니다.") + } else { + self?.viewModel.makeInitialFlow() + } + }, for: .touchUpInside) + + popupVC.modalPresentationStyle = .overFullScreen + present(popupVC, animated: false) + } + + private func fetchAppStoreVersion() { + guard let url = URL(string: "https://itunes.apple.com/lookup?id=6747877903&country=kr") else { return } + + URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in + guard let self, + let data, + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let results = json["results"] as? [[String: Any]], + let appStoreVersion = results.first?["version"] as? String else { return } - // 3. [닫기/취소] 버튼 로직 - popupVC.cancelButton.addAction(UIAction { [weak self, weak popupVC] _ in - popupVC?.dismiss(animated: false) + DispatchQueue.main.async { + let appVersion = AppInfoProvider.versionWithV + let storeVersion = "v\(appStoreVersion)" - if isEssential { - print("필수 업데이트입니다. 진행할 수 없습니다.") + if self.isVersion(appVersion, lessThan: storeVersion) { + self.showUpdatePopup(isEssential: false) } else { - self?.viewModel.makeInitialFlow() + self.viewModel.makeInitialFlow() } - }, for: .touchUpInside) - - popupVC.modalPresentationStyle = .overFullScreen - present(popupVC, animated: false) - } + } + }.resume() + } }