@@ -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
0 commit comments