diff --git a/EATSSU/App/Sources/Data/Firebase/HomeAnalyticsManager.swift b/EATSSU/App/Sources/Data/Firebase/HomeAnalyticsManager.swift index f5d76ebb..34cb084c 100644 --- a/EATSSU/App/Sources/Data/Firebase/HomeAnalyticsManager.swift +++ b/EATSSU/App/Sources/Data/Firebase/HomeAnalyticsManager.swift @@ -37,7 +37,8 @@ final class HomeAnalyticsManager { TextLiteral.Restaurant.studentRestaurant: "haksik", TextLiteral.Restaurant.dodamRestaurant: "dodam", TextLiteral.Restaurant.dormitoryRestaurant: "dormitory", - TextLiteral.Restaurant.facultyRestaurant: "faculty" + TextLiteral.Restaurant.facultyRestaurant: "faculty", + TextLiteral.Restaurant.snackCorner: "snack_corner" ] // 식사 유형(한글) -> 영문 소문자 파라미터로 변환 diff --git a/EATSSU/App/Sources/Data/Firebase/MyPageAnalyticsManager.swift b/EATSSU/App/Sources/Data/Firebase/MyPageAnalyticsManager.swift new file mode 100644 index 00000000..33759a55 --- /dev/null +++ b/EATSSU/App/Sources/Data/Firebase/MyPageAnalyticsManager.swift @@ -0,0 +1,60 @@ +// +// MyPageAnalyticsManager.swift +// EATSSU +// +// Created by 황상환 on 2026/05/23. +// + +import Foundation + +/// 마이페이지에서 발생하는 주요 이벤트를 Firebase Analytics에 로깅하는 담당자 +final class MyPageAnalyticsManager { + + // MARK: - Singleton + + static let shared = MyPageAnalyticsManager() + private init() {} + + // MARK: - Event & Parameter Keys + + private enum Event { + static let clickMyPageMenu = "click_mypage_menu" + } + + private enum Parameter { + static let menu = "menu" + static let college = "college" + static let major = "major" + } + + /// click_mypage_menu 이벤트의 menu 파라미터 값 + enum Menu: String { + case notificationSetting = "notification_setting" + case myInfo = "my_info" + case myReview = "my_review" + case inquiry = "inquiry" + case termsOfUse = "terms_of_use" + case privacyPolicy = "privacy_policy" + case creator = "creator" + case logout = "logout" + case withdraw = "withdraw" + } + + // MARK: - Logging Methods + + /// 마이페이지의 메뉴 항목을 클릭했을 때 호출 + /// - Parameter menu: 클릭한 메뉴 종류 + func logClickMyPageMenu(menu: Menu) { + var parameters: [String: Any] = [Parameter.menu: menu.rawValue] + + let userInfo = UserInfoManager.shared.getCurrentUserInfo() + if let collegeId = userInfo?.collegeId { + parameters[Parameter.college] = collegeId + } + if let departmentId = userInfo?.departmentId { + parameters[Parameter.major] = departmentId + } + + AnalyticsService.logEvent(Event.clickMyPageMenu, parameters: parameters) + } +} diff --git a/EATSSU/App/Sources/Data/Firebase/ReviewAnalyticsManager.swift b/EATSSU/App/Sources/Data/Firebase/ReviewAnalyticsManager.swift index 03ab8635..b9e09b58 100644 --- a/EATSSU/App/Sources/Data/Firebase/ReviewAnalyticsManager.swift +++ b/EATSSU/App/Sources/Data/Firebase/ReviewAnalyticsManager.swift @@ -21,31 +21,53 @@ final class ReviewAnalyticsManager { } private enum Parameter { + static let restaurants = "restaurants" static let photoAttached = "photo_attached" static let rating = "rating" static let likes = "likes" } + // 식당 이름(한글) -> 영문 소문자 파라미터로 변환 + private let restaurantNameMap: [String: String] = [ + TextLiteral.Restaurant.studentRestaurant: "haksik", + TextLiteral.Restaurant.dodamRestaurant: "dodam", + TextLiteral.Restaurant.dormitoryRestaurant: "dormitory", + TextLiteral.Restaurant.facultyRestaurant: "faculty", + TextLiteral.Restaurant.snackCorner: "snack_corner" + ] + // MARK: - Logging Methods /** #1 '리뷰 작성하기' 버튼을 클릭했을 때 호출 + - Parameter restaurantName: 리뷰를 작성할 메뉴가 속한 식당 이름 (예: "학생 식당") */ - func logWriteReviewV1() { - AnalyticsService.logEvent(Event.writeReview) + func logWriteReviewV2(restaurantName: String?) { + AnalyticsService.logEvent(Event.writeReview, parameters: makeRestaurantsParameter(restaurantName)) } /** #2 리뷰 작성을 마치고 '완료하기' 버튼을 클릭했을 때 호출 + - Parameter restaurantName: 리뷰를 작성한 메뉴가 속한 식당 이름 (예: "학생 식당") - Parameter photoAttached: 사진 첨부 여부 (0: 없음, 1: 있음) - Parameter rating: 사용자가 부여한 메인 별점 (1~5) - - Parameter selection: 사용자가 한 번에 리뷰를 작성하는 메뉴의 총 개수 + - Parameter likes: 사용자가 한 번에 리뷰를 작성하는 메뉴의 총 개수 */ - func logCompleteReviewV1(photoAttached: Int, rating: Int, likes: Int) { - AnalyticsService.logEvent(Event.completeReview, parameters: [ - Parameter.photoAttached: photoAttached, - Parameter.rating: rating, - Parameter.likes: likes - ]) + func logCompleteReviewV2(restaurantName: String?, photoAttached: Int, rating: Int, likes: Int) { + var parameters: [String: Any] = makeRestaurantsParameter(restaurantName) ?? [:] + parameters[Parameter.photoAttached] = photoAttached + parameters[Parameter.rating] = rating + parameters[Parameter.likes] = likes + AnalyticsService.logEvent(Event.completeReview, parameters: parameters) + } + + // MARK: - Helpers + + private func makeRestaurantsParameter(_ restaurantName: String?) -> [String: Any]? { + guard let restaurantName, + let value = restaurantNameMap[restaurantName] else { + return nil + } + return [Parameter.restaurants: value] } } diff --git a/EATSSU/App/Sources/Data/Firebase/WidgetAnalyticsManager.swift b/EATSSU/App/Sources/Data/Firebase/WidgetAnalyticsManager.swift index 793c01e6..1cbe1b6d 100644 --- a/EATSSU/App/Sources/Data/Firebase/WidgetAnalyticsManager.swift +++ b/EATSSU/App/Sources/Data/Firebase/WidgetAnalyticsManager.swift @@ -53,6 +53,7 @@ final class WidgetAnalyticsManager { "도담식당": "dodam", "기숙사 식당": "dormitory", "FACULTY (교직원 전용)": "faculty", + "스낵 코너": "snack_corner", ] private init() {} diff --git a/EATSSU/App/Sources/Presentation/Home/Model/ReviewMenuTypeInfo.swift b/EATSSU/App/Sources/Presentation/Home/Model/ReviewMenuTypeInfo.swift index d700c4dd..e594977a 100644 --- a/EATSSU/App/Sources/Presentation/Home/Model/ReviewMenuTypeInfo.swift +++ b/EATSSU/App/Sources/Presentation/Home/Model/ReviewMenuTypeInfo.swift @@ -11,4 +11,5 @@ struct ReviewMenuTypeInfo { var menuType: String var menuID: Int var changeMenuIDList: [Int]? + var restaurantName: String? } diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift index 238e16ad..3263077a 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift @@ -288,6 +288,10 @@ extension HomeRestaurantViewController: UITableViewDataSource { var reviewMenuTypeInfo = ReviewMenuTypeInfo(menuType: "", menuID: 0) + if section < sectionHeaderRestaurant.count { + reviewMenuTypeInfo.restaurantName = sectionHeaderRestaurant[section] + } + if !isSnackCorner { guard let menus = changeMenuTableViewData[restaurant], menuIndex < menus.count else { diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift index 7217334b..53f78889 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift @@ -79,7 +79,7 @@ final class MyPageViewController: BaseViewController { @objc private func userWithdrawButtonTapped() { - AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "withdraw"]) + MyPageAnalyticsManager.shared.logClickMyPageMenu(menu: .withdraw) let userWithdrawViewController = UserWithdrawViewController(nickName: nickName) navigationController?.pushViewController(userWithdrawViewController, animated: true) } @@ -189,25 +189,25 @@ extension MyPageViewController: UITableViewDelegate { switch indexPath.row { // "푸시 알림 설정" 스위치 토글 case MyPageLabels.NotificationSetting.rawValue: - AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "notification_setting"]) + MyPageAnalyticsManager.shared.logClickMyPageMenu(menu: .notificationSetting) handleNotificationSettingToggle(at: indexPath) // "내 정보" 스크린으로 이동 case MyPageLabels.MyInfo.rawValue: - AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "my_info"]) + MyPageAnalyticsManager.shared.logClickMyPageMenu(menu: .myInfo) let setNickNameVC = SetNickNameViewController() setNickNameVC.source = .signup navigationController?.pushViewController(setNickNameVC, animated: true) // "내 리뷰" 스크린으로 이동 case MyPageLabels.MyReview.rawValue: - AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "my_review"]) + MyPageAnalyticsManager.shared.logClickMyPageMenu(menu: .myReview) let myReviewViewController = MyReviewViewController(nickname: nickName) navigationController?.pushViewController(myReviewViewController, animated: true) // "문의하기" 스크린으로 이동 case MyPageLabels.Inquiry.rawValue: - AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "inquiry"]) + MyPageAnalyticsManager.shared.logClickMyPageMenu(menu: .inquiry) TalkApi.shared.chatChannel(channelPublicId: TextLiteral.KakaoChannel.id) { [weak self] error in if error != nil { if let kakaoChannelLink = URL(string: "http://pf.kakao.com/\(TextLiteral.KakaoChannel.id)") { @@ -226,21 +226,21 @@ extension MyPageViewController: UITableViewDelegate { // "서비스 이용약관" 스크린으로 이동 case MyPageLabels.TermsOfUse.rawValue: - AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "terms_of_use"]) + MyPageAnalyticsManager.shared.logClickMyPageMenu(menu: .termsOfUse) let provisionViewController = ProvisionViewController(agreementType: .termsOfService) provisionViewController.navigationTitle = TextLiteral.MyPage.termsOfUse navigationController?.pushViewController(provisionViewController, animated: true) // "개인정보 이용약관" 스크린으로 이동 case MyPageLabels.PrivacyTermsOfUse.rawValue: - AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "privacy_policy"]) + MyPageAnalyticsManager.shared.logClickMyPageMenu(menu: .privacyPolicy) let provisionViewController = ProvisionViewController(agreementType: .privacyPolicy) provisionViewController.navigationTitle = TextLiteral.MyPage.privacyTermsOfUse navigationController?.pushViewController(provisionViewController, animated: true) // "만든사람들" 노션 페이지로 이동 (앱 내 웹뷰) case MyPageLabels.Creator.rawValue: - AnalyticsService.logEvent("click_mypage_menu", parameters: ["menu": "creator"]) + MyPageAnalyticsManager.shared.logClickMyPageMenu(menu: .creator) if let creatorsURL = URL(string: URLConstants.creatorsNotion) { let creatorsWebVC = ProvisionViewController(url: creatorsURL) creatorsWebVC.navigationTitle = TextLiteral.MyPage.creators @@ -249,6 +249,7 @@ extension MyPageViewController: UITableViewDelegate { // "로그아웃" 팝업알림 표시 case MyPageLabels.Logout.rawValue: + MyPageAnalyticsManager.shared.logClickMyPageMenu(menu: .logout) logoutShowAlert() default: diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index b8caff6f..68bcd243 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -28,9 +28,12 @@ final class ReviewViewController: BaseViewController { /// 메뉴 ID (FIXED 타입) 또는 식사 ID (VARIABLE 타입) var menuID: Int = 0 - + /// 메뉴 타입 ("FIXED" 또는 "VARIABLE") var type = "VARIABLE" + + /// 리뷰 작성 대상 메뉴가 속한 식당 이름 (예: "학생 식당") + private var restaurantName: String? /// 메뉴 이름 리스트 private var menuNameList: [String] = [] @@ -201,18 +204,20 @@ final class ReviewViewController: BaseViewController { /// 리뷰 작성 버튼 탭 처리 @objc private func handleAddReviewButtonTap() { - ReviewAnalyticsManager.shared.logWriteReviewV1() + ReviewAnalyticsManager.shared.logWriteReviewV2(restaurantName: restaurantName) if type == "VARIABLE" { let reviewVC = SetRateViewController(mealId: menuID) + reviewVC.restaurantName = restaurantName reviewVC.dataBind( list: validMenusForReview.map { $0.name }, idList: validMenusForReview.map { $0.menuId } ) navigationController?.pushViewController(reviewVC, animated: true) - + } else { let reviewVC = SetRateViewController(menuId: menuID) + reviewVC.restaurantName = restaurantName reviewVC.dataBind( list: menuNameList, idList: menuIDList ?? [] @@ -294,10 +299,12 @@ final class ReviewViewController: BaseViewController { /// 리뷰 작성 버튼 탭 처리 (로그인 체크 포함) func userTapReviewButton() { if RealmService.shared.isAccessTokenPresent() { + ReviewAnalyticsManager.shared.logWriteReviewV2(restaurantName: restaurantName) DispatchQueue.global().async { DispatchQueue.main.async { [self] in if type == "FIXED" { let setRateViewController = SetRateViewController(menuId: menuID) + setRateViewController.restaurantName = restaurantName setRateViewController.dataBind( list: menuNameList, idList: menuIDList ?? [] @@ -308,6 +315,7 @@ final class ReviewViewController: BaseViewController { ) } else { let setRateViewController = SetRateViewController(mealId: menuID) + setRateViewController.restaurantName = restaurantName setRateViewController.dataBind( list: validMenusForReview.map { $0.name }, idList: validMenusForReview.map { $0.menuId } @@ -715,14 +723,9 @@ extension ReviewViewController: ReviewMenuTypeInfoDelegate { /// 메뉴 타입 정보 델리게이트 func didDelegateReviewMenuTypeInfo(for menuTypeData: ReviewMenuTypeInfo) { - let reviewMenuTypeInfo = ReviewMenuTypeInfo( - menuType: menuTypeData.menuType, - menuID: menuTypeData.menuID, - changeMenuIDList: menuTypeData.changeMenuIDList - ) - - type = reviewMenuTypeInfo.menuType - menuID = reviewMenuTypeInfo.menuID - menuIDList = reviewMenuTypeInfo.changeMenuIDList + type = menuTypeData.menuType + menuID = menuTypeData.menuID + menuIDList = menuTypeData.changeMenuIDList + restaurantName = menuTypeData.restaurantName } } diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 306d8325..ba09c54b 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -27,6 +27,9 @@ final class SetRateViewController: BaseViewController, UINavigationControllerDel private var userPickedImage: UIImage? private var isReviewSubmitted = false + + /// 리뷰를 작성 중인 메뉴가 속한 식당 이름 (예: "학생 식당") + var restaurantName: String? enum ReviewType { @@ -425,7 +428,8 @@ extension SetRateViewController { await MainActor.run { self.isReviewSubmitted = true let hasPhoto = self.userPickedImage != nil || self.setRateView.userReviewImageView.image != nil - ReviewAnalyticsManager.shared.logCompleteReviewV1( + ReviewAnalyticsManager.shared.logCompleteReviewV2( + restaurantName: self.restaurantName, photoAttached: hasPhoto ? 1 : 0, rating: self.setRateView.rateView.currentStar, likes: self.likedStates.filter { $0 }.count @@ -476,7 +480,8 @@ extension SetRateViewController { await MainActor.run { self.isReviewSubmitted = true - ReviewAnalyticsManager.shared.logCompleteReviewV1( + ReviewAnalyticsManager.shared.logCompleteReviewV2( + restaurantName: self.restaurantName, photoAttached: imageUrl != nil ? 1 : 0, rating: self.setRateView.rateView.currentStar, likes: self.likedStates.filter { $0 }.count @@ -528,7 +533,8 @@ extension SetRateViewController { await MainActor.run { self.isReviewSubmitted = true - ReviewAnalyticsManager.shared.logCompleteReviewV1( + ReviewAnalyticsManager.shared.logCompleteReviewV2( + restaurantName: self.restaurantName, photoAttached: imageUrl != nil ? 1 : 0, rating: self.setRateView.rateView.currentStar, likes: self.likedStates.filter { $0 }.count diff --git a/EATSSU/App/Sources/Utility/Application/AppDelegate.swift b/EATSSU/App/Sources/Utility/Application/AppDelegate.swift index f9bb17bc..5bb7758a 100644 --- a/EATSSU/App/Sources/Utility/Application/AppDelegate.swift +++ b/EATSSU/App/Sources/Utility/Application/AppDelegate.swift @@ -97,7 +97,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let config = PostHogConfig(apiKey: apiKey, host: "https://us.i.posthog.com") config.captureApplicationLifecycleEvents = true - config.captureScreenViews = true + // 수동 .screen() 호출(AnalyticsService.logScreen)과 중복되므로 자동수집은 비활성화 + config.captureScreenViews = false PostHogSDK.shared.setup(config) #endif