From cd27ce21a2026592ec4af900b3c75307c7271393 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Sat, 14 Mar 2026 00:52:04 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[FEAT]=20Intro,=20Main=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EC=A7=84=EC=9E=85=20=EB=B0=8F=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=B6=94=EC=A0=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Atcha-iOS.xcodeproj/project.pbxproj | 6 +- .../Manager/Amplitude/AmplitudeEvent.swift | 109 ++++++++++++------ .../Manager/Amplitude/AmplitudeManager.swift | 45 ++++---- .../Intro/IntroViewController.swift | 8 ++ .../Location/MainViewController.swift | 31 +++-- .../View/LastTrainSearchBottomView.swift | 6 + .../Login/LoginViewController.swift | 4 + .../HomeRegisterViewController.swift | 13 ++- .../User/Home/HomeFindViewController.swift | 4 +- 9 files changed, 147 insertions(+), 79 deletions(-) diff --git a/Atcha-iOS.xcodeproj/project.pbxproj b/Atcha-iOS.xcodeproj/project.pbxproj index b2665eb7..f3833992 100644 --- a/Atcha-iOS.xcodeproj/project.pbxproj +++ b/Atcha-iOS.xcodeproj/project.pbxproj @@ -2511,7 +2511,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.8; + MARKETING_VERSION = 1.9; PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2558,7 +2558,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.8; + MARKETING_VERSION = 1.9; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2606,7 +2606,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.8; + MARKETING_VERSION = 1.9; PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Atcha-iOS/Core/Manager/Amplitude/AmplitudeEvent.swift b/Atcha-iOS/Core/Manager/Amplitude/AmplitudeEvent.swift index d43c0d19..4eff1345 100644 --- a/Atcha-iOS/Core/Manager/Amplitude/AmplitudeEvent.swift +++ b/Atcha-iOS/Core/Manager/Amplitude/AmplitudeEvent.swift @@ -8,42 +8,78 @@ import Foundation enum AmplitudeEvent: String { - case permission_setting = "permission_setting" - case search_location_click = "search_location_click" + // MARK: - 온보딩 + case intro_view = "intro_view" + case intro_start_click = "intro_start_click" + + // MARK: - 메인 + case main_view = "main_view" // 1. 비로그인 진입 2. 로그인 진입 + case mypage_click = "mypage_click" case current_location_click = "current_location_click" - case home_register = "home_register" - case alarm_alert_type_setting = "alarm_alert_type_setting" - case signup = "signup" + + case departure_change_click = "departure_change_click" + case home_change_click = "home_change_click" case course_search_click = "course_search_click" + + case login_click = "login_click" + case home_register_view = "home_register_view" // 1.가입 2.메인 3.설정 + case home_setting_view = "home_setting_view" + case home_setting_click = "home_search_click" + + case character_click = "character_click" + case alarm_force_stop = "alarm_force_stop" + case alarm_timeout_stop = "alarm_timeout_stop" + case departure_time_click = "departure_time_click" + case course_click = "course_click" + + + // MARK: - 마이페이지 + case signup = "signup" + case search_location_click = "search_location_click" + case origin_search_click = "origin_search_click" - case origin_setting = "origin_setting" + case my_page_click = "my_page_click" + case alarm_cancel = "alarm_cancel" + + + // MARK: - 마이페이지 + case logout = "logout" + case withdraw = "withdraw" + case alarm_alert_type_setting = "alarm_alert_type_setting" + case home_register = "home_register" + + // MARK: - 경로 탐색 case course_change_click = "course_change_click" - case course_detail_toggle_click = "course_detail_toggle_click" case course_detail_click = "course_detail_click" case bus_detail_click = "bus_detail_click" case bus_info_click = "bus_info_click" case long_interval_alarm_register = "long_interval_alarm_register" case alarm_register = "alarm_register" - case character_click = "character_click" - case origin_time_click = "origin_time_click" - case course_click = "course_click" + case another_alarm_register = "another_alarm_register" case course_refresh_click = "course_refresh_click" + + // MARK: - 경로 수정 + case origin_setting = "origin_setting" + + // MARK: - 알람화면 case start_click = "start_click" case later_course_click = "later_course_click" - case another_alarm_register = "another_alarm_register" - case logout = "logout" - case withdraw = "withdraw" - case alarm_cancel = "alarm_cancel" } enum AmplitudePropertyKey: String { case alertType = "alert_type" case dwellTime = "dwell_time" - case screenName = "screen_name" case withdrawReason = "withdraw_reason" + case social = "social" + case userStatus = "userStatus" + case entryPoint = "entryPoint" } enum AmplitudeProperty { + static func social(_ social: SocialType) -> (String, Any) { + (AmplitudePropertyKey.social.rawValue, social.rawValue) + } + static func alertType(_ type: AlertType) -> (String, Any) { (AmplitudePropertyKey.alertType.rawValue, type.rawValue) } @@ -52,32 +88,17 @@ enum AmplitudeProperty { (AmplitudePropertyKey.dwellTime.rawValue, seconds) } - static func screenName(_ screen: ScreenName) -> (String, Any) { - (AmplitudePropertyKey.screenName.rawValue, screen.rawValue) + static func userStatus(_ userStatus: UserStatus) -> (String, Any) { + (AmplitudePropertyKey.userStatus.rawValue, userStatus.rawValue) } static func withdrawReason(_ reason: WithdrawReason) -> (String, Any) { (AmplitudePropertyKey.withdrawReason.rawValue, reason.rawValue) } -} - -enum ScreenName: String { - case home_register = "우리집 등록" - case home_search = "우리집 검색" - case home_setting = "우리집 설정" - case alarm_setting = "알람 설정" - case mypage = "마이페이지" - case account = "내 계정" - case withdraw = "계정 탈퇴" - case terms = "약관" - case main = "메인" - case course_search = "경로 탐색" - case course_detail = "상세 경로" - case origin_search = "출발지 검색" - case origin_setting = "출발지 설정" - case alarm = "알람" - case bus_detail = "버스 경로" - case bus_info = "버스 정보" + + static func entryPoint(_ entryPoint: EntryPoint) -> (String, Any) { + (AmplitudePropertyKey.entryPoint.rawValue, entryPoint.rawValue) + } } enum AlertType: String { @@ -94,3 +115,19 @@ enum WithdrawReason: String { case dont_know_how = "앱 사용법을 모르겠어요" case map_app_enough = "기존에 쓰던 지도 앱으로 충분해요" } + +enum SocialType: String { + case kakao = "kakao" + case apple = "apple" +} + +enum UserStatus: String { + case guest = "guest" + case member = "member" +} + +enum EntryPoint: String { + case signup = "signup" + case main = "main" + case mypage = "mypage" +} diff --git a/Atcha-iOS/Core/Manager/Amplitude/AmplitudeManager.swift b/Atcha-iOS/Core/Manager/Amplitude/AmplitudeManager.swift index 8bf74eec..b3806bd7 100644 --- a/Atcha-iOS/Core/Manager/Amplitude/AmplitudeManager.swift +++ b/Atcha-iOS/Core/Manager/Amplitude/AmplitudeManager.swift @@ -13,14 +13,14 @@ import UIKit final class AmplitudeManager { static let shared = AmplitudeManager() private init() {} - + private let queue = DispatchQueue(label: "amp.manager.queue") private var client: Amplitude? - + private var timers: [String: Date] = [:] - + // MARK: Public API - + func start( userId: Int? = nil, autocapture: AutocaptureOptions = [.sessions, .appLifecycles], @@ -32,13 +32,13 @@ final class AmplitudeManager { assertionFailure("[AmplitudeManager] Missing AMPLITUDE_API_KEY. Check Info.plist + xcconfig mapping.") return } - + let config = Configuration( apiKey: apiKey, logLevel: logLevel, autocapture: autocapture ) - + queue.sync { let c = Amplitude(configuration: config) if let uid = userId { @@ -47,18 +47,18 @@ final class AmplitudeManager { self.client = c } } - + func bindUser(id: String) { queue.async { [weak self] in guard let self, let client = self.client else { return } client.setUserId(userId: "USER_ID: \(id)") } } - + func track(_ event: AmplitudeEvent, _ properties: [String: Any?] = [:]) { track(event.rawValue, properties) } - + func track(_ event: String, _ properties: [String: Any?] = [:]) { queue.async { [weak self] in guard let self, let client = self.client else { return } @@ -66,13 +66,7 @@ final class AmplitudeManager { client.track(eventType: event, eventProperties: props) } } - - func trackScreen(_ screen: ScreenName, _ properties: [String: Any?] = [:]) { - var props = properties - props[AmplitudePropertyKey.screenName.rawValue] = screen.rawValue - track("screen_view", props) - } - + func identify( set: [String: Any?] = [:], add: [String: Double] = [:], @@ -91,25 +85,25 @@ final class AmplitudeManager { client.identify(identify: i) } } - + func setUserProperties(_ properties: [String: Any?]) { identify(set: properties) } - + func reset() { queue.async { [weak self] in guard let self, let client = self.client else { return } client.reset() } } - + func flush() { queue.async { [weak self] in guard let self, let client = self.client else { return } client.flush() } } - + var deviceId: String? { queue.sync { client?.getDeviceId() } } @@ -120,7 +114,7 @@ private extension AmplitudeManager { static func readApiKey() -> String? { Bundle.main.object(forInfoDictionaryKey: "AMPLITUDE_API_KEY") as? String } - + static func clean(_ dict: [String: Any?]) -> [String: Any] { var out: [String: Any] = [:] dict.forEach { k, v in if let v = v { out[k] = v } } @@ -130,8 +124,8 @@ private extension AmplitudeManager { // MARK: - UIKit convenience extension UIViewController { - func amp_trackScreen(_ screen: ScreenName, extra: [String: Any?] = [:]) { - AmplitudeManager.shared.trackScreen(screen, extra) + func amp_track(_ event: AmplitudeEvent, props: [String: Any?] = [:]) { + AmplitudeManager.shared.track(event, props) } } @@ -140,7 +134,7 @@ extension AmplitudeManager { func timerStart(_ key: String) { queue.async { [weak self] in self?.timers[key] = Date() } } - + /// 타이머 종료(초 단위 반환). 없으면 0 @discardableResult func timerEndSeconds(_ key: String) -> Int { @@ -149,7 +143,7 @@ extension AmplitudeManager { guard let s = start else { return 0 } return Int(Date().timeIntervalSince(s).rounded()) } - + /// 사용자 프로퍼티 값을 누적(+) func incrementUserProperty(_ key: String, by value: Double = 1) { queue.async { [weak self] in @@ -168,3 +162,4 @@ func props(_ items: (String, Any)... ) -> AmpProps { items.forEach { dict[$0.0] = $0.1 } return dict } + diff --git a/Atcha-iOS/Presentation/Intro/IntroViewController.swift b/Atcha-iOS/Presentation/Intro/IntroViewController.swift index dbf1dd20..77855fc2 100644 --- a/Atcha-iOS/Presentation/Intro/IntroViewController.swift +++ b/Atcha-iOS/Presentation/Intro/IntroViewController.swift @@ -79,6 +79,12 @@ final class IntroViewController: BaseViewController { autoScrollTimer = nil } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + amp_track(.intro_view) + } + private func setupUI() { view.addSubViews(backgroundImageView, pageControl, collectionView, guestLoginButton) @@ -184,6 +190,8 @@ extension IntroViewController { // MARK: - Actions @objc private func didTapGuestLogin() { viewModel.guestLoginTapped() + + amp_track(.intro_start_click) } } diff --git a/Atcha-iOS/Presentation/Location/MainViewController.swift b/Atcha-iOS/Presentation/Location/MainViewController.swift index 0d911848..87f9f380 100644 --- a/Atcha-iOS/Presentation/Location/MainViewController.swift +++ b/Atcha-iOS/Presentation/Location/MainViewController.swift @@ -203,7 +203,16 @@ final class MainViewController: BaseViewController, self.viewModel.refreshCurrentMapCenterData() - AmplitudeManager.shared.trackScreen(.main) + let isGuest = UserDefaultsWrapper.shared.bool( + forKey: UserDefaultsWrapper.Key.isGuest.rawValue + ) ?? false + + if isGuest { + amp_track(.main_view, props: props(AmplitudeProperty.userStatus(.guest))) + } else { + amp_track(.main_view, props: props(AmplitudeProperty.userStatus(.member))) + } + } override func viewWillDisappear(_ animated: Bool) { @@ -400,8 +409,6 @@ extension MainViewController { if viewModel.isGuest { presentLoginAlert() } else { - AmplitudeManager.shared.track(.origin_search_click) - viewModel.handleRoute(route: .changeCourse( location: Location(name: "", lat: 0.0, lon: 0.0, businessCategory: "", address: "", radius: ""))) } @@ -414,8 +421,6 @@ extension MainViewController { return } - AmplitudeManager.shared.track(.course_search_click) - let wrapper = UserDefaultsWrapper.shared let endLatStr = wrapper.string(forKey: UserDefaultsWrapper.Key.homeLat.rawValue) ?? "37.554722" let endLonStr = wrapper.string(forKey: UserDefaultsWrapper.Key.homeLon.rawValue) ?? "126.970833" @@ -453,14 +458,17 @@ extension MainViewController { self.showOrUpdateImmediateBalloon( .text(top: nil, bottom: "위치를 변경하려면 알람을 종료해야 해요") ) - AmplitudeManager.shared.track(.course_click) + + amp_track(.course_click) + case .reloadTapped: viewModel.refreshDepatrueTime() case .timeTapped: self.showOrUpdateImmediateBalloon( .text(top: "이때 자리에서 출발하면 돼요", bottom: "교통 상황에 따라 시간이 달라질 수 있어요") ) - AmplitudeManager.shared.track(.origin_time_click) + + amp_track(.departure_time_click) } } @@ -492,6 +500,8 @@ extension MainViewController { self.viewModel.alarmDelete() self.exitButtonTapped() + + amp_track(.alarm_force_stop) }, for: .touchUpInside) popupVC.modalPresentationStyle = .overFullScreen @@ -1019,6 +1029,7 @@ extension MainViewController { presentLoginAlert() } else { viewModel.handleRoute(route: .myPage) + amp_track(.mypage_click) } } @@ -1036,6 +1047,8 @@ extension MainViewController { shouldCenterToCurrentLocationOnce = true viewModel.setupLocation() } + + amp_track(.current_location_click) } private func safeStartJump() { @@ -1056,7 +1069,7 @@ extension MainViewController { case .pre: print("") case .next: - AmplitudeManager.shared.track(.character_click) + amp_track(.character_click) let now = CACurrentMediaTime() let shouldRefreshFare = (now - lastFareRefreshTime) > fareRefreshInterval @@ -1481,6 +1494,8 @@ extension MainViewController { self.viewModel.alarmDelete() self.exitButtonTapped() + amp_track(.alarm_timeout_stop) + if let coord = self.viewModel.currentLocation { self.mapContainerView.setupCenter(location: coord) self.viewModel.selectedLocation = coord // 주소도 다시 검색 diff --git a/Atcha-iOS/Presentation/Location/View/LastTrainSearchBottomView.swift b/Atcha-iOS/Presentation/Location/View/LastTrainSearchBottomView.swift index 9bdf6777..bf21d9ce 100644 --- a/Atcha-iOS/Presentation/Location/View/LastTrainSearchBottomView.swift +++ b/Atcha-iOS/Presentation/Location/View/LastTrainSearchBottomView.swift @@ -158,13 +158,19 @@ final class LastTrainSearchBottomView: UIView { extension LastTrainSearchBottomView { @objc private func handleCurrentTap() { actionPublisher.send(.currentTapped) + + AmplitudeManager.shared.track(.departure_change_click) } @objc private func handleSearchTap() { actionPublisher.send(.searchTapped) + + AmplitudeManager.shared.track(.course_search_click) } @objc private func handleDestinationTap() { actionPublisher.send(.homeChangeTapped) + + AmplitudeManager.shared.track(.home_change_click) } } diff --git a/Atcha-iOS/Presentation/Login/LoginViewController.swift b/Atcha-iOS/Presentation/Login/LoginViewController.swift index 4987e3cf..5e0be280 100644 --- a/Atcha-iOS/Presentation/Login/LoginViewController.swift +++ b/Atcha-iOS/Presentation/Login/LoginViewController.swift @@ -174,6 +174,8 @@ extension LoginViewController { @objc private func didTapKakaoLoginButton() { print("카카오 로그인 버튼 터치됨") viewModel.kakaoLoginTapped() + + amp_track(.login_click, props: props(AmplitudeProperty.social(.kakao))) } @objc private func didTapAppleLoginButton() { @@ -183,6 +185,8 @@ extension LoginViewController { ) { [weak self] delegate in self?.appleLoginDelegateWrapper = delegate } + + amp_track(.login_click, props: props(AmplitudeProperty.social(.apple))) } } diff --git a/Atcha-iOS/Presentation/Onboarding/HomeRegister/HomeRegisterViewController.swift b/Atcha-iOS/Presentation/Onboarding/HomeRegister/HomeRegisterViewController.swift index d43c7fac..0c237b52 100644 --- a/Atcha-iOS/Presentation/Onboarding/HomeRegister/HomeRegisterViewController.swift +++ b/Atcha-iOS/Presentation/Onboarding/HomeRegister/HomeRegisterViewController.swift @@ -35,11 +35,14 @@ final class HomeRegisterViewController: BaseViewController, override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - AmplitudeManager.shared.trackScreen(.home_setting) + amp_track(.home_setting_view) } override func viewWillDisappear(_ animated: Bool) { @@ -87,7 +87,7 @@ final class HomeFindViewController: BaseViewController, AtchaToast(message: "앗차는 현재 서울, 경기, 인천에서만 이용 가능해요") .show(in: self.view) } - + amp_track(.home_setting_click) self.bottomView.isUserInteractionEnabled = true } } From 9befb437b04443c1eefb5171c94b667564e21f48 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Sat, 14 Mar 2026 03:53:18 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[FEAT]=20=EC=9C=A0=EC=A0=80=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=B6=94=EC=A0=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Manager/Amplitude/AmplitudeEvent.swift | 128 ++++++++++-------- .../Manager/Amplitude/AmplitudeManager.swift | 4 +- .../BusDetail/BusDetailViewController.swift | 6 +- .../BusInfo/BusInfoViewController.swift | 4 +- .../CourseModifyViewController.swift | 2 +- .../CourseSearchViewController.swift | 32 ++--- .../CourseSearch/CourseSearchViewModel.swift | 12 +- .../CourseSettingViewController.swift | 14 +- .../CourseSettingViewModel.swift | 3 +- .../DetailRouteViewController.swift | 35 +++-- .../Location/MainViewController.swift | 13 +- .../Presentation/Location/MainViewModel.swift | 2 +- .../SearchLocationViewController.swift | 2 +- .../View/LastTrainSearchBottomView.swift | 4 +- .../Lock/LockViewController.swift | 6 +- .../Login/LoginViewController.swift | 4 +- .../HomeRegisterViewController.swift | 8 +- .../Permission/PermissionViewController.swift | 2 - .../PushAlarm/PushAlarmViewController.swift | 10 +- .../PushAlarm/PushAlarmViewModel.swift | 7 - .../PushAlarmSheetViewController.swift | 16 +++ .../User/Home/HomeFindViewController.swift | 10 +- .../User/Home/HomeFindViewModel.swift | 8 +- .../MyAccount/MyAccountViewController.swift | 4 - .../User/MyPage/MyPageViewController.swift | 5 +- .../Withdraw/WithdrawViewController.swift | 2 - .../WebView/WebViewController.swift | 2 +- 27 files changed, 182 insertions(+), 163 deletions(-) diff --git a/Atcha-iOS/Core/Manager/Amplitude/AmplitudeEvent.swift b/Atcha-iOS/Core/Manager/Amplitude/AmplitudeEvent.swift index 4eff1345..bac7ac18 100644 --- a/Atcha-iOS/Core/Manager/Amplitude/AmplitudeEvent.swift +++ b/Atcha-iOS/Core/Manager/Amplitude/AmplitudeEvent.swift @@ -9,70 +9,81 @@ import Foundation enum AmplitudeEvent: String { // MARK: - 온보딩 - case intro_view = "intro_view" - case intro_start_click = "intro_start_click" + case intro_view = "인트로_진입" + case intro_start_click = "인트로_시작_클릭" // MARK: - 메인 - case main_view = "main_view" // 1. 비로그인 진입 2. 로그인 진입 - case mypage_click = "mypage_click" - case current_location_click = "current_location_click" - - case departure_change_click = "departure_change_click" - case home_change_click = "home_change_click" - case course_search_click = "course_search_click" - - case login_click = "login_click" - case home_register_view = "home_register_view" // 1.가입 2.메인 3.설정 - case home_setting_view = "home_setting_view" - case home_setting_click = "home_search_click" - - case character_click = "character_click" - case alarm_force_stop = "alarm_force_stop" - case alarm_timeout_stop = "alarm_timeout_stop" - case departure_time_click = "departure_time_click" - case course_click = "course_click" + case main_view = "메인_진입" // 1. 비로그인 진입 2. 로그인 진입 + case mypage_click = "마이페이지_클릭" + case current_location_click = "현재_위치_버튼_클릭" + + case departure_modify_click = "출발지_수정_클릭" + case home_modify_click = "집주소_수정_클릭" + case course_search_click = "막차_검색하기_클릭" + + case login_view = "로그인_진입" // 1. 마이페이지 2.출발지 수정 3.집주소 수정 4.막차검색하기 + case login_click = "로그인_클릭" + case home_register_view = "집주소_등록_진입" // 1.가입 2.메인 3.설정 + case home_search_view = "집주소_검색_진입" + case home_setting_view = "집주소_설정_진입" + case home_setting_click = "집주소_설정_클릭" + case signup = "회원가입" + + case character_click = "캐릭터_클릭" + case alarm_force_stop = "알람_강제_종료" + case alarm_timeout_stop = "알람_타임아웃_종료" + case alarm_arrive_stop = "알람_도착_종료" + case departure_time_click = "출발시간_영역_클릭" + case course_click = "경로_영역_클릭" // MARK: - 마이페이지 - case signup = "signup" - case search_location_click = "search_location_click" + case mypage_view = "마이페이지_진입" + case logout = "로그아웃" + case withdraw = "회원탈퇴" + case alarm_alert_type_setting = "알람설정" + case term = "약관동의_진입" + case feedback = "피드백_진입" - case origin_search_click = "origin_search_click" - case my_page_click = "my_page_click" - case alarm_cancel = "alarm_cancel" - - // MARK: - 마이페이지 - case logout = "logout" - case withdraw = "withdraw" - case alarm_alert_type_setting = "alarm_alert_type_setting" - case home_register = "home_register" + // MARK: - 알람화면 + case alarm_view = "알람_진입" + case start_click = "출발하기_클릭" + case later_course_click = "늦은_경로_확인하기_클릭" + // MARK: - 경로 탐색 - case course_change_click = "course_change_click" - case course_detail_click = "course_detail_click" - case bus_detail_click = "bus_detail_click" - case bus_info_click = "bus_info_click" - case long_interval_alarm_register = "long_interval_alarm_register" - case alarm_register = "alarm_register" - case another_alarm_register = "another_alarm_register" - case course_refresh_click = "course_refresh_click" + case course_search_view = "경로_탐색_진입" + case course_modify_click = "경로_수정_클릭" + case alarm_register = "알람_등록" // 1. 경로 탐색 2.경로 상세 + case another_alarm_register = "다른_알람_등록" + case long_interval_alarm_register = "배차_긴_알람_등록" + case course_detail_click = "경로_상세_영역_클릭" + + // MARK: - 경로 상세 + case course_detail_view = "경로_상세_진입" + case course_refresh_click = "경로_새로고침_클릭" + case bus_detail_view = "버스_상세_진입" + case bus_detail_click = "버스_상세_클릭" + case bus_info_click = "버스_정보_클릭" + case bus_info_view = "버스_정보_진입" + case bus_refresh_click = "버스_새로고침_클릭" + case bus_info_refresh_click = "버스_정보_새로고침_클릭" - // MARK: - 경로 수정 - case origin_setting = "origin_setting" - // MARK: - 알람화면 - case start_click = "start_click" - case later_course_click = "later_course_click" + // MARK: - 경로 수정 + case departure_modify_view = "출발지_수정_진입" + case departure_setting_view = "출발지_설정_진입" + case departure_setting_click = "출발지_설정_클릭" } enum AmplitudePropertyKey: String { - case alertType = "alert_type" - case dwellTime = "dwell_time" - case withdrawReason = "withdraw_reason" - case social = "social" - case userStatus = "userStatus" - case entryPoint = "entryPoint" + case alertType = "알람_방식" + case dwellTime = "알람_등록_시간" + case withdrawReason = "탈퇴_사유" + case social = "로그인_방식" + case userStatus = "로그인_상태" + case entryPoint = "진입_경로" } enum AmplitudeProperty { @@ -117,17 +128,20 @@ enum WithdrawReason: String { } enum SocialType: String { - case kakao = "kakao" - case apple = "apple" + case kakao = "카카오" + case apple = "애플" } enum UserStatus: String { - case guest = "guest" - case member = "member" + case guest = "게스트" + case member = "로그인" } enum EntryPoint: String { - case signup = "signup" - case main = "main" - case mypage = "mypage" + case signup = "회원가입" + case main = "메인" + case mypage = "마이페이지" + case departure = "출발지_수정" + case home_modify = "집주소_수정" + case course_search = "막차_검색하기" } diff --git a/Atcha-iOS/Core/Manager/Amplitude/AmplitudeManager.swift b/Atcha-iOS/Core/Manager/Amplitude/AmplitudeManager.swift index b3806bd7..6a7a3fa2 100644 --- a/Atcha-iOS/Core/Manager/Amplitude/AmplitudeManager.swift +++ b/Atcha-iOS/Core/Manager/Amplitude/AmplitudeManager.swift @@ -124,8 +124,8 @@ private extension AmplitudeManager { // MARK: - UIKit convenience extension UIViewController { - func amp_track(_ event: AmplitudeEvent, props: [String: Any?] = [:]) { - AmplitudeManager.shared.track(event, props) + func amp_track(_ event: AmplitudeEvent, properties: [String: Any?] = [:]) { + AmplitudeManager.shared.track(event, properties) } } diff --git a/Atcha-iOS/Presentation/BusDetail/BusDetailViewController.swift b/Atcha-iOS/Presentation/BusDetail/BusDetailViewController.swift index 6b1ab5b2..99d3b03a 100644 --- a/Atcha-iOS/Presentation/BusDetail/BusDetailViewController.swift +++ b/Atcha-iOS/Presentation/BusDetail/BusDetailViewController.swift @@ -72,7 +72,7 @@ class BusDetailViewController: BaseViewController { } } - AmplitudeManager.shared.trackScreen(.bus_detail) + amp_track(.bus_detail_view) } override func viewDidDisappear(_ animated: Bool) { @@ -148,7 +148,7 @@ class BusDetailViewController: BaseViewController { private func bindActions() { headerView.onInfoTap = { [weak self] in self?.viewModel.didTapInfo() - AmplitudeManager.shared.track(.bus_info_click) + self?.amp_track(.bus_info_click) } } @@ -285,6 +285,8 @@ class BusDetailViewController: BaseViewController { refreshButton.start() showLoadingOnce() viewModel.refresh() + + amp_track(.bus_refresh_click) } private func itemAt(_ indexPath: IndexPath) -> BusRouteStationList? { diff --git a/Atcha-iOS/Presentation/BusDetail/BusInfo/BusInfoViewController.swift b/Atcha-iOS/Presentation/BusDetail/BusInfo/BusInfoViewController.swift index 496b8a57..dadcc42d 100644 --- a/Atcha-iOS/Presentation/BusDetail/BusInfo/BusInfoViewController.swift +++ b/Atcha-iOS/Presentation/BusDetail/BusInfo/BusInfoViewController.swift @@ -71,7 +71,7 @@ class BusInfoViewController: BaseViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - AmplitudeManager.shared.trackScreen(.bus_info) + amp_track(.bus_info_view) } override func viewDidDisappear(_ animated: Bool) { @@ -332,5 +332,7 @@ class BusInfoViewController: BaseViewController { refreshButton.start() showLoadingOnce() viewModel.refresh() + + amp_track(.bus_info_refresh_click) } } diff --git a/Atcha-iOS/Presentation/Course/CourseModify/CourseModifyViewController.swift b/Atcha-iOS/Presentation/Course/CourseModify/CourseModifyViewController.swift index 2f745127..63449fcc 100644 --- a/Atcha-iOS/Presentation/Course/CourseModify/CourseModifyViewController.swift +++ b/Atcha-iOS/Presentation/Course/CourseModify/CourseModifyViewController.swift @@ -54,7 +54,7 @@ final class CourseModifyViewController: BaseViewController Bool { diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift index 601da4e4..94d7633a 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift @@ -77,7 +77,7 @@ final class DetailRouteViewController: BaseViewController, } override func viewDidAppear(_ animated: Bool) { - AmplitudeManager.shared.trackScreen(.course_detail) + amp_track(.course_detail_view) } override func viewWillAppear(_ animated: Bool) { @@ -248,7 +248,7 @@ final class DetailRouteViewController: BaseViewController, bottomSheet.onBusDetail = { [weak self] info in self?.viewModel.onBusDetail?(info) - AmplitudeManager.shared.track(.bus_detail_click) + self?.amp_track(.bus_detail_click) } bottomSheet.getNewBusRealTime = { [weak self] in @@ -540,10 +540,9 @@ final class DetailRouteViewController: BaseViewController, self.viewModel.getAlarmTapped?(self.viewModel.address, self.viewModel.infos) let dwellSeconds = AmplitudeManager.shared.timerEndSeconds("alarm_dwell") - AmplitudeManager.shared.track( - .another_alarm_register, - props(AmplitudeProperty.dwellTime(seconds: dwellSeconds)) - ) + self.amp_track(.alarm_register, properties: props( + AmplitudeProperty.dwellTime(seconds: dwellSeconds) + )) // 등록 완료 후 메인 지도로 이동 (필요시 호출) self.navigationController?.popToMainViewControllerNoAnimation() @@ -616,6 +615,8 @@ extension DetailRouteViewController { make.top.equalToSuperview() make.bottom.equalToSuperview().inset(200) } + + amp_track(.current_location_click) } @objc private func didTapReload() { @@ -630,7 +631,7 @@ extension DetailRouteViewController { } viewModel.fetchInfo() - AmplitudeManager.shared.track(.course_refresh_click) + amp_track(.course_refresh_click) } } @@ -674,12 +675,10 @@ extension DetailRouteViewController { UserDefaultsWrapper.shared.set(true, forKey: UserDefaultsWrapper.Key.popRegister.rawValue) let dwellSeconds = AmplitudeManager.shared.timerEndSeconds("alarm_dwell") - AmplitudeManager.shared.track( - .another_alarm_register, - props( - AmplitudeProperty.dwellTime(seconds: dwellSeconds) - ) - ) + self.amp_track(.long_interval_alarm_register, properties: props( + AmplitudeProperty.dwellTime(seconds: dwellSeconds) + )) + }, for: .touchUpInside) popupVC.cancelButton.addAction(UIAction { [weak self, weak popupVC] _ in @@ -703,12 +702,10 @@ extension DetailRouteViewController { UserDefaultsWrapper.shared.set(true, forKey: UserDefaultsWrapper.Key.popRegister.rawValue) let dwellSeconds = AmplitudeManager.shared.timerEndSeconds("alarm_dwell") - AmplitudeManager.shared.track( - .another_alarm_register, - props( - AmplitudeProperty.dwellTime(seconds: dwellSeconds) - ) - ) + self.amp_track(.another_alarm_register, properties: props( + AmplitudeProperty.dwellTime(seconds: dwellSeconds) + )) + }, for: .touchUpInside) popupVC.cancelButton.addAction(UIAction { [weak self, weak popupVC] _ in diff --git a/Atcha-iOS/Presentation/Location/MainViewController.swift b/Atcha-iOS/Presentation/Location/MainViewController.swift index 87f9f380..d9985e75 100644 --- a/Atcha-iOS/Presentation/Location/MainViewController.swift +++ b/Atcha-iOS/Presentation/Location/MainViewController.swift @@ -206,11 +206,11 @@ final class MainViewController: BaseViewController, let isGuest = UserDefaultsWrapper.shared.bool( forKey: UserDefaultsWrapper.Key.isGuest.rawValue ) ?? false - + if isGuest { - amp_track(.main_view, props: props(AmplitudeProperty.userStatus(.guest))) + amp_track(.main_view, properties: props(AmplitudeProperty.userStatus(.guest))) } else { - amp_track(.main_view, props: props(AmplitudeProperty.userStatus(.member))) + amp_track(.main_view, properties: props(AmplitudeProperty.userStatus(.member))) } } @@ -402,12 +402,14 @@ extension MainViewController { case .homeChangeTapped: if viewModel.isGuest { presentLoginAlert() + amp_track(.login_view, properties: props(AmplitudeProperty.entryPoint(.home_modify))) } else { viewModel.handleRoute(route: .changeHome) } case .currentTapped: if viewModel.isGuest { presentLoginAlert() + amp_track(.login_view, properties: props(AmplitudeProperty.entryPoint(.departure))) } else { viewModel.handleRoute(route: .changeCourse( location: Location(name: "", lat: 0.0, lon: 0.0, businessCategory: "", address: "", radius: ""))) @@ -415,6 +417,7 @@ extension MainViewController { case .searchTapped: if viewModel.isGuest { presentLoginAlert() + amp_track(.login_view, properties: props(AmplitudeProperty.entryPoint(.course_search))) } else { guard let startCoord = mapContainerView.tMapWrapper.mapView.getCenter() else { view.showToast(message: "현재 위치를 확인 중이에요. 잠시 후 다시 시도해 주세요.") @@ -1027,6 +1030,8 @@ extension MainViewController { @objc private func didTapMyPageButton() { if viewModel.isGuest { presentLoginAlert() + + amp_track(.login_view, properties: props(AmplitudeProperty.entryPoint(.mypage))) } else { viewModel.handleRoute(route: .myPage) amp_track(.mypage_click) @@ -1454,6 +1459,8 @@ extension MainViewController { self.viewModel.alarmDelete() self.exitButtonTapped() + amp_track(.alarm_arrive_stop) + // 3. 안내용 팝업 띄우기 (화면 이동이 끝난 0.3초 뒤에 띄워서 자연스럽게) DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { self.showArrivalPopup() diff --git a/Atcha-iOS/Presentation/Location/MainViewModel.swift b/Atcha-iOS/Presentation/Location/MainViewModel.swift index 1c4926c8..6dea51d0 100644 --- a/Atcha-iOS/Presentation/Location/MainViewModel.swift +++ b/Atcha-iOS/Presentation/Location/MainViewModel.swift @@ -372,7 +372,7 @@ final class MainViewModel: BaseViewModel{ let _ = try await alarmUseCase.alarmDelete(request) wrapper.remove(forKey: UserDefaultsWrapper.Key.lastRouteId.rawValue) print("알람 취소 성공") - AmplitudeManager.shared.track(.alarm_cancel) + } catch { print("알람 취소 실패: \(error)") } diff --git a/Atcha-iOS/Presentation/Location/SearchLocation/SearchLocationViewController.swift b/Atcha-iOS/Presentation/Location/SearchLocation/SearchLocationViewController.swift index 815bf814..097e67e3 100644 --- a/Atcha-iOS/Presentation/Location/SearchLocation/SearchLocationViewController.swift +++ b/Atcha-iOS/Presentation/Location/SearchLocation/SearchLocationViewController.swift @@ -31,7 +31,7 @@ final class SearchLocationViewController: BaseViewController { } override func viewDidAppear(_ animated: Bool) { - AmplitudeManager.shared.trackScreen(.alarm) + amp_track(.alarm_view) } // MARK: - ViewModel 바인딩 @@ -154,7 +154,7 @@ final class LockViewController: BaseViewController { forKey: UserDefaultsWrapper.Key.departureAlarmDidFire.rawValue ) - AmplitudeManager.shared.track(.start_click) + amp_track(.start_click) } @objc private func detailRouteTapped() { @@ -166,7 +166,7 @@ final class LockViewController: BaseViewController { let lon = wrapper.string(forKey: UserDefaultsWrapper.Key.startLon.rawValue) ?? "" let address = wrapper.string(forKey: UserDefaultsWrapper.Key.startAddress.rawValue) ?? "" - AmplitudeManager.shared.track(.later_course_click) + amp_track(.later_course_click) viewModel.routerHandler?(.courseSearch(startLat: lat, startLon: lon, startAddress: address)) } diff --git a/Atcha-iOS/Presentation/Login/LoginViewController.swift b/Atcha-iOS/Presentation/Login/LoginViewController.swift index 5e0be280..a4c63712 100644 --- a/Atcha-iOS/Presentation/Login/LoginViewController.swift +++ b/Atcha-iOS/Presentation/Login/LoginViewController.swift @@ -175,7 +175,7 @@ extension LoginViewController { print("카카오 로그인 버튼 터치됨") viewModel.kakaoLoginTapped() - amp_track(.login_click, props: props(AmplitudeProperty.social(.kakao))) + amp_track(.login_click, properties: props(AmplitudeProperty.social(.kakao))) } @objc private func didTapAppleLoginButton() { @@ -186,7 +186,7 @@ extension LoginViewController { self?.appleLoginDelegateWrapper = delegate } - amp_track(.login_click, props: props(AmplitudeProperty.social(.apple))) + amp_track(.login_click, properties: props(AmplitudeProperty.social(.apple))) } } diff --git a/Atcha-iOS/Presentation/Onboarding/HomeRegister/HomeRegisterViewController.swift b/Atcha-iOS/Presentation/Onboarding/HomeRegister/HomeRegisterViewController.swift index 0c237b52..896f731e 100644 --- a/Atcha-iOS/Presentation/Onboarding/HomeRegister/HomeRegisterViewController.swift +++ b/Atcha-iOS/Presentation/Onboarding/HomeRegister/HomeRegisterViewController.swift @@ -37,11 +37,11 @@ final class HomeRegisterViewController: BaseViewController { if let selectedOption = self.selectedOption { AlarmManager.shared.setAlarmOption(selectedOption) - AmplitudeManager.shared.track( - .alarm_alert_type_setting, + amp_track( + .alarm_alert_type_setting, properties: props( AmplitudeProperty.alertType(self.mapAlertType(selectedOption)) ) @@ -52,12 +52,6 @@ final class PushAlarmViewController: BaseViewController { } } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - AmplitudeManager.shared.trackScreen(.alarm_setting) - - } override func viewDidLoad() { super.viewDidLoad() diff --git a/Atcha-iOS/Presentation/Setting/PushAlarm/PushAlarmViewModel.swift b/Atcha-iOS/Presentation/Setting/PushAlarm/PushAlarmViewModel.swift index 80915394..7644eea7 100644 --- a/Atcha-iOS/Presentation/Setting/PushAlarm/PushAlarmViewModel.swift +++ b/Atcha-iOS/Presentation/Setting/PushAlarm/PushAlarmViewModel.swift @@ -72,13 +72,6 @@ final class PushAlarmViewModel: BaseViewModel { UserDefaultsWrapper.shared.set(false, forKey: UserDefaultsWrapper.Key.reVisit .rawValue) - let dwellSeconds = AmplitudeManager.shared.timerEndSeconds("signup_dwell") - AmplitudeManager.shared.track( - .signup, - props( - AmplitudeProperty.dwellTime(seconds: dwellSeconds) - ) - ) print("회원가입 lat/lon 저장 완료: \(lat), \(lon)") } else { print("회원가입 응답에 lat/lon 없음") diff --git a/Atcha-iOS/Presentation/Setting/PushAlarmSheet/PushAlarmSheetViewController.swift b/Atcha-iOS/Presentation/Setting/PushAlarmSheet/PushAlarmSheetViewController.swift index 6edad519..92d09f0e 100644 --- a/Atcha-iOS/Presentation/Setting/PushAlarmSheet/PushAlarmSheetViewController.swift +++ b/Atcha-iOS/Presentation/Setting/PushAlarmSheet/PushAlarmSheetViewController.swift @@ -236,10 +236,26 @@ extension PushAlarmSheetViewController { AlarmManager.shared.setAlarmOption(option) AlarmManager.shared.setAlarmArmed(true) isConfirmed = true + + amp_track( + .alarm_alert_type_setting, properties: + props( + AmplitudeProperty.alertType(self.mapAlertType(option)) + ) + ) } + dismissSheet() } + private func mapAlertType(_ option: PushAlarmOption) -> AlertType { + switch option { + case .onlyVibration: return .onlyVibration + case .onlySound: return .onlySound + case .both: return .soundAndVibration + } + } + @objc private func sliderChanged(_ sender: UISlider) { let clampedValue = max(sender.value, 0.1) sender.setValue(clampedValue, animated: false) diff --git a/Atcha-iOS/Presentation/User/Home/HomeFindViewController.swift b/Atcha-iOS/Presentation/User/Home/HomeFindViewController.swift index 887b8b7a..542f9f9c 100644 --- a/Atcha-iOS/Presentation/User/Home/HomeFindViewController.swift +++ b/Atcha-iOS/Presentation/User/Home/HomeFindViewController.swift @@ -16,7 +16,7 @@ final class HomeFindViewController: BaseViewController, private let bottomView: HomeRegisterBottomView = HomeRegisterBottomView() private let flagImageView: UIImageView = UIImageView() private let backButton: UIButton = UIButton() - private let loactionButton: UIButton = UIButton() + private let locationButton: UIButton = UIButton() private let exitButton: UIButton = UIButton() var routeHandler: ((HomeRouter) -> Void)? @@ -47,7 +47,7 @@ final class HomeFindViewController: BaseViewController, view.addSubViews(mapContainerView, flagImageView, bottomView, - loactionButton, + locationButton, backButton) mapContainerView.delegate = self @@ -58,7 +58,7 @@ final class HomeFindViewController: BaseViewController, backButton.clipsToBounds = true backButton.setCornerRadius(18) - configureButton(loactionButton, + configureButton(locationButton, imageName: "mylocation-filled", action: #selector(didTapLocationButton)) } @@ -145,7 +145,7 @@ final class HomeFindViewController: BaseViewController, make.width.equalTo(48) } - loactionButton.snp.makeConstraints { make in + locationButton.snp.makeConstraints { make in make.bottom.equalTo(bottomView.snp.top).inset(-16) make.trailing.equalToSuperview().inset(16) make.width.height.equalTo(40) @@ -205,6 +205,8 @@ extension HomeFindViewController { @objc private func didTapLocationButton() { ensureLocationPermissionOrShowToast() viewModel.setupLocation() + + amp_track(.current_location_click) } } diff --git a/Atcha-iOS/Presentation/User/Home/HomeFindViewModel.swift b/Atcha-iOS/Presentation/User/Home/HomeFindViewModel.swift index eb5fdda9..30fc5c18 100644 --- a/Atcha-iOS/Presentation/User/Home/HomeFindViewModel.swift +++ b/Atcha-iOS/Presentation/User/Home/HomeFindViewModel.swift @@ -73,8 +73,6 @@ final class HomeFindViewModel: BaseViewModel { } func handleRegister() { - AmplitudeManager.shared.track(.home_register) - switch context { case .onboarding: saveCurrentLoaction() @@ -270,12 +268,8 @@ extension HomeFindViewModel { UserDefaultsWrapper.shared.set(false, forKey: UserDefaultsWrapper.Key.reVisit .rawValue) - let dwellSeconds = AmplitudeManager.shared.timerEndSeconds("signup_dwell") AmplitudeManager.shared.track( - .signup, - props( - AmplitudeProperty.dwellTime(seconds: dwellSeconds) - ) + .signup ) print("회원가입 lat/lon 저장 완료: \(lat), \(lon)") UserDefaultsWrapper.shared.set(false, forKey: UserDefaultsWrapper.Key.isGuest.rawValue) diff --git a/Atcha-iOS/Presentation/User/MyAccount/MyAccountViewController.swift b/Atcha-iOS/Presentation/User/MyAccount/MyAccountViewController.swift index ffe4142d..31bd3bbc 100644 --- a/Atcha-iOS/Presentation/User/MyAccount/MyAccountViewController.swift +++ b/Atcha-iOS/Presentation/User/MyAccount/MyAccountViewController.swift @@ -36,10 +36,6 @@ final class MyAccountViewController: BaseViewController { setupAutoLayout() } - override func viewDidAppear(_ animated: Bool) { - AmplitudeManager.shared.trackScreen(.account) - } - private func setupUI() { view.addSubViews(navigationBar, collectionView) diff --git a/Atcha-iOS/Presentation/User/MyPage/MyPageViewController.swift b/Atcha-iOS/Presentation/User/MyPage/MyPageViewController.swift index 34e13aae..1722357c 100644 --- a/Atcha-iOS/Presentation/User/MyPage/MyPageViewController.swift +++ b/Atcha-iOS/Presentation/User/MyPage/MyPageViewController.swift @@ -38,7 +38,7 @@ final class MyPageViewController: BaseViewController { } override func viewDidAppear(_ animated: Bool) { - AmplitudeManager.shared.trackScreen(.mypage) + amp_track(.mypage_view) } private func setupUI() { @@ -93,6 +93,8 @@ final class MyPageViewController: BaseViewController { @objc func bannerTapped() { viewModel.bannerTapped() + + amp_track(.feedback) } } @@ -134,6 +136,7 @@ extension MyPageViewController: UICollectionViewDelegate, viewModel.navigationTarget.send(.notification) case .term: viewModel.navigationTarget.send(.term) + amp_track(.term) case .version: viewModel.navigationTarget.send(.versionUpdate) } diff --git a/Atcha-iOS/Presentation/User/Withdraw/WithdrawViewController.swift b/Atcha-iOS/Presentation/User/Withdraw/WithdrawViewController.swift index 187c711f..ac6af2ba 100644 --- a/Atcha-iOS/Presentation/User/Withdraw/WithdrawViewController.swift +++ b/Atcha-iOS/Presentation/User/Withdraw/WithdrawViewController.swift @@ -31,8 +31,6 @@ class WithdrawViewController: BaseViewController { if #available(iOS 16.0, *) { view.keyboardLayoutGuide.followsUndockedKeyboard = true } - - AmplitudeManager.shared.trackScreen(.withdraw) } override func viewDidLoad() { diff --git a/Atcha-iOS/Presentation/WebView/WebViewController.swift b/Atcha-iOS/Presentation/WebView/WebViewController.swift index dc389532..f3de0606 100644 --- a/Atcha-iOS/Presentation/WebView/WebViewController.swift +++ b/Atcha-iOS/Presentation/WebView/WebViewController.swift @@ -54,7 +54,7 @@ final class WebViewController: BaseViewController { override func viewDidAppear(_ animated: Bool) { if type == .term { - AmplitudeManager.shared.trackScreen(.terms) + amp_track(.term) } } From fa98ccf311f85a63355c056841e42b009054d1ea Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Sat, 14 Mar 2026 04:28:24 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[FEAT]=20=ED=86=A0=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=82=AD=EC=A0=9C=20=EC=A0=9C?= =?UTF-8?q?=EC=B8=A0=EC=B2=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DesignSource/AtchaToast/AtchaToast.swift | 100 ++++++++++++++---- .../Location/MainViewController.swift | 11 +- 2 files changed, 86 insertions(+), 25 deletions(-) diff --git a/Atcha-iOS/DesignSource/AtchaToast/AtchaToast.swift b/Atcha-iOS/DesignSource/AtchaToast/AtchaToast.swift index 7b671e76..7e08b8f7 100644 --- a/Atcha-iOS/DesignSource/AtchaToast/AtchaToast.swift +++ b/Atcha-iOS/DesignSource/AtchaToast/AtchaToast.swift @@ -17,6 +17,7 @@ final class AtchaToast: UIView { setupLabel(message: message) setupView() setupAutoLayout() + setupGesture() } required init?(coder: NSCoder) { @@ -35,6 +36,7 @@ final class AtchaToast: UIView { backgroundColor = UIColor.gray930 layer.cornerRadius = 12 clipsToBounds = true + self.isUserInteractionEnabled = true } private func setupAutoLayout() { @@ -42,6 +44,55 @@ final class AtchaToast: UIView { make.edges.equalToSuperview().inset(UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)) } } + + private func setupGesture() { + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) + self.addGestureRecognizer(panGesture) + } + + @objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) { + let translation = gesture.translation(in: self.superview) + let velocity = gesture.velocity(in: self.superview) + + switch gesture.state { + case .began: + // 중요: 자동 사라짐 예약 취소! + NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(autoHide), object: nil) + self.layer.removeAllAnimations() + + case .changed: + if translation.y < 0 { + self.transform = CGAffineTransform(translationX: 0, y: translation.y) + } + + case .ended: + if translation.y < -30 || velocity.y < -500 { + dismissWithAnimation() + } else { + UIView.animate(withDuration: 0.3, delay: 0, options: [.allowUserInteraction], animations: { + self.transform = .identity + }) { _ in + // 다시 제자리로 왔으니 자동 사라짐 재예약 (선택 사항) + self.perform(#selector(self.autoHide), with: nil, afterDelay: 2.0) + } + } + default: break + } + } + + // 공통 삭제 애니메이션 + // dismissWithAnimation에서도 안전하게 한 번 더 취소해주는 게 좋습니다. + private func dismissWithAnimation() { + NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(autoHide), object: nil) + self.isUserInteractionEnabled = false + + UIView.animate(withDuration: 0.3, animations: { + self.alpha = 0 + self.transform = CGAffineTransform(translationX: 0, y: -100) + }) { _ in + self.removeFromSuperview() + } + } } extension AtchaToast { @@ -50,7 +101,6 @@ extension AtchaToast { topOffset: CGFloat = 10) { guard self.superview == nil else { return } - parentView.addSubview(self) snp.makeConstraints { make in @@ -61,35 +111,43 @@ extension AtchaToast { parentView.layoutIfNeeded() + // 초기 상태 alpha = 0.0 transform = CGAffineTransform(translationX: 0, y: -10) + // 1. 등장 애니메이션 (사용자 터치 허용 옵션 추가) UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, - options: [.curveEaseOut], - animations: { [weak self] in - guard let self else { return } - alpha = 1.0 - transform = .identity - }, completion: { _ in - UIView.animate(withDuration: 0.8, - delay: duration, - options: [.curveEaseIn], - animations: { [weak self] in - guard let self else { return } - alpha = 1.0 - transform = CGAffineTransform(translationX: 0, y: -parentView.bounds.height) - }, completion: { [weak self] _ in - guard let self else { return } - removeFromSuperview() - }) - }) + options: [.beginFromCurrentState, .allowUserInteraction], // 터치 허용! + animations: { + self.alpha = 1.0 + self.transform = .identity + }) { _ in + // 2. 일정 시간 뒤에 자동으로 사라지게 함 (애니메이션 내부 delay 대신 사용) + // 이렇게 해야 대기 시간 동안 제스처가 먹습니다. + self.perform(#selector(self.autoHide), with: nil, afterDelay: duration) + } + } + + @objc private func autoHide() { + dismissWithAnimation() } + // AtchaToast.swift 내부 수정 + func hideImmediately() { - layer.removeAllAnimations() - removeFromSuperview() + // 1. 예약된 autoHide 타이머를 즉시 취소 (가장 중요) + NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(autoHide), object: nil) + + // 2. 현재 실행 중인 모든 레이어 애니메이션 중단 + self.layer.removeAllAnimations() + + // 3. 부모 뷰에서 즉시 제거 + self.removeFromSuperview() + + // 4. 터치 상태 초기화 + self.isUserInteractionEnabled = false } } diff --git a/Atcha-iOS/Presentation/Location/MainViewController.swift b/Atcha-iOS/Presentation/Location/MainViewController.swift index d9985e75..e0dc262f 100644 --- a/Atcha-iOS/Presentation/Location/MainViewController.swift +++ b/Atcha-iOS/Presentation/Location/MainViewController.swift @@ -108,6 +108,7 @@ final class MainViewController: BaseViewController, // } var shouldShowWelcomeToast: Bool = false + private var hasShownAlarmRegisteredToast = false // MARK: - Life Cycle @@ -204,9 +205,9 @@ final class MainViewController: BaseViewController, self.viewModel.refreshCurrentMapCenterData() let isGuest = UserDefaultsWrapper.shared.bool( - forKey: UserDefaultsWrapper.Key.isGuest.rawValue - ) ?? false - + forKey: UserDefaultsWrapper.Key.isGuest.rawValue + ) ?? false + if isGuest { amp_track(.main_view, properties: props(AmplitudeProperty.userStatus(.guest))) } else { @@ -520,6 +521,7 @@ extension MainViewController { // 4. 알람 해제 시 1번(초기 상태)으로 돌아감 isFollowingUser = false shouldCenterToCurrentLocationOnce = true + hasShownAlarmRegisteredToast = false // 이번 한 번은 프리 말풍선 자동 표시를 건너뛰도록 플래그 세팅 deferPreBalloonOnce = true @@ -787,9 +789,10 @@ extension MainViewController { guard gen == self.setupGen, self.viewModel.bottomType == .departure else { return } guard let first = self.postAlarmMessages.first else { return } - if !wasAlarmRegisteredOnLaunch { + if !wasAlarmRegisteredOnLaunch && !hasShownAlarmRegisteredToast { DispatchQueue.main.asyncAfter(deadline: .now() + popToastDelay) { self.view.showToast(message: "알람이 등록되었습니다.") + self.hasShownAlarmRegisteredToast = true // 띄웠다고 표시! UserDefaultsWrapper.shared.set(false, forKey: UserDefaultsWrapper.Key.popRegister.rawValue) } }