From 8f082dad20f1a25c3c4e82230fe1cc132a1d9820 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sat, 30 May 2026 15:37:34 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[#419]=20=EC=8B=9D=EB=8B=B9=20=EB=A7=A4?= =?UTF-8?q?=ED=95=91=EC=97=90=20snack=5Fcorner=20=EA=B0=92=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EATSSU/App/Sources/Data/Firebase/HomeAnalyticsManager.swift | 3 ++- EATSSU/App/Sources/Data/Firebase/WidgetAnalyticsManager.swift | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) 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/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() {} From 0697bda6ea2d850711669e42ce647274962fc5c2 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sat, 30 May 2026 15:37:38 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[#419]=20write/complete=5Freview=5Fv2=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=EC=97=90=20restaurants=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Firebase/ReviewAnalyticsManager.swift | 40 ++++++++++++++----- .../Home/Model/ReviewMenuTypeInfo.swift | 1 + .../HomeRestaurantViewController.swift | 4 ++ .../ViewController/ReviewViewController.swift | 24 +++++------ .../SetRateViewController.swift | 12 ++++-- 5 files changed, 57 insertions(+), 24 deletions(-) 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/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/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index b8caff6f..d14a55fe 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 ?? [] @@ -715,14 +720,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 From 92cdad571cae689a4c34f5bdc314ec01d95d0df6 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sat, 30 May 2026 15:37:42 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[#419]=20click=5Fmypage=5Fmenu=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=EC=97=90=20college/major=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20+=20logout=20=EA=B0=92=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Firebase/MyPageAnalyticsManager.swift | 60 +++++++++++++++++++ .../ViewController/MyPageViewController.swift | 17 +++--- 2 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 EATSSU/App/Sources/Data/Firebase/MyPageAnalyticsManager.swift 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/Presentation/MyPage/ViewController/MyPageViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift index d93632d2..d8a11fdf 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift @@ -75,7 +75,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) } @@ -185,25 +185,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)") { @@ -222,26 +222,27 @@ 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) let creatorViewController = CreatorViewController() navigationController?.pushViewController(creatorViewController, animated: true) // "로그아웃" 팝업알림 표시 case MyPageLabels.Logout.rawValue: + MyPageAnalyticsManager.shared.logClickMyPageMenu(menu: .logout) logoutShowAlert() default: From ca4e94421eaa11cb19c183fccba2b8b74cab4133 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sat, 30 May 2026 15:37:45 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[#419]=20PostHog=20screen=5Fview=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=EC=88=98=EC=A7=91=20=EB=B9=84=ED=99=9C?= =?UTF-8?q?=EC=84=B1=ED=99=94=EB=A1=9C=20=EC=A4=91=EB=B3=B5=20=EB=B0=9C?= =?UTF-8?q?=EC=82=AC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EATSSU/App/Sources/Utility/Application/AppDelegate.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From b84c19814096f9cfe2ce72ad8f6b09c51edf043e Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sat, 30 May 2026 16:34:32 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[#419]=20userTapReviewButton=20=EC=A7=84?= =?UTF-8?q?=EC=9E=85=20=EA=B2=BD=EB=A1=9C=EC=97=90=EB=8F=84=20write=5Frevi?= =?UTF-8?q?ew=5Fv2=20=EB=B0=9C=EC=82=AC=20+=20restaurantName=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Review/ViewController/ReviewViewController.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index d14a55fe..68bcd243 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -299,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 ?? [] @@ -313,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 }