diff --git a/EATSSU/App/Resources/en.lproj/Localizable.strings b/EATSSU/App/Resources/en.lproj/Localizable.strings index 9e583b59..8224339e 100644 --- a/EATSSU/App/Resources/en.lproj/Localizable.strings +++ b/EATSSU/App/Resources/en.lproj/Localizable.strings @@ -167,6 +167,8 @@ "home.dodamEtc" = "Asian food, pork cutlet, salad, gukbap, etc.\nCafe"; /// RestaurantMenuGroupCell - "영업 시간이 아니에요." "home.notBusinessHour" = "Closed now"; +/// RestaurantMenuGroupCell - "메뉴를 불러오지 못했어요." +"home.menuLoadFailed" = "Couldn't load the menu"; /// RestaurantTableViewHeader - "기숙사 식당" "home.dormitoryRestaurant" = "Dormitory Restaurant"; diff --git a/EATSSU/App/Resources/ja.lproj/Localizable.strings b/EATSSU/App/Resources/ja.lproj/Localizable.strings index ac2189ef..916416f4 100644 --- a/EATSSU/App/Resources/ja.lproj/Localizable.strings +++ b/EATSSU/App/Resources/ja.lproj/Localizable.strings @@ -168,6 +168,8 @@ "home.dodamEtc" = "アジアンフード、トンカツ、サラダ、クッパなど\nカフェ"; /// RestaurantMenuGroupCell - "영업 시간이 아니에요." "home.notBusinessHour" = "営業時間外です。"; +/// RestaurantMenuGroupCell - "메뉴를 불러오지 못했어요." +"home.menuLoadFailed" = "メニューを読み込めませんでした。"; /// RestaurantTableViewHeader - "기숙사 식당" "home.dormitoryRestaurant" = "寮食堂"; diff --git a/EATSSU/App/Resources/ko.lproj/Localizable.strings b/EATSSU/App/Resources/ko.lproj/Localizable.strings index 2296203f..70abfcc4 100644 --- a/EATSSU/App/Resources/ko.lproj/Localizable.strings +++ b/EATSSU/App/Resources/ko.lproj/Localizable.strings @@ -168,6 +168,8 @@ "home.dodamEtc" = "아시안푸드, 돈까스, 샐러드, 국밥 등\n카페"; /// RestaurantMenuGroupCell - "영업 시간이 아니에요." "home.notBusinessHour" = "영업 시간이 아니에요."; +/// RestaurantMenuGroupCell - "메뉴를 불러오지 못했어요." +"home.menuLoadFailed" = "메뉴를 불러오지 못했어요."; /// RestaurantTableViewHeader - "기숙사 식당" "home.dormitoryRestaurant" = "기숙사 식당"; diff --git a/EATSSU/App/Resources/vi.lproj/Localizable.strings b/EATSSU/App/Resources/vi.lproj/Localizable.strings index 859b299d..ae70223c 100644 --- a/EATSSU/App/Resources/vi.lproj/Localizable.strings +++ b/EATSSU/App/Resources/vi.lproj/Localizable.strings @@ -168,6 +168,8 @@ "home.dodamEtc" = "Món Á, tonkatsu, salad, gukbap, v.v.\nQuán cà phê"; /// RestaurantMenuGroupCell - "영업 시간이 아니에요." "home.notBusinessHour" = "Hiện không trong giờ hoạt động."; +/// RestaurantMenuGroupCell - "메뉴를 불러오지 못했어요." +"home.menuLoadFailed" = "Không thể tải thực đơn."; /// RestaurantTableViewHeader - "기숙사 식당" "home.dormitoryRestaurant" = "Nhà ăn ký túc xá"; diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift index a9ad2c1f..a41e1f66 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift @@ -178,12 +178,11 @@ final class SetNickNameViewController: BaseViewController { myPageVC.showToast(message: TextLiteral.Auth.updateSuccess, type: .success) } } else { - let homeVC = HomeViewController() + // 로그인 완료와 같은 탭바 화면으로 이동 (홈만 올리면 지도·찜·마이 탭이 없다) + let customTabVC = CustomTabBarContainerController() if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) { - keyWindow.replaceRootViewController( - UINavigationController(rootViewController: homeVC) - ) + keyWindow.replaceRootViewController(customTabVC) } } } diff --git a/EATSSU/App/Sources/Presentation/Home/View/RestaurantTableView/RestaurantMenuGroupCell.swift b/EATSSU/App/Sources/Presentation/Home/View/RestaurantTableView/RestaurantMenuGroupCell.swift index 01a6a5e3..dac0f3ea 100644 --- a/EATSSU/App/Sources/Presentation/Home/View/RestaurantTableView/RestaurantMenuGroupCell.swift +++ b/EATSSU/App/Sources/Presentation/Home/View/RestaurantTableView/RestaurantMenuGroupCell.swift @@ -107,14 +107,21 @@ final class RestaurantMenuGroupCell: BaseTableViewCell { /// 메뉴 데이터를 이용해 스택뷰를 구성 /// - Parameter menus: 메뉴 리스트 (MenuTypeInfo 배열) + /// - Parameter emptyMessage: 메뉴가 없을 때 보여줄 문구 (영업 시간 아님 / 조회 실패) /// - Parameter indexPath: 현재 테이블뷰 indexPath (섹션, row) /// - Parameter onMenuTap: 터치 시 실행할 클로저 (section, index 전달) - func configure(with menus: [MenuTypeInfo], at indexPath: IndexPath, onMenuTap: @escaping (IndexPath, Int) -> Void) { + func configure( + with menus: [MenuTypeInfo], + emptyMessage: String = TextLiteral.Home.notBusinessHour, + at indexPath: IndexPath, + onMenuTap: @escaping (IndexPath, Int) -> Void + ) { // 이전 뷰들 제거 (주의: prepareForReuse와 중복 제거됨 → 안정성 보장용) menuStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } // 메뉴가 없을 경우 → 안내 텍스트 표시 if menus.isEmpty { + emptyLabel.text = emptyMessage emptyLabel.isHidden = false menuStackView.isHidden = true } else { diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/CustomTimeTabController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/CustomTimeTabController.swift index 5face11d..6c36cd32 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/CustomTimeTabController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/CustomTimeTabController.swift @@ -163,6 +163,12 @@ final class CustomTimeTabController: BaseViewController { dateSubject.send(date) } + /// 같은 날짜라도 다시 조회한다 (리뷰 작성 후 평점 갱신 등) + /// 날짜 파이프라인은 같은 날짜를 거르므로 거치지 않고 바로 요청한다 + func reloadData(for date: Date) { + performDateUpdate(date) + } + private func setupPageViewController() { addChild(pageViewController) view.addSubview(pageViewController.view) diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift index afe8d29c..05a92076 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift @@ -93,6 +93,9 @@ final class HomeRestaurantViewController: BaseViewController { // 고정 메뉴 데이터 (간식코너) var fixMenuTableViewData: [String: [Menus]] = [:] + + // 메뉴 조회에 실패한 식당. 빈 목록을 "영업 시간 아님"과 구분해 안내한다 + private var failedRestaurants: Set = [] // MARK: - UI Components @@ -189,6 +192,8 @@ final class HomeRestaurantViewController: BaseViewController { guard let self else { return } let isHoliday = await self.isHoliday(date: date) + // 공휴일을 조회하는 사이 다른 날짜로 바뀌었다면 반영하지 않는다 + guard !_Concurrency.Task.isCancelled else { return } if !FirebaseRemoteConfig.shared.isVacationPeriod, !weekday.isWeekend, @@ -227,6 +232,7 @@ final class HomeRestaurantViewController: BaseViewController { private func hideSnackCorner() { isSelectable = false fixMenuTableViewData[RestaurantIdentifier.snackCorner.rawValue] = [] + setLoadFailed(false, for: RestaurantIdentifier.snackCorner.rawValue) if let sectionIndex = getSectionIndex(for: RestaurantIdentifier.snackCorner.rawValue) { restaurantView.restaurantTableView.reloadSections( @@ -264,7 +270,11 @@ extension HomeRestaurantViewController: UITableViewDataSource { ? (fixMenuTableViewData[sectionKey]?.map { .fix($0) } ?? []) : (changeMenuTableViewData[sectionKey]?.map { .change($0) } ?? []) - cell.configure(with: menuList, at: indexPath) { [weak self] indexPath, menuIndex in + let emptyMessage = failedRestaurants.contains(sectionKey) + ? TextLiteral.Home.menuLoadFailed + : TextLiteral.Home.notBusinessHour + + cell.configure(with: menuList, emptyMessage: emptyMessage, at: indexPath) { [weak self] indexPath, menuIndex in self?.handleMenuTap(section: indexPath.section, menuIndex: menuIndex) } return cell @@ -347,7 +357,8 @@ extension HomeRestaurantViewController: UITableViewDataSource { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let sceneDelegate = windowScene.delegate as? SceneDelegate, let window = sceneDelegate.window { - window.replaceRootViewController(loginVC) + // 신규 가입자는 로그인 화면에서 닉네임 설정으로 push되므로 네비게이션으로 감싼다 + window.replaceRootViewController(UINavigationController(rootViewController: loginVC)) } } alert.addAction(confirm) @@ -428,13 +439,22 @@ extension HomeRestaurantViewController: UITableViewDelegate { // MARK: - Network extension HomeRestaurantViewController { + private func setLoadFailed(_ didFail: Bool, for restaurant: String) { + if didFail { + failedRestaurants.insert(restaurant) + } else { + failedRestaurants.remove(restaurant) + } + } + // 변경 메뉴 요청 API 호출 func fetchChangeMenuData(date: String, restaurant: String, time: String) async { // UI 업데이트에 사용할 메뉴 목록과 애니메이션 타입을 저장할 변수 let menusToUpdate: [ChangeMenuTableResponse] let animation: UITableView.RowAnimation - + var didFail = false + do { // 비동기 네트워크 요청을 통해 메뉴 데이터 가져오기 let menus: [ChangeMenuTableResponse] = try await withCheckedThrowingContinuation { continuation in @@ -458,11 +478,15 @@ extension HomeRestaurantViewController { print("\(restaurant) 변경 메뉴 조회 실패: \(error.localizedDescription)") menusToUpdate = [] animation = .none + didFail = true } - + // 메인 스레드에서 UI 업데이트 await MainActor.run { + // 네트워크 요청은 취소되지 않으므로, 다른 날짜로 바뀐 뒤 도착한 응답은 버린다 + guard !_Concurrency.Task.isCancelled else { return } self.changeMenuTableViewData[restaurant] = menusToUpdate + self.setLoadFailed(didFail, for: restaurant) if let sectionIndex = self.getSectionIndex(for: restaurant) { self.restaurantView.restaurantTableView.reloadSections(IndexSet(integer: sectionIndex), with: animation) @@ -474,6 +498,7 @@ extension HomeRestaurantViewController { func fetchFixedMenuData(restaurant: String) async { var menuData: [Menus] = [] var animation: UITableView.RowAnimation = .none + var didFail = false do { let response: FixedMenuTableResponse = try await withCheckedThrowingContinuation { continuation in @@ -492,11 +517,15 @@ extension HomeRestaurantViewController { animation = .fade } catch { print("\(restaurant) 고정 메뉴 조회 실패: \(error.localizedDescription)") + didFail = true } - + // 메인 스레드에서 UI 업데이트 await MainActor.run { + // 네트워크 요청은 취소되지 않으므로, 다른 날짜로 바뀐 뒤 도착한 응답은 버린다 + guard !_Concurrency.Task.isCancelled else { return } self.fixMenuTableViewData[restaurant] = menuData + self.setLoadFailed(didFail, for: restaurant) if let sectionIndex = self.getSectionIndex(for: restaurant) { self.restaurantView.restaurantTableView.reloadSections(IndexSet(integer: sectionIndex), with: animation) } diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift index 32230d4a..49b82306 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift @@ -121,7 +121,7 @@ final class HomeViewController: BaseViewController { /// 리뷰 작성 후 데이터 새로고침 func refreshAfterReview() { - tabmanController.dateFetchData(for: currentDate) + tabmanController.reloadData(for: currentDate) } /// 하단 탭바에서 학식 탭 클릭 시 오늘 날짜로 초기화 diff --git a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift index 52606301..94de24f1 100644 --- a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift @@ -238,7 +238,8 @@ final class CustomTabBarContainerController: UITabBarController { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let sceneDelegate = windowScene.delegate as? SceneDelegate, let window = sceneDelegate.window { - window.replaceRootViewController(loginVC) + // 신규 가입자는 로그인 화면에서 닉네임 설정으로 push되므로 네비게이션으로 감싼다 + window.replaceRootViewController(UINavigationController(rootViewController: loginVC)) } } } diff --git a/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift b/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift index d6d6a739..a894ba96 100644 --- a/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift +++ b/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift @@ -452,6 +452,11 @@ enum TextLiteral { Localization.localized("home.notBusinessHour", fallback: "영업 시간이 아니에요.") } + /// RestaurantMenuGroupCell - "메뉴를 불러오지 못했어요." + static var menuLoadFailed: String { + Localization.localized("home.menuLoadFailed", fallback: "메뉴를 불러오지 못했어요.") + } + /// RestaurantTableViewHeader - "기숙사 식당" static var dormitoryRestaurant: String { Localization.localized("home.dormitoryRestaurant", fallback: "기숙사 식당")