From aa550abf4a6d329588a8a00ad8606d99baab3d15 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Wed, 1 Apr 2026 23:18:00 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[FEAT]=20=EA=B2=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=9C=A0=EB=8F=84=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B3=A0=EB=8F=84=ED=99=94=20=EB=B0=8F=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EB=B2=84=ED=8A=BC=20=ED=99=9C=EC=84=B1=ED=99=94=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Location/MainViewController.swift | 155 +++++++++--------- 1 file changed, 78 insertions(+), 77 deletions(-) diff --git a/Atcha-iOS/Presentation/Location/MainViewController.swift b/Atcha-iOS/Presentation/Location/MainViewController.swift index d59b7c8..42f67c3 100644 --- a/Atcha-iOS/Presentation/Location/MainViewController.swift +++ b/Atcha-iOS/Presentation/Location/MainViewController.swift @@ -79,6 +79,7 @@ final class MainViewController: BaseViewController, return viewModel.isGuideActiveInSession } private var guestTapCount = 0 + private var guestLoginWorkItem: DispatchWorkItem? // MARK: - Life Cycle @@ -778,27 +779,28 @@ extension MainViewController { } private func bindServiceRegionUpdates() { - viewModel.$isServiceRegion - .removeDuplicates() + // isServiceRegion과 isGuest 상태를 결합하여 판단 + Publishers.CombineLatest(viewModel.$isServiceRegion, viewModel.$isGuest) + .removeDuplicates { $0.0 == $1.0 && $0.1 == $1.1 } .receive(on: RunLoop.main) - .sink { [weak self] ok in + .sink { [weak self] ok, isGuest in guard let self = self else { return } self.latestIsServiceRegion = ok - switch ok { - case .some(true): - self.lastTrainSearchView.updateSearchEnabled(true) - case .some(false): + // 핵심 로직: + // 1. 게스트 모드라면 지역에 상관없이 항상 활성화 (로그인 시트 유도) + // 2. 회원 모드라면 서비스 지역(ok == true)일 때만 활성화 + let shouldEnableSearch = isGuest || (ok == true) + self.lastTrainSearchView.updateSearchEnabled(shouldEnableSearch) + + // 서비스 지역이 아닐 때의 데이터 정리 + if ok == false { self.latestFareString = nil self.viewModel.taxiFare = nil - self.lastTrainSearchView.updateSearchEnabled(false) - case .none: - self.lastTrainSearchView.updateSearchEnabled(false) } - // 검색 모드일 때는 즉시 말풍선 글자 업데이트 + // 검색 모드일 때 말풍선 업데이트 로직 유지 if self.viewModel.bottomType == .search || self.viewModel.bottomType == nil { - // 방해물(업데이트 생략 조건문) 제거! self.showOrUpdatePersistentBalloon( isFirstVisit: self.isFirstVisit, isServiceRegion: ok, @@ -966,100 +968,103 @@ extension MainViewController { } @objc private func handleBallonTap() { - safeStartJump() + safeStartJump() // 캐릭터 점프 + // 1. 게스트 모드일 때 if viewModel.isGuest { + // [요청사항 반영] 서비스 지역 외라면 즉시 로그인 시트 노출 (1-tap) if latestIsServiceRegion == false { presentLoginAlert() return } - // 서비스 지역 내라면 기존의 2-tap 로직(문구 노출 후 로그인) 유지 + // 서비스 지역 내라면 문구 노출 후 2초 뒤 자동 로그인 (handleGuestBallonTap으로 이동) handleGuestBallonTap() return } - // 알람 등록 후(departure 상태)일 때만 반응 + // 2. 회원 모드일 때 (알람 등록된 상태에서만 동작) guard viewModel.bottomType == .departure else { return } amp_track(.character_click) - - let cycle = postAlarmTapIndex % 3 - - // 추가: 현재 알람이 울린 상태인지 확인 let isAlarmFired = UserDefaultsWrapper.shared.bool(forKey: UserDefaultsWrapper.Key.departureAlarmDidFire.rawValue) ?? false + let cycle = postAlarmTapIndex % 3 if cycle == 0 { - // 요금 정보 표시 (동적 로딩) - let now = CACurrentMediaTime() - if (now - lastFareRefreshTime) > fareRefreshInterval && !isFetchingFare { - isFetchingFare = true - Task { - defer { Task { @MainActor in self.isFetchingFare = false } } - do { - let fare = try await viewModel.fetchFareForRegisteredStart() - let fareInt = Int(fare) - let fareStr = self.decimalFormatter.string(from: NSNumber(value: fareInt)) ?? "\(fareInt)" - await MainActor.run { - self.latestFareString = fareStr - self.lastFareRefreshTime = CACurrentMediaTime() - let displayFare = self.viewModel.isGuest ? "???원" : "\(fareStr)원" - self.showTransientBalloon(isFare: true, text: displayFare) - self.postAlarmTapIndex += 1 - } - } catch { - await MainActor.run { - self.showTransientBalloon(isFare: false, text: "택시비 조회에 실패했어요") - self.postAlarmTapIndex += 1 - } - } - } - } else { - let fareStr = latestFareString ?? "???" - let displayFare = viewModel.isGuest ? "???원" : "\(fareStr)원" - showTransientBalloon(isFare: true, text: displayFare) - self.postAlarmTapIndex += 1 - } - + handleMemberFareTap() // 요금 정보 조회/표시 로직 } else if cycle == 1 { - // 수정: 알람이 울렸다면 이 메시지를 건너뛰고 다음 메시지를 띄움 if isAlarmFired { showTransientBalloon(isFare: false, text: "교통 상황에 따라 시간이 달라질 수 있어요") - // cycle 1을 건너뛰었으므로 다음 탭이 cycle 0(택시비)으로 돌아가도록 index를 2 올려줌 postAlarmTapIndex += 2 } else { showTransientBalloon(isFare: false, text: "시간에 맞춰 알림을 드릴게요") postAlarmTapIndex += 1 } - } else { showTransientBalloon(isFare: false, text: "교통 상황에 따라 시간이 달라질 수 있어요") postAlarmTapIndex += 1 } } - + + /// 게스트 전용: 서비스 지역 내(서울/경기/인천)에서 문구 노출 후 2초 뒤 자동 로그인 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) + // 기존에 예약된 타이머가 있다면 취소 (중복 실행 방지) + guestLoginWorkItem?.cancel() + + ballonView.layer.removeAllAnimations() + ballonView.isHidden = false + ballonView.alpha = 1 + + ballonView.setupTitle(topMessage: nil, bottomMessage: "택시비가 궁금하면 로그인해봐요!") + ballonView.animateStaggered(secondaryDelay: 0, fade: 0.25) + + let workItem = DispatchWorkItem { [weak self] in + guard let self = self, self.viewModel.isGuest else { return } + // 현재 떠있는 화면이 없을 때만 로그인 시트 노출 + if self.presentedViewController == nil { + self.presentLoginAlert() + } + } + + self.guestLoginWorkItem = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: workItem) + } + + /// 회원 전용: 실시간 택시 요금 조회 로직 + private func handleMemberFareTap() { + let now = CACurrentMediaTime() + + if (now - lastFareRefreshTime) < fareRefreshInterval || isFetchingFare { + let fareStr = latestFareString ?? "???" + showTransientBalloon(isFare: true, text: "\(fareStr)원") + postAlarmTapIndex += 1 } else { - // 두 번째 터치: 로그인 시트 노출 - guestTapCount = 0 // 카운트 리셋 - - // [중요] 말풍선을 즉시 숨김 상태로 만들어야 시트가 내려간 뒤 다시 Persistent(???원) 메시지가 나타납니다. - ballonView.layer.removeAllAnimations() - ballonView.isHidden = true - ballonView.alpha = 0 - - presentLoginAlert() + isFetchingFare = true + Task { [weak self] in // weak self 추가 + guard let self = self else { return } + defer { + Task { @MainActor in self.isFetchingFare = false } + } + + do { + let fare = try await viewModel.fetchFareForRegisteredStart() + let fareInt = Int(fare) + let fareStr = self.decimalFormatter.string(from: NSNumber(value: fareInt)) ?? "\(fareInt)" + + await MainActor.run { + self.latestFareString = fareStr + self.lastFareRefreshTime = CACurrentMediaTime() + self.showTransientBalloon(isFare: true, text: "\(fareStr)원") + self.postAlarmTapIndex += 1 + } + } catch { + await MainActor.run { + self.showTransientBalloon(isFare: false, text: "택시비 조회에 실패했어요") + self.postAlarmTapIndex += 1 + } + } + } } } } @@ -1281,10 +1286,6 @@ 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 From b0f707688395e8447f6becabd85d29a02ae2e43c Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Thu, 2 Apr 2026 22:23:49 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[BUGFIX]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=ED=8A=B8=20=EC=9E=90=EB=8F=99=20=EC=98=A4=ED=94=88=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Atcha-iOS/Presentation/Location/MainViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Atcha-iOS/Presentation/Location/MainViewController.swift b/Atcha-iOS/Presentation/Location/MainViewController.swift index 42f67c3..9ed9b86 100644 --- a/Atcha-iOS/Presentation/Location/MainViewController.swift +++ b/Atcha-iOS/Presentation/Location/MainViewController.swift @@ -1028,7 +1028,7 @@ extension MainViewController { } self.guestLoginWorkItem = workItem - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: workItem) + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: workItem) } /// 회원 전용: 실시간 택시 요금 조회 로직 From b9bcd39de79e5bbc60628a4dc433339306a74d63 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Sun, 5 Apr 2026 07:41:48 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[SETTING]=20=EB=B9=8C=EB=93=9C=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Atcha-iOS.xcodeproj/project.pbxproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Atcha-iOS.xcodeproj/project.pbxproj b/Atcha-iOS.xcodeproj/project.pbxproj index 862e705..1631467 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 = 1; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 23SCTLK482; FRAMEWORK_SEARCH_PATHS = ( @@ -2563,7 +2563,7 @@ CODE_SIGN_ENTITLEMENTS = "Atcha-iOS/Atcha-iOS.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_TEAM = 23SCTLK482; EXCLUDED_ARCHS = ""; FRAMEWORK_SEARCH_PATHS = ( @@ -2611,7 +2611,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 23SCTLK482; FRAMEWORK_SEARCH_PATHS = ( From 70046026cdbf30dd395ce4a946ab77721df75fec Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Mon, 27 Apr 2026 14:48:15 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[FEAT]=20=EC=9E=A0=EA=B8=88=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=95=8C=EB=9E=8C=20=EC=B7=A8=EC=86=8C=20=ED=8C=9D?= =?UTF-8?q?=EC=97=85=20=EB=B0=8F=20=EC=B7=A8=EC=86=8C=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=B2=84=ED=8A=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Lock/alarm_cancel.imageset/Contents.json | 23 ++++++++++++++++++ .../alarm_cancel.imageset/alarm_cancel.png | Bin 0 -> 438 bytes .../alarm_cancel.imageset/alarm_cancel@2x.png | Bin 0 -> 624 bytes .../alarm_cancel.imageset/alarm_cancel@3x.png | Bin 0 -> 835 bytes 4 files changed, 23 insertions(+) create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Lock/alarm_cancel.imageset/Contents.json create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Lock/alarm_cancel.imageset/alarm_cancel.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Lock/alarm_cancel.imageset/alarm_cancel@2x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Lock/alarm_cancel.imageset/alarm_cancel@3x.png 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 0000000..7ef96f3 --- /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 0000000000000000000000000000000000000000..2c95d12d340d73d3b2544ee7bc8c50cd083decd9 GIT binary patch literal 438 zcmV;n0ZIOeP)89$FqS(&N_xuyv02d|1x4d}@=&4w~@;(;E zEdQEkKAXMXfl!p=^2un=pi%7#tCcAwCKO9KyO#evGJ=O%DY%!Kgm7XYJ}q2b`@vQ9 z^*tEe{8|ems#|ME!{P0ssy=`C@JZ>%=rD;DT5I=9?_F=S8q=E# zPa4Ebe5K%Rry!wlx+L&64j9TZWjSpzB}cHoi8f08!d*usaJLb|g`Z9zKMpap1Wt1< z-HciScXf%0mcpebx({4p(k=4#KA3aizQWC@)YUbe&TZL?tNXw=29wf1>izp52z)LO gF^8TyHUB)d246!ihm6PilK=n!07*qoM6N<$f)6#a&j0`b literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7549f6b6c0df3b63af77e1283d4d9d9a2cea2dbc GIT binary patch literal 624 zcmV-$0+0QPP)Y@o5QSF~o*{GzYQZJY0VU7^p|i^f-2x@h z0j;P65+^p2tXsjzWA6Xn%EOGl1c7dM-y0Cx)e$W$EIc)Z?SxVvuWftX>Eo(eos`-) zG$Wz)3rld_b>HXVu)clNl;z82E)$meZ$tgd-?gPV#dZ5DSi=c*ViLw6p6j>m*Uq%w zV!(*F35$3q76YqeQBY$zA!foQUTdkU?qrH9&B^wcqG&gBned6%T9kTkOtIE~p)v8H z?)&Q8_r|Xja$)C)=QZRlAZH?1Jg=jmLUQAt9b)DUlcxQsOzDG6*rD52VC%4$8!5;`aJ*4k4cN5CD&f$D$zdoQpt&nm~%@ zd<4X20xez>GYn|)U%)}2y17}$R<6Zjz0000< KMNUMnLSTX%!3=r; literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f73169859b43c2680bd547d3f1f5390b9d8c3809 GIT binary patch literal 835 zcmV-J1HAl+P)Eqh_A6H-)?|&~st}oz)`JbE|e985n zat-Erv-d*q$SMvTCjdryLr!Eh&%gF_F|vZD$$fw0-)1FcLgpk< zQU` z2~E|mq*6J~{a*tC@_kU8KnOP%7O$ilIo^BH+{l8`M65~Tl!JYs5KEG{XCU<_zPQj)YBi~}8zkfb38V?if4C27gQc+e3JNt$vnCUmB! zByBku7Z~Uv$$}h=4NSC1vLXlL10yw(;xAU@;26M6sicQj6LN4I5TH;}7AZM676{Qx zlB66Q4+LqIBrONW1Yv3=X~@BGL4i_9T5=+!q&)|VoPxITXSRgJg33pXdY|(Qm6DS9 zB3&sbAudVuYrauVLR6C2jbbM`2{B3Ri*)&er>zPutDu^va%*L7)^tbhQ@N$$BwNC) zHDzH%5=+XpS3bvrr0z-k-pMX)NnOi6T$w;kN!<#E+-XuRNnJ|MQaNM|NzI!Vq!aAY zlA5-^XjLC1B~@>r(k{(NNvhg{XQgf>A*py1r=_A^niHp_qHVy|our4PO-)w2BgwU?(yx0>oUpt(`=HHU2ys0MSMjRy{^uXqA$EQKgTY`h7z_r|+yVCD%hrAbvx@)# N002ovPDHLkV1f_}U=9EP literal 0 HcmV?d00001 From 19393f160f28de929a6d16d39f1cb5d1392e68af Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Mon, 27 Apr 2026 15:52:20 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[FEAT]=20=EC=95=B1=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EC=96=B4=20API=20=EA=B8=B0=EB=B0=98=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=B2=B4=ED=81=AC=EB=A1=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=ED=8C=9D=EC=97=85=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + Atcha-iOS.xcodeproj/project.pbxproj | 12 +-- .../Lock/LockViewController.swift | 46 ++++++++- .../Presentation/Main/MainCoordinator.swift | 7 +- .../Presentation/Popup/AtchaPopupInfo.swift | 7 +- .../Splash/SplashViewController.swift | 98 +++++++++++-------- 6 files changed, 121 insertions(+), 51 deletions(-) diff --git a/.gitignore b/.gitignore index 0896441..cc5cc87 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 1631467..353cec7 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/Presentation/Lock/LockViewController.swift b/Atcha-iOS/Presentation/Lock/LockViewController.swift index d90fbce..927cfb4 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 8c5e379..0bca15e 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 75c1a8e..3c1c1ff 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 f91f003..8c6551f 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() + } }