Skip to content

Commit c3e95b3

Browse files
authored
Merge pull request #337 from Atcha-Project/env/dev
[Merge] Stage 환경 배포 및 QA 진행 (v1.9.7)
2 parents a03259b + 913c290 commit c3e95b3

11 files changed

Lines changed: 219 additions & 125 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,6 @@ StageConfig.xcconfig
114114
LiveConfig.xcconfig
115115
BaseConfig.xcconfig
116116

117+
.claude
118+
117119
Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Onboarding/.DS_Store

Atcha-iOS.xcodeproj/project.pbxproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2539,7 +2539,7 @@
25392539
"$(inherited)",
25402540
"@executable_path/Frameworks",
25412541
);
2542-
MARKETING_VERSION = 1.9.6;
2542+
MARKETING_VERSION = 1.9.7;
25432543
PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS;
25442544
PRODUCT_NAME = "$(TARGET_NAME)";
25452545
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2586,7 +2586,7 @@
25862586
"$(inherited)",
25872587
"@executable_path/Frameworks",
25882588
);
2589-
MARKETING_VERSION = 1.9.6;
2589+
MARKETING_VERSION = 1.9.7;
25902590
OTHER_SWIFT_FLAGS = "";
25912591
PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS;
25922592
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -2634,7 +2634,7 @@
26342634
"$(inherited)",
26352635
"@executable_path/Frameworks",
26362636
);
2637-
MARKETING_VERSION = 1.9.6;
2637+
MARKETING_VERSION = 1.9.7;
26382638
PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS;
26392639
PRODUCT_NAME = "$(TARGET_NAME)";
26402640
PROVISIONING_PROFILE_SPECIFIER = "";
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "alarm_cancel.png",
5+
"idiom" : "universal",
6+
"scale" : "1x"
7+
},
8+
{
9+
"filename" : "alarm_cancel@2x.png",
10+
"idiom" : "universal",
11+
"scale" : "2x"
12+
},
13+
{
14+
"filename" : "alarm_cancel@3x.png",
15+
"idiom" : "universal",
16+
"scale" : "3x"
17+
}
18+
],
19+
"info" : {
20+
"author" : "xcode",
21+
"version" : 1
22+
}
23+
}
438 Bytes
Loading
624 Bytes
Loading
835 Bytes
Loading

Atcha-iOS/Presentation/Location/MainViewController.swift

Lines changed: 78 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ final class MainViewController: BaseViewController<MainViewModel>,
7979
return viewModel.isGuideActiveInSession
8080
}
8181
private var guestTapCount = 0
82+
private var guestLoginWorkItem: DispatchWorkItem?
8283

8384
// MARK: - Life Cycle
8485

@@ -778,27 +779,28 @@ extension MainViewController {
778779
}
779780

780781
private func bindServiceRegionUpdates() {
781-
viewModel.$isServiceRegion
782-
.removeDuplicates()
782+
// isServiceRegion과 isGuest 상태를 결합하여 판단
783+
Publishers.CombineLatest(viewModel.$isServiceRegion, viewModel.$isGuest)
784+
.removeDuplicates { $0.0 == $1.0 && $0.1 == $1.1 }
783785
.receive(on: RunLoop.main)
784-
.sink { [weak self] ok in
786+
.sink { [weak self] ok, isGuest in
785787
guard let self = self else { return }
786788
self.latestIsServiceRegion = ok
787789

788-
switch ok {
789-
case .some(true):
790-
self.lastTrainSearchView.updateSearchEnabled(true)
791-
case .some(false):
790+
// 핵심 로직:
791+
// 1. 게스트 모드라면 지역에 상관없이 항상 활성화 (로그인 시트 유도)
792+
// 2. 회원 모드라면 서비스 지역(ok == true)일 때만 활성화
793+
let shouldEnableSearch = isGuest || (ok == true)
794+
self.lastTrainSearchView.updateSearchEnabled(shouldEnableSearch)
795+
796+
// 서비스 지역이 아닐 때의 데이터 정리
797+
if ok == false {
792798
self.latestFareString = nil
793799
self.viewModel.taxiFare = nil
794-
self.lastTrainSearchView.updateSearchEnabled(false)
795-
case .none:
796-
self.lastTrainSearchView.updateSearchEnabled(false)
797800
}
798801

799-
// 검색 모드일 때는 즉시 말풍선 글자 업데이트
802+
// 검색 모드일 말풍선 업데이트 로직 유지
800803
if self.viewModel.bottomType == .search || self.viewModel.bottomType == nil {
801-
// 방해물(업데이트 생략 조건문) 제거!
802804
self.showOrUpdatePersistentBalloon(
803805
isFirstVisit: self.isFirstVisit,
804806
isServiceRegion: ok,
@@ -966,100 +968,103 @@ extension MainViewController {
966968
}
967969

968970
@objc private func handleBallonTap() {
969-
safeStartJump()
971+
safeStartJump() // 캐릭터 점프
970972

973+
// 1. 게스트 모드일 때
971974
if viewModel.isGuest {
975+
// [요청사항 반영] 서비스 지역 외라면 즉시 로그인 시트 노출 (1-tap)
972976
if latestIsServiceRegion == false {
973977
presentLoginAlert()
974978
return
975979
}
976980

977-
// 서비스 지역 내라면 기존의 2-tap 로직(문구 노출 후 로그인) 유지
981+
// 서비스 지역 내라면 문구 노출 후 2초 뒤 자동 로그인 (handleGuestBallonTap으로 이동)
978982
handleGuestBallonTap()
979983
return
980984
}
981985

982-
// 알람 등록 후(departure 상태)일 때만 반응
986+
// 2. 회원 모드일 때 (알람 등록된 상태에서만 동작)
983987
guard viewModel.bottomType == .departure else { return }
984988

985989
amp_track(.character_click)
986-
987-
let cycle = postAlarmTapIndex % 3
988-
989-
// 추가: 현재 알람이 울린 상태인지 확인
990990
let isAlarmFired = UserDefaultsWrapper.shared.bool(forKey: UserDefaultsWrapper.Key.departureAlarmDidFire.rawValue) ?? false
991+
let cycle = postAlarmTapIndex % 3
991992

992993
if cycle == 0 {
993-
// 요금 정보 표시 (동적 로딩)
994-
let now = CACurrentMediaTime()
995-
if (now - lastFareRefreshTime) > fareRefreshInterval && !isFetchingFare {
996-
isFetchingFare = true
997-
Task {
998-
defer { Task { @MainActor in self.isFetchingFare = false } }
999-
do {
1000-
let fare = try await viewModel.fetchFareForRegisteredStart()
1001-
let fareInt = Int(fare)
1002-
let fareStr = self.decimalFormatter.string(from: NSNumber(value: fareInt)) ?? "\(fareInt)"
1003-
await MainActor.run {
1004-
self.latestFareString = fareStr
1005-
self.lastFareRefreshTime = CACurrentMediaTime()
1006-
let displayFare = self.viewModel.isGuest ? "???원" : "\(fareStr)"
1007-
self.showTransientBalloon(isFare: true, text: displayFare)
1008-
self.postAlarmTapIndex += 1
1009-
}
1010-
} catch {
1011-
await MainActor.run {
1012-
self.showTransientBalloon(isFare: false, text: "택시비 조회에 실패했어요")
1013-
self.postAlarmTapIndex += 1
1014-
}
1015-
}
1016-
}
1017-
} else {
1018-
let fareStr = latestFareString ?? "???"
1019-
let displayFare = viewModel.isGuest ? "???원" : "\(fareStr)"
1020-
showTransientBalloon(isFare: true, text: displayFare)
1021-
self.postAlarmTapIndex += 1
1022-
}
1023-
994+
handleMemberFareTap() // 요금 정보 조회/표시 로직
1024995
} else if cycle == 1 {
1025-
// 수정: 알람이 울렸다면 이 메시지를 건너뛰고 다음 메시지를 띄움
1026996
if isAlarmFired {
1027997
showTransientBalloon(isFare: false, text: "교통 상황에 따라 시간이 달라질 수 있어요")
1028-
// cycle 1을 건너뛰었으므로 다음 탭이 cycle 0(택시비)으로 돌아가도록 index를 2 올려줌
1029998
postAlarmTapIndex += 2
1030999
} else {
10311000
showTransientBalloon(isFare: false, text: "시간에 맞춰 알림을 드릴게요")
10321001
postAlarmTapIndex += 1
10331002
}
1034-
10351003
} else {
10361004
showTransientBalloon(isFare: false, text: "교통 상황에 따라 시간이 달라질 수 있어요")
10371005
postAlarmTapIndex += 1
10381006
}
10391007
}
1040-
1008+
1009+
/// 게스트 전용: 서비스 지역 내(서울/경기/인천)에서 문구 노출 후 2초 뒤 자동 로그인
10411010
private func handleGuestBallonTap() {
1042-
if guestTapCount == 0 {
1043-
// 첫 번째 터치: "궁금하면 로그인 해봐요!"
1044-
guestTapCount = 1
1045-
1046-
ballonView.layer.removeAllAnimations()
1047-
ballonView.isHidden = false
1048-
ballonView.alpha = 1
1049-
1050-
ballonView.setupTitle(topMessage: nil, bottomMessage: "택시비가 궁금하면 로그인해봐요!")
1051-
ballonView.animateStaggered(secondaryDelay: 0, fade: 0.25)
1011+
// 기존에 예약된 타이머가 있다면 취소 (중복 실행 방지)
1012+
guestLoginWorkItem?.cancel()
1013+
1014+
ballonView.layer.removeAllAnimations()
1015+
ballonView.isHidden = false
1016+
ballonView.alpha = 1
1017+
1018+
ballonView.setupTitle(topMessage: nil, bottomMessage: "택시비가 궁금하면 로그인해봐요!")
1019+
ballonView.animateStaggered(secondaryDelay: 0, fade: 0.25)
1020+
1021+
let workItem = DispatchWorkItem { [weak self] in
1022+
guard let self = self, self.viewModel.isGuest else { return }
10521023

1024+
// 현재 떠있는 화면이 없을 때만 로그인 시트 노출
1025+
if self.presentedViewController == nil {
1026+
self.presentLoginAlert()
1027+
}
1028+
}
1029+
1030+
self.guestLoginWorkItem = workItem
1031+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: workItem)
1032+
}
1033+
1034+
/// 회원 전용: 실시간 택시 요금 조회 로직
1035+
private func handleMemberFareTap() {
1036+
let now = CACurrentMediaTime()
1037+
1038+
if (now - lastFareRefreshTime) < fareRefreshInterval || isFetchingFare {
1039+
let fareStr = latestFareString ?? "???"
1040+
showTransientBalloon(isFare: true, text: "\(fareStr)")
1041+
postAlarmTapIndex += 1
10531042
} else {
1054-
// 두 번째 터치: 로그인 시트 노출
1055-
guestTapCount = 0 // 카운트 리셋
1056-
1057-
// [중요] 말풍선을 즉시 숨김 상태로 만들어야 시트가 내려간 뒤 다시 Persistent(???원) 메시지가 나타납니다.
1058-
ballonView.layer.removeAllAnimations()
1059-
ballonView.isHidden = true
1060-
ballonView.alpha = 0
1061-
1062-
presentLoginAlert()
1043+
isFetchingFare = true
1044+
Task { [weak self] in // weak self 추가
1045+
guard let self = self else { return }
1046+
defer {
1047+
Task { @MainActor in self.isFetchingFare = false }
1048+
}
1049+
1050+
do {
1051+
let fare = try await viewModel.fetchFareForRegisteredStart()
1052+
let fareInt = Int(fare)
1053+
let fareStr = self.decimalFormatter.string(from: NSNumber(value: fareInt)) ?? "\(fareInt)"
1054+
1055+
await MainActor.run {
1056+
self.latestFareString = fareStr
1057+
self.lastFareRefreshTime = CACurrentMediaTime()
1058+
self.showTransientBalloon(isFare: true, text: "\(fareStr)")
1059+
self.postAlarmTapIndex += 1
1060+
}
1061+
} catch {
1062+
await MainActor.run {
1063+
self.showTransientBalloon(isFare: false, text: "택시비 조회에 실패했어요")
1064+
self.postAlarmTapIndex += 1
1065+
}
1066+
}
1067+
}
10631068
}
10641069
}
10651070
}
@@ -1281,10 +1286,6 @@ extension MainViewController {
12811286
private func showOrUpdatePersistentBalloon(isFirstVisit: Bool, isServiceRegion: Bool?, fareStr: String?) {
12821287
guard !isShowingToast else { return }
12831288

1284-
if viewModel.isGuest && guestTapCount == 1 {
1285-
return
1286-
}
1287-
12881289
// [수정] 우리가 정의한 로그인 기반 가이드 로직 적용
12891290
let showGuideLine = shouldShowMapGuide
12901291
let topText: String? = showGuideLine ? "지도를 움직여 출발지를 설정해요" : nil

Atcha-iOS/Presentation/Lock/LockViewController.swift

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ final class LockViewController: BaseViewController<LockViewModel> {
1515
private let titleLabel: UILabel = UILabel()
1616
private let taxiFareLabel: UILabel = UILabel()
1717
private let startButton: AtchaButton = AtchaButton(text: "출발하기", size: .h52, style: .filled(.primary))
18+
private let cancelImageView: UIImageView = UIImageView()
1819
private let detailRouteButton: AtchaButton = AtchaButton(text: "더 늦은 경로 확인하기", size: .h52, style: .filled(.opacity))
1920
private let bottomStack: UIStackView = UIStackView()
2021
private var lottieAnimationView: LottieAnimationView = LottieAnimationView(name: "Alarm")
@@ -71,7 +72,7 @@ final class LockViewController: BaseViewController<LockViewModel> {
7172

7273
// MARK: - Lock UI
7374
private func setupUI() {
74-
view.addSubViews(backgroundImageView, lottieAnimationView, gradientView, logoImageView, titleLabel, taxiFareLabel, bottomStack)
75+
view.addSubViews(backgroundImageView, lottieAnimationView, gradientView, logoImageView, titleLabel, taxiFareLabel, bottomStack, cancelImageView)
7576

7677
backgroundImageView.image = UIImage.lockBackground
7778
gradient.colors = [
@@ -96,6 +97,9 @@ final class LockViewController: BaseViewController<LockViewModel> {
9697
startButton.addTarget(self,
9798
action: #selector(startTapped),
9899
for: .touchUpInside)
100+
cancelImageView.image = UIImage.alarmCancel
101+
cancelImageView.isUserInteractionEnabled = true
102+
cancelImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cancelAlarmTapped)))
99103

100104
detailRouteButton.addTarget(self,
101105
action: #selector(detailRouteTapped),
@@ -137,6 +141,12 @@ final class LockViewController: BaseViewController<LockViewModel> {
137141
make.leading.equalToSuperview().offset(20)
138142
make.trailing.equalToSuperview().inset(20)
139143
}
144+
145+
cancelImageView.snp.makeConstraints { make in
146+
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).inset(18)
147+
make.trailing.equalToSuperview().inset(16)
148+
make.size.equalTo(24)
149+
}
140150
}
141151

142152
@objc private func startTapped() {
@@ -175,6 +185,10 @@ final class LockViewController: BaseViewController<LockViewModel> {
175185
)
176186
}
177187

188+
@objc private func cancelAlarmTapped() {
189+
showAlarmCancelPopup()
190+
}
191+
178192
private func observeAlarmTimeout() {
179193
NotificationCenter.default.publisher(for: NSNotification.Name("alarmDidTimeout"))
180194
.receive(on: RunLoop.main)
@@ -184,4 +198,34 @@ final class LockViewController: BaseViewController<LockViewModel> {
184198
}
185199
.store(in: &cancellables)
186200
}
201+
202+
private func showAlarmCancelPopup() {
203+
AlarmManager.shared.stopAlarm()
204+
205+
let popupVM = AtchaPopupViewModel(info: .alarm_cancel)
206+
let popupVC = AtchaPopupViewController(viewModel: popupVM)
207+
208+
popupVC.cancelButton.addAction(UIAction { [weak popupVC] _ in
209+
popupVC?.dismiss(animated: false)
210+
}, for: .touchUpInside)
211+
212+
popupVC.confirmButton.addAction(UIAction { [weak self, weak popupVC] _ in
213+
guard let self else { return }
214+
popupVC?.dismiss(animated: false)
215+
216+
self.viewModel.cancelLockScreenTimer()
217+
AlarmManager.shared.stopAlarm()
218+
AlarmManager.shared.removeAllAlarmNotificationsExceptAutoStop()
219+
220+
UserDefaultsWrapper.shared.set(
221+
false,
222+
forKey: UserDefaultsWrapper.Key.departureAlarmDidFire.rawValue
223+
)
224+
225+
viewModel.routerHandler?(.dismissLockScreen)
226+
}, for: .touchUpInside)
227+
228+
popupVC.modalPresentationStyle = .overFullScreen
229+
present(popupVC, animated: false)
230+
}
187231
}

Atcha-iOS/Presentation/Main/MainCoordinator.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,12 @@ final class MainCoordinator: NSObject {
320320
context: .afterReigster))
321321
}
322322
case .dismissLockScreen:
323-
self?.navigationController.dismiss(animated: true)
323+
self?.mainViewModel?.stopAlarmTimeoutTimer()
324+
self?.navigationController.dismiss(animated: true) { [weak self] in
325+
self?.mainViewModel?.alarmDelete()
326+
self?.mainViewModel?.bottomType = .search
327+
self?.mainViewModel?.removeLegInfoAndAddress()
328+
}
324329
default: do {}
325330
}
326331
}

0 commit comments

Comments
 (0)