From 1786ac46ead993cd77d04a59a25fed2b025982ac Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sat, 17 Oct 2026 17:58:47 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[#473]=20=ED=91=B8=EC=8B=9C=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=EC=9D=84=20=EC=84=9C=EB=B2=84=EC=97=90=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=C2=B7=EC=82=AD=EC=A0=9C=ED=95=98=EA=B3=A0=20=EA=B8=B0?= =?UTF-8?q?=EC=A1=B4=2011=EC=8B=9C=20=EB=A1=9C=EC=BB=AC=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Resources/en.lproj/Localizable.strings | 4 - .../Resources/ja.lproj/Localizable.strings | 4 - .../Resources/ko.lproj/Localizable.strings | 4 - .../Resources/vi.lproj/Localizable.strings | 4 - .../Sources/Data/LocalDB/AccountStorage.swift | 2 + .../Data/Network/Router/PushTokenRouter.swift | 54 +++++++++ .../Notification/NotificationManager.swift | 68 ++--------- .../Notification/PushTokenManager.swift | 107 ++++++++++++++++++ .../ViewController/MyPageViewController.swift | 19 ++-- .../CustomTabBarContainerController.swift | 3 +- .../Utility/Application/AppDelegate.swift | 22 ++-- .../Sources/Utility/Literal/TextLiteral.swift | 9 -- 12 files changed, 192 insertions(+), 108 deletions(-) create mode 100644 EATSSU/App/Sources/Data/Network/Router/PushTokenRouter.swift create mode 100644 EATSSU/App/Sources/Notification/PushTokenManager.swift diff --git a/EATSSU/App/Resources/en.lproj/Localizable.strings b/EATSSU/App/Resources/en.lproj/Localizable.strings index 61ea5310..05ade024 100644 --- a/EATSSU/App/Resources/en.lproj/Localizable.strings +++ b/EATSSU/App/Resources/en.lproj/Localizable.strings @@ -458,10 +458,6 @@ // MARK: - Notification -/// ๐Ÿค” ์˜ค๋Š˜ ๋ฐฅ ๋ญ ๋จน์ง€โ€ฆ -"notification.dailyWeekdayNotificationTitle" = "๐Ÿค” What should I eat todayโ€ฆ"; -/// ์˜ค๋Š˜์˜ ํ•™์‹์„ ํ™•์ธํ•ด๋ณด์„ธ์š”! -"notification.dailyWeekdayNotificationBody" = "Check today's cafeteria menu!"; /// ์•Œ๋ฆผ ๊ถŒํ•œ ํ•„์š” "notification_error_permission_denied_message" = "Notification Permission Required"; /// ์•Œ๋ฆผ์„ ๋ฐ›์œผ๋ ค๋ฉด ์•Œ๋ฆผ ๊ถŒํ•œ์„ ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n์„ค์ •์—์„œ ์•Œ๋ฆผ ๊ถŒํ•œ์„ ํ—ˆ์šฉํ•ด์ฃผ์„ธ์š”. diff --git a/EATSSU/App/Resources/ja.lproj/Localizable.strings b/EATSSU/App/Resources/ja.lproj/Localizable.strings index 85d0f374..1c9d52ea 100644 --- a/EATSSU/App/Resources/ja.lproj/Localizable.strings +++ b/EATSSU/App/Resources/ja.lproj/Localizable.strings @@ -459,10 +459,6 @@ // MARK: - Notification -/// ๐Ÿค” ์˜ค๋Š˜ ๋ฐฅ ๋ญ ๋จน์ง€โ€ฆ -"notification.dailyWeekdayNotificationTitle" = "๐Ÿค” ไปŠๆ—ฅไฝ•ใ‚’้ฃŸในใ‚ˆใ†โ€ฆ"; -/// ์˜ค๋Š˜์˜ ํ•™์‹์„ ํ™•์ธํ•ด๋ณด์„ธ์š”! -"notification.dailyWeekdayNotificationBody" = "ไปŠๆ—ฅใฎๅญฆ้ฃŸใƒกใƒ‹ใƒฅใƒผใ‚’ใƒใ‚งใƒƒใ‚ฏใ—ใพใ—ใ‚‡ใ†๏ผ"; /// ์•Œ๋ฆผ ๊ถŒํ•œ ํ•„์š” "notification_error_permission_denied_message" = "้€š็Ÿฅๆจฉ้™ใŒๅฟ…่ฆใงใ™"; /// ์•Œ๋ฆผ์„ ๋ฐ›์œผ๋ ค๋ฉด ์•Œ๋ฆผ ๊ถŒํ•œ์„ ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n์„ค์ •์—์„œ ์•Œ๋ฆผ ๊ถŒํ•œ์„ ํ—ˆ์šฉํ•ด์ฃผ์„ธ์š”. diff --git a/EATSSU/App/Resources/ko.lproj/Localizable.strings b/EATSSU/App/Resources/ko.lproj/Localizable.strings index 20e7f796..16e8de7d 100644 --- a/EATSSU/App/Resources/ko.lproj/Localizable.strings +++ b/EATSSU/App/Resources/ko.lproj/Localizable.strings @@ -459,10 +459,6 @@ // MARK: - Notification -/// ๐Ÿค” ์˜ค๋Š˜ ๋ฐฅ ๋ญ ๋จน์ง€โ€ฆ -"notification.dailyWeekdayNotificationTitle" = "๐Ÿค” ์˜ค๋Š˜ ๋ฐฅ ๋ญ ๋จน์ง€โ€ฆ"; -/// ์˜ค๋Š˜์˜ ํ•™์‹์„ ํ™•์ธํ•ด๋ณด์„ธ์š”! -"notification.dailyWeekdayNotificationBody" = "์˜ค๋Š˜์˜ ํ•™์‹์„ ํ™•์ธํ•ด๋ณด์„ธ์š”!"; /// ์•Œ๋ฆผ ๊ถŒํ•œ ํ•„์š” "notification_error_permission_denied_message" = "์•Œ๋ฆผ ๊ถŒํ•œ ํ•„์š”"; /// ์•Œ๋ฆผ์„ ๋ฐ›์œผ๋ ค๋ฉด ์•Œ๋ฆผ ๊ถŒํ•œ์„ ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n์„ค์ •์—์„œ ์•Œ๋ฆผ ๊ถŒํ•œ์„ ํ—ˆ์šฉํ•ด์ฃผ์„ธ์š”. diff --git a/EATSSU/App/Resources/vi.lproj/Localizable.strings b/EATSSU/App/Resources/vi.lproj/Localizable.strings index 5afb46ea..acd81f18 100644 --- a/EATSSU/App/Resources/vi.lproj/Localizable.strings +++ b/EATSSU/App/Resources/vi.lproj/Localizable.strings @@ -459,10 +459,6 @@ // MARK: - Notification -/// ๐Ÿค” ์˜ค๋Š˜ ๋ฐฅ ๋ญ ๋จน์ง€โ€ฆ -"notification.dailyWeekdayNotificationTitle" = "๐Ÿค” Hรดm nay ฤƒn gรฌ ฤ‘รขyโ€ฆ"; -/// ์˜ค๋Š˜์˜ ํ•™์‹์„ ํ™•์ธํ•ด๋ณด์„ธ์š”! -"notification.dailyWeekdayNotificationBody" = "Xem thแปฑc ฤ‘ฦกn nhร  ฤƒn hรดm nay!"; /// ์•Œ๋ฆผ ๊ถŒํ•œ ํ•„์š” "notification_error_permission_denied_message" = "Cแบงn quyแปn thรดng bรกo"; /// ์•Œ๋ฆผ์„ ๋ฐ›์œผ๋ ค๋ฉด ์•Œ๋ฆผ ๊ถŒํ•œ์„ ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n์„ค์ •์—์„œ ์•Œ๋ฆผ ๊ถŒํ•œ์„ ํ—ˆ์šฉํ•ด์ฃผ์„ธ์š”. diff --git a/EATSSU/App/Sources/Data/LocalDB/AccountStorage.swift b/EATSSU/App/Sources/Data/LocalDB/AccountStorage.swift index befa46b2..6aa6dc17 100644 --- a/EATSSU/App/Sources/Data/LocalDB/AccountStorage.swift +++ b/EATSSU/App/Sources/Data/LocalDB/AccountStorage.swift @@ -15,5 +15,7 @@ enum AccountStorage { // ์ฐœ ๋ชฉ๋กยท์ˆœ์„œ PartnershipLikeManager.shared.reset() MenuFavoriteManager.shared.reset() + // ๋‹ค์Œ ๋กœ๊ทธ์ธ ๊ณ„์ •์— ํ‘ธ์‹œ ํ† ํฐ์„ ๋‹ค์‹œ ๋“ฑ๋กํ•˜๋„๋ก + PushTokenManager.shared.reset() } } diff --git a/EATSSU/App/Sources/Data/Network/Router/PushTokenRouter.swift b/EATSSU/App/Sources/Data/Network/Router/PushTokenRouter.swift new file mode 100644 index 00000000..3083596d --- /dev/null +++ b/EATSSU/App/Sources/Data/Network/Router/PushTokenRouter.swift @@ -0,0 +1,54 @@ +// +// PushTokenRouter.swift +// EATSSU +// +// Created by ํ™ฉ์ƒํ™˜ on 10/17/26. +// + +import Foundation + +import Moya + +/// ํ‘ธ์‹œ ํ† ํฐ API (๋กœ๊ทธ์ธ ํ•„์š”). ์„œ๋ฒ„๋Š” ํ† ํฐ ๋“ฑ๋ก ์—ฌ๋ถ€๋กœ ์•Œ๋ฆผ ํ—ˆ์šฉ ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•œ๋‹ค +enum PushTokenRouter { + /// ๋“ฑ๋กยท๊ฐฑ์‹ . ๋‹ค๋ฅธ ๊ณ„์ •์— ๋“ฑ๋ก๋œ ํ† ํฐ์ด๋ฉด ํ˜„์žฌ ๊ณ„์ •์œผ๋กœ ์˜ฎ๊ฒจ์ง„๋‹ค + case register(token: String) + /// ์‚ญ์ œ. ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ํ† ํฐ์ด์–ด๋„ ์„ฑ๊ณต + case unregister(token: String) +} + +extension PushTokenRouter: TargetType { + var baseURL: URL { + URL(string: Config.baseURL)! + } + + var path: String { + "/users/push-token" + } + + var method: Moya.Method { + switch self { + case .register: + .put + case .unregister: + .delete + } + } + + var task: Moya.Task { + switch self { + case let .register(token): + .requestParameters(parameters: ["token": token, "deviceType": "IOS"], encoding: JSONEncoding.default) + case let .unregister(token): + .requestParameters(parameters: ["token": token], encoding: JSONEncoding.default) + } + } + + var headers: [String: String]? { + ["Content-Type": "application/json"] + } + + var validationType: ValidationType { + .successCodes + } +} diff --git a/EATSSU/App/Sources/Notification/NotificationManager.swift b/EATSSU/App/Sources/Notification/NotificationManager.swift index aee3b253..666bdcb0 100644 --- a/EATSSU/App/Sources/Notification/NotificationManager.swift +++ b/EATSSU/App/Sources/Notification/NotificationManager.swift @@ -16,55 +16,12 @@ class NotificationManager { // MARK: - Methods - /// ํ‰์ผ 11์‹œ์— ์•ฑ์˜ ์œ ์ž…์„ ์œ ๋„ํ•˜๋Š” ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ๋ฐœ์†กํ•˜๋Š” ๋ฉ”์†Œ๋“œ + /// ์ด์ „ ๋ฒ„์ „์ด ๊ธฐ๊ธฐ์— ์˜ˆ์•ฝํ•ด ๋‘” ํ‰์ผ 11์‹œ ๋กœ์ปฌ ์•Œ๋ฆผ์„ ์ง€์šด๋‹ค /// - /// - Title : ๐Ÿค” ์˜ค๋Š˜ ๋ฐฅ ๋ญ ๋จน์ง€โ€ฆ - /// - Body : ์˜ค๋Š˜์˜ ํ•™์‹์„ ํ™•์ธํ•ด๋ณด์„ธ์š”! - func scheduleWeekday11AMNotification() { - let center = UNUserNotificationCenter.current() - - // ์•Œ๋ฆผ ์ฝ˜ํ…์ธ  ์„ค์ • - let content = UNMutableNotificationContent() - - content.title = TextLiteral.Notification.dailyWeekdayNotificationTitle - content.body = TextLiteral.Notification.dailyWeekdayNotificationBody - content.sound = .default - - // ๋ฐ˜๋ณตํ•  ์š”์ผ ๋ฐ ์‹œ๊ฐ„ ์„ค์ • (ํ‰์ผ ์˜ค์ „ 11์‹œ) - let weekdays = [2, 3, 4, 5, 6] // ์›”, ํ™”, ์ˆ˜, ๋ชฉ, ๊ธˆ (Calendar์—์„œ 1์ด ์ผ์š”์ผ) - - for weekday in weekdays { - var dateComponents = DateComponents() - dateComponents.hour = 11 - dateComponents.minute = 0 - dateComponents.weekday = weekday - - let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true) - - // ๊ณ ์œ ํ•œ ์‹๋ณ„์ž๋ฅผ ์œ„ํ•ด weekday๋ฅผ ์‚ฌ์šฉ - let identifier = "weekdayNotification-\(weekday)" - let request = UNNotificationRequest( - identifier: identifier, content: content, trigger: trigger - ) - - // ์•Œ๋ฆผ ๋“ฑ๋ก - center.add(request) { error in - if let error { - print("์•Œ๋ฆผ ๋“ฑ๋ก ๊ฐ„ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€: \(error.localizedDescription)") - } - } - } - } - - /// ํ‰์ผ 11์‹œ์— ์•ฑ์˜ ์œ ์ž…์„ ์œ ๋„ํ•˜๋Š” ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ์ทจ์†Œํ•˜๋Š” ๋ฉ”์†Œ๋“œ - func cancelWeekday11AMNotification() { - let weekdays = [2, 3, 4, 5, 6] - - for weekday in weekdays { - let identifier = "weekdayNotification-\(weekday)" - let center = UNUserNotificationCenter.current() - center.removePendingNotificationRequests(withIdentifiers: [identifier]) - } + /// 11์‹œ ํ•™์‹ ์•Œ๋ฆผ์€ ์„œ๋ฒ„ ํ‘ธ์‹œ(์ฐœ ์•Œ๋ฆผ / ์ผ๋ฐ˜ ํ•™์‹ ์•Œ๋ฆผ, ํ•˜๋ฃจ 1๊ฑด)๋กœ ์˜ฎ๊ฒจ, ์—…๋ฐ์ดํŠธ ํ›„์—๋„ ๋กœ์ปฌ ์•Œ๋ฆผ์ด ๋‚จ์•„ ์žˆ์œผ๋ฉด ์ค‘๋ณต์œผ๋กœ ์˜จ๋‹ค. + func removeLegacyDailyNotifications() { + let identifiers = [2, 3, 4, 5, 6].map { "weekdayNotification-\($0)" } + UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers) } /// ์•ฑ ์‹คํ–‰ ์‹œ ์•Œ๋ฆผ ๋ฐœ์†ก ๊ถŒํ•œ์„ ์š”์ฒญํ•˜๋Š” ํŒ์—… ํ˜ธ์ถœ ๋ฉ”์†Œ๋“œ @@ -77,7 +34,7 @@ class NotificationManager { return await UNUserNotificationCenter.current().notificationSettings() } - /// ์•Œ๋ฆผ ๊ถŒํ•œ ์ƒํƒœ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ์•ก์…˜์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ + /// ์•Œ๋ฆผ ๊ถŒํ•œ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ด ํ† ๊ธ€์˜ ๋‹ค์Œ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜. ์„œ๋ฒ„ ๋ฐ˜์˜์€ `PushTokenManager.sync()`๊ฐ€ ํ•œ๋‹ค func handleNotificationToggle(currentState: Bool) async throws -> Bool { let settings = await checkNotificationSetting() @@ -86,21 +43,12 @@ class NotificationManager { throw NotificationError.permissionDenied case .authorized, .provisional, .ephemeral: - let newState = !currentState - - if newState { - scheduleWeekday11AMNotification() - } else { - cancelWeekday11AMNotification() - } - - return newState - + return !currentState + case .notDetermined: let granted = try await requestNotificationPermission() if granted { - scheduleWeekday11AMNotification() return true } else { throw NotificationError.permissionDenied diff --git a/EATSSU/App/Sources/Notification/PushTokenManager.swift b/EATSSU/App/Sources/Notification/PushTokenManager.swift new file mode 100644 index 00000000..7c1e94fe --- /dev/null +++ b/EATSSU/App/Sources/Notification/PushTokenManager.swift @@ -0,0 +1,107 @@ +// +// PushTokenManager.swift +// EATSSU +// +// Created by ํ™ฉ์ƒํ™˜ on 10/17/26. +// + +import Foundation +import UserNotifications + +import FirebaseMessaging + +/// FCM ํ† ํฐ์„ ์„œ๋ฒ„์— ๋“ฑ๋กยท์‚ญ์ œํ•ด ์•Œ๋ฆผ ์ˆ˜์‹  ์—ฌ๋ถ€๋ฅผ ๋งž์ถ˜๋‹ค +/// +/// ์„œ๋ฒ„๋Š” ํ† ํฐ์ด ๋“ฑ๋ก๋ผ ์žˆ์œผ๋ฉด ์•Œ๋ฆผ์„ ๋ณด๋‚ธ๋‹ค. ๊ทธ๋ž˜์„œ "๋กœ๊ทธ์ธ ์ƒํƒœ + ๋งˆ์ดํŽ˜์ด์ง€ ์•Œ๋ฆผ ON + ๊ธฐ๊ธฐ ์•Œ๋ฆผ ๊ถŒํ•œ ํ—ˆ์šฉ"์ผ ๋•Œ๋งŒ +/// ๋“ฑ๋กํ•˜๊ณ , ๊ทธ ์™ธ์—๋Š” ์‚ญ์ œํ•œ๋‹ค. ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋Š” ์„œ๋ฒ„ API๋ฅผ ์“ธ ์ˆ˜ ์—†์–ด ์•Œ๋ฆผ์„ ๋ฐ›์ง€ ์•Š๋Š”๋‹ค. +final class PushTokenManager { + + // MARK: - Singleton + + static let shared = PushTokenManager() + + private init() {} + + // MARK: - Constants + + /// ๋กœ๊ทธ์•„์›ƒ ์‹œ ์‚ญ์ œ ์š”์ฒญ์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ตœ๋Œ€ ์‹œ๊ฐ„. ๋„คํŠธ์›Œํฌ๊ฐ€ ๋А๋ ค๋„ ๋กœ๊ทธ์•„์›ƒ์ด ๋ง‰ํžˆ์ง€ ์•Š๊ฒŒ ํ•œ๋‹ค + private static let unregisterTimeout: TimeInterval = 3 + + // MARK: - State + + /// ๋งˆ์ง€๋ง‰์œผ๋กœ ์„œ๋ฒ„์— ๋ฐ˜์˜ํ•œ ์ƒํƒœ. ์•ฑ ์‹คํ–‰ ์ค‘ ๊ฐ™์€ ์š”์ฒญ์„ ๋ฐ˜๋ณตํ•˜์ง€ ์•Š๊ฒŒ ํ•œ๋‹ค + private var lastSynced: (token: String, isRegistered: Bool)? + + /// ๋กœ๊ทธ์•„์›ƒยทํƒˆํ‡ดยท์„ธ์…˜ ๋งŒ๋ฃŒ ์‹œ ํ˜ธ์ถœ. ๋‹ค์Œ ๋กœ๊ทธ์ธ ๊ณ„์ •์— ๋‹ค์‹œ ๋“ฑ๋ก๋˜๊ฒŒ ํ•œ๋‹ค + func reset() { + lastSynced = nil + } + + // MARK: - Sync + + /// ํ˜„์žฌ ์ƒํƒœ์— ๋งž๊ฒŒ ์„œ๋ฒ„์— ํ† ํฐ์„ ๋“ฑ๋กํ•˜๊ฑฐ๋‚˜ ์‚ญ์ œํ•œ๋‹ค + /// ํ˜ธ์ถœ ์‹œ์ : ๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ์ง„์ž…, FCM ํ† ํฐ ๊ฐฑ์‹ , ์•Œ๋ฆผ ๊ถŒํ•œ ๊ฒฐ๊ณผ, ๋งˆ์ดํŽ˜์ด์ง€ ์•Œ๋ฆผ ํ† ๊ธ€ ๋ณ€๊ฒฝ + func sync() { + guard TokenStore.hasAccessToken, let token = Messaging.messaging().fcmToken, !token.isEmpty else { return } + + Task { @MainActor in + let shouldRegister = await Self.isNotificationEnabled() + // ํ™•์ธํ•˜๋Š” ์‚ฌ์ด ๋กœ๊ทธ์•„์›ƒ๋์œผ๋ฉด ๋ณด๋‚ด์ง€ ์•Š๋Š”๋‹ค + guard TokenStore.hasAccessToken else { return } + if let lastSynced, lastSynced.token == token, lastSynced.isRegistered == shouldRegister { return } + + let target: PushTokenRouter = shouldRegister ? .register(token: token) : .unregister(token: token) + // ์•ฑ ์ˆ˜๋ช… ๋™์•ˆ ์œ ์ง€๋˜๋Š” ์‹ฑ๊ธ€ํ„ด์ด๋ผ self๋ฅผ ๊ทธ๋Œ€๋กœ ์บก์ฒ˜ํ•œ๋‹ค + NetworkService.shared.request(target, responseType: Bool.self, useAuth: true) { result in + switch result { + case .success: + self.lastSynced = (token, shouldRegister) + case .failure(let error): + // ๋‹ค์Œ ์ง„์ž…ยทํ† ํฐ ๊ฐฑ์‹  ๋•Œ ๋‹ค์‹œ ๋งž์ถ”๋ฏ€๋กœ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค + print("ํ‘ธ์‹œ ํ† ํฐ \(shouldRegister ? "๋“ฑ๋ก" : "์‚ญ์ œ") ์‹คํŒจ: \(error.localizedDescription)") + } + } + } + } + + /// ๋กœ๊ทธ์•„์›ƒ ์ง์ „ ํ˜ธ์ถœ. ๊ณ„์ • ํ† ํฐ์ด ์ง€์›Œ์ง€๊ธฐ ์ „์— ์„œ๋ฒ„์˜ ํ‘ธ์‹œ ํ† ํฐ์„ ์‚ญ์ œํ•˜๊ณ  completion์„ ๋ถ€๋ฅธ๋‹ค. + /// ์„œ๋ฒ„์— ๋กœ๊ทธ์•„์›ƒ API๊ฐ€ ์—†์–ด, ์ด ์š”์ฒญ์ด ์—†์œผ๋ฉด ๋กœ๊ทธ์•„์›ƒํ•œ ๊ธฐ๊ธฐ๋กœ ์ด์ „ ๊ณ„์ •์˜ ์•Œ๋ฆผ์ด ๊ณ„์† ๊ฐ„๋‹ค. + /// ์‹คํŒจํ•˜๊ฑฐ๋‚˜ ์‘๋‹ต์ด ๋Šฆ์–ด๋„ `unregisterTimeout` ๋’ค์—๋Š” ๋กœ๊ทธ์•„์›ƒ์„ ์ง„ํ–‰ํ•œ๋‹ค. + func unregisterBeforeLogout(completion: @escaping () -> Void) { + guard TokenStore.hasAccessToken, let token = Messaging.messaging().fcmToken, !token.isEmpty else { + completion() + return + } + + var didComplete = false + let finish = { + guard !didComplete else { return } + didComplete = true + completion() + } + + NetworkService.shared.request(PushTokenRouter.unregister(token: token), responseType: Bool.self, useAuth: true) { result in + if case .failure(let error) = result { + print("๋กœ๊ทธ์•„์›ƒ ํ‘ธ์‹œ ํ† ํฐ ์‚ญ์ œ ์‹คํŒจ: \(error.localizedDescription)") + } + finish() + } + DispatchQueue.main.asyncAfter(deadline: .now() + Self.unregisterTimeout, execute: finish) + } + + // MARK: - Private + + /// ๋งˆ์ดํŽ˜์ด์ง€ ์•Œ๋ฆผ ํ† ๊ธ€ ON + ๊ธฐ๊ธฐ ์•Œ๋ฆผ ๊ถŒํ•œ ํ—ˆ์šฉ + private static func isNotificationEnabled() async -> Bool { + guard UserDefaults.standard.bool(forKey: TextLiteral.MyPage.pushNotificationUserSettingKey) else { return false } + let settings = await UNUserNotificationCenter.current().notificationSettings() + switch settings.authorizationStatus { + case .authorized, .provisional, .ephemeral: + return true + case .denied, .notDetermined: + return false + @unknown default: + return false + } + } +} diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift index 2863ef00..c3c7b0f1 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift @@ -114,14 +114,17 @@ final class MyPageViewController: BaseViewController { let fixAction = UIAlertAction(title: TextLiteral.MyPage.logout, style: .default, handler: { _ in - AnalyticsIdentityManager.reset() - AccountStorage.reset() + // ์„œ๋ฒ„์— ๋กœ๊ทธ์•„์›ƒ API๊ฐ€ ์—†์–ด, ๊ณ„์ • ํ† ํฐ์„ ์ง€์šฐ๊ธฐ ์ „์— ์ด ๊ธฐ๊ธฐ์˜ ํ‘ธ์‹œ ํ† ํฐ์„ ์‚ญ์ œํ•ด์•ผ ์•Œ๋ฆผ์ด ๋ฉˆ์ถ˜๋‹ค + PushTokenManager.shared.unregisterBeforeLogout { + AnalyticsIdentityManager.reset() + AccountStorage.reset() - let loginViewController = LoginViewController() - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) - { - keyWindow.replaceRootViewController(UINavigationController(rootViewController: loginViewController)) + let loginViewController = LoginViewController() + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) + { + keyWindow.replaceRootViewController(UINavigationController(rootViewController: loginViewController)) + } } }) @@ -372,6 +375,8 @@ extension MyPageViewController: UITableViewDelegate { // ์Šค์œ„์น˜ ์ƒํƒœ ์—…๋ฐ์ดํŠธ self.switchState = newState self.saveSwitchStateToUserDefaults() + // ์„œ๋ฒ„๋Š” ํ‘ธ์‹œ ํ† ํฐ ๋“ฑ๋ก ์—ฌ๋ถ€๋กœ ์•Œ๋ฆผ ํ—ˆ์šฉ์„ ํŒ๋‹จํ•œ๋‹ค (ON: ๋“ฑ๋ก, OFF: ์‚ญ์ œ) + PushTokenManager.shared.sync() // UI ์—…๋ฐ์ดํŠธ if let cell = self.mypageView.myPageTableView.cellForRow(at: indexPath) as? NotificationSettingTableViewCell { diff --git a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift index 92cca80c..bac90758 100644 --- a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift @@ -42,8 +42,9 @@ final class CustomTabBarContainerController: UITabBarController { setupTabBar() setupViewControllers() delegate = self - // ๋กœ๊ทธ์ธ ์ƒํƒœ์˜ ๋ชจ๋“  ์ง„์ž…(์•ฑ ์‹คํ–‰ยท๋กœ๊ทธ์ธยท๊ฐ€์ž…ยท์–ธ์–ด ๋ณ€๊ฒฝ ํ›„ ์žฌ๊ตฌ์„ฑ)์ด ์ด ํ™”๋ฉด์„ ๊ฑฐ์น˜๋ฏ€๋กœ ์—ฌ๊ธฐ์„œ ์„œ๋ฒ„ ์–ธ์–ด๋ฅผ ๋งž์ถ˜๋‹ค + // ๋กœ๊ทธ์ธ ์ƒํƒœ์˜ ๋ชจ๋“  ์ง„์ž…(์•ฑ ์‹คํ–‰ยท๋กœ๊ทธ์ธยท๊ฐ€์ž…ยท์–ธ์–ด ๋ณ€๊ฒฝ ํ›„ ์žฌ๊ตฌ์„ฑ)์ด ์ด ํ™”๋ฉด์„ ๊ฑฐ์น˜๋ฏ€๋กœ ์—ฌ๊ธฐ์„œ ์„œ๋ฒ„ ์–ธ์–ดยทํ‘ธ์‹œ ํ† ํฐ์„ ๋งž์ถ˜๋‹ค ServerLanguageSync.sync() + PushTokenManager.shared.sync() } // MARK: - Setup diff --git a/EATSSU/App/Sources/Utility/Application/AppDelegate.swift b/EATSSU/App/Sources/Utility/Application/AppDelegate.swift index 14f96acc..60a31c39 100644 --- a/EATSSU/App/Sources/Utility/Application/AppDelegate.swift +++ b/EATSSU/App/Sources/Utility/Application/AppDelegate.swift @@ -56,7 +56,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } else { LaunchSourceManager.shared.setSource(.localNotification) } - + completionHandler() } @@ -116,6 +116,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// ํ‘ธ์‹œ ์•Œ๋ฆผ ๊ถŒํ•œ์„ ์š”์ฒญํ•˜๊ณ  ์„ค์ •์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. private func setupNotificationPermissions() { + // 11์‹œ ์•Œ๋ฆผ์€ ์„œ๋ฒ„ ํ‘ธ์‹œ๋กœ ์˜ฎ๊ฒจ, ์ด์ „ ๋ฒ„์ „์ด ์˜ˆ์•ฝํ•œ ๋กœ์ปฌ ์•Œ๋ฆผ์€ ์ง€์šด๋‹ค + NotificationManager.shared.removeLegacyDailyNotifications() _Concurrency.Task { let userSettingKey = TextLiteral.MyPage.pushNotificationUserSettingKey do { @@ -126,20 +128,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD if granted { if !hasExistingSetting || isAppPermissionGranted { - NotificationManager.shared.scheduleWeekday11AMNotification() UserDefaults.standard.set(true, forKey: userSettingKey) - } else { - NotificationManager.shared.cancelWeekday11AMNotification() } } else { - NotificationManager.shared.cancelWeekday11AMNotification() UserDefaults.standard.set(false, forKey: userSettingKey) } } catch { print("์•Œ๋ฆผ ๊ถŒํ•œ ์š”์ฒญ ์‹คํŒจ: \(error)") - NotificationManager.shared.cancelWeekday11AMNotification() UserDefaults.standard.set(false, forKey: userSettingKey) } + // ๊ถŒํ•œยทํ† ๊ธ€ ๊ฒฐ๊ณผ์— ๋งž๊ฒŒ ์„œ๋ฒ„ ํ‘ธ์‹œ ํ† ํฐ ๋“ฑ๋ก/์‚ญ์ œ (๋กœ๊ทธ์ธ ์ƒํƒœ์ผ ๋•Œ๋งŒ ๋™์ž‘) + PushTokenManager.shared.sync() } } @@ -236,14 +235,7 @@ extension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { print("Firebase registration token: \(String(describing: fcmToken))") - let dataDict: [String: String] = ["token": fcmToken ?? ""] - NotificationCenter.default.post( - name: Notification.Name("FCMToken"), - object: nil, - userInfo: dataDict - ) - - // TODO: ํ•„์š”์‹œ ์„œ๋ฒ„์— ํ† ํฐ ์ „์†ก - // ์•ฑ ์‹œ์ž‘์‹œ์™€ ํ† ํฐ์ด ์ƒˆ๋กœ ์ƒ์„ฑ๋  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. + // ์•ฑ ์‹œ์ž‘ ์‹œ์™€ ํ† ํฐ์ด ์ƒˆ๋กœ ์ƒ์„ฑ๋  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋˜๋ฏ€๋กœ ์„œ๋ฒ„ ๋“ฑ๋ก ์ƒํƒœ๋ฅผ ๋งž์ถ˜๋‹ค (๋กœ๊ทธ์ธ ์ƒํƒœ์ผ ๋•Œ๋งŒ ๋™์ž‘) + PushTokenManager.shared.sync() } } diff --git a/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift b/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift index 91c680e1..d5fea5b5 100644 --- a/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift +++ b/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift @@ -1205,15 +1205,6 @@ enum TextLiteral { // MARK: - Notification enum Notification { - /// ๐Ÿค” ์˜ค๋Š˜ ๋ฐฅ ๋ญ ๋จน์ง€โ€ฆ - static var dailyWeekdayNotificationTitle: String { - Localization.localized("notification.dailyWeekdayNotificationTitle", fallback: "๐Ÿค” ์˜ค๋Š˜ ๋ฐฅ ๋ญ ๋จน์ง€โ€ฆ") - } - - /// ์˜ค๋Š˜์˜ ํ•™์‹์„ ํ™•์ธํ•ด๋ณด์„ธ์š”! - static var dailyWeekdayNotificationBody: String { - Localization.localized("notification.dailyWeekdayNotificationBody", fallback: "์˜ค๋Š˜์˜ ํ•™์‹์„ ํ™•์ธํ•ด๋ณด์„ธ์š”!") - } /// ์•Œ๋ฆผ ๊ถŒํ•œ ํ•„์š” static var permissionDeniedMessage: String { Localization.localized( From 6d504ce3e934577ffc44a3591e176f1ce59e905d Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sat, 17 Oct 2026 17:58:47 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[#473]=20=ED=95=99=EC=8B=9D=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=EC=9D=84=20=EB=88=84=EB=A5=B4=EB=A9=B4=20=ED=95=B4?= =?UTF-8?q?=EB=8B=B9=20=EB=82=A0=EC=A7=9C=C2=B7=EC=8B=9C=EA=B0=84=EB=8C=80?= =?UTF-8?q?=EC=9D=98=20=ED=95=99=EC=8B=9D=20=ED=99=94=EB=A9=B4=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Notification/PushRoute.swift | 88 +++++++++++++++++++ .../CustomTimeTabController.swift | 17 +++- .../ViewController/HomeViewController.swift | 10 +++ .../CustomTabBarContainerController.swift | 17 ++++ .../Utility/Application/AppDelegate.swift | 3 + .../Utility/Application/SceneDelegate.swift | 6 ++ EATSSU/Tests/UnitTests/PushRouteTests.swift | 43 +++++++++ 7 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 EATSSU/App/Sources/Notification/PushRoute.swift create mode 100644 EATSSU/Tests/UnitTests/PushRouteTests.swift diff --git a/EATSSU/App/Sources/Notification/PushRoute.swift b/EATSSU/App/Sources/Notification/PushRoute.swift new file mode 100644 index 00000000..4565b030 --- /dev/null +++ b/EATSSU/App/Sources/Notification/PushRoute.swift @@ -0,0 +1,88 @@ +// +// PushRoute.swift +// EATSSU +// +// Created by ํ™ฉ์ƒํ™˜ on 10/17/26. +// + +import UIKit + +/// ํ•™์‹ ์•Œ๋ฆผ์„ ๋ˆŒ๋ €์„ ๋•Œ ์ด๋™ํ•  ํ•™์‹ ํ™”๋ฉด (๋‚ ์งœ + ์‹œ๊ฐ„๋Œ€) +struct PushRoute: Equatable { + /// ํ•™์‹ ํƒญ ์‹œ๊ฐ„๋Œ€ ์ˆœ์„œ (์•„์นจยท์ ์‹ฌยท์ €๋…) + enum MealTime: String, CaseIterable { + case morning = "MORNING" + case lunch = "LUNCH" + case dinner = "DINNER" + + var tabIndex: Int { + switch self { + case .morning: 0 + case .lunch: 1 + case .dinner: 2 + } + } + } + + let date: Date + /// ์ฐœ ์•Œ๋ฆผ์—๋งŒ ์žˆ๋‹ค. ์—†์œผ๋ฉด(์ผ๋ฐ˜ ํ•™์‹ ์•Œ๋ฆผ) ํ˜„์žฌ ์‹œ๊ฐ ๊ธฐ์ค€ ์‹œ๊ฐ„๋Œ€๋ฅผ ๊ทธ๋Œ€๋กœ ์“ด๋‹ค + let mealTime: MealTime? + + /// ์„œ๋ฒ„ ํ‘ธ์‹œ data: `type`(MENU_FAVORITE / DAILY_MENU), `date`(yyyyMMdd), `timePart`(์ฐœ ์•Œ๋ฆผ๋งŒ) + init?(userInfo: [AnyHashable: Any]) { + guard let type = userInfo["type"] as? String, + Self.supportedTypes.contains(type), + let dateString = userInfo["date"] as? String, + let date = Self.dateFormatter.date(from: dateString) else { return nil } + self.date = date + self.mealTime = (userInfo["timePart"] as? String).flatMap(MealTime.init(rawValue:)) + } + + private static let supportedTypes: Set = ["MENU_FAVORITE", "DAILY_MENU"] + + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.calendar = Calendar(identifier: .gregorian) + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = .current + formatter.dateFormat = "yyyyMMdd" + return formatter + }() +} + +/// ์•Œ๋ฆผ์„ ๋ˆ„๋ฅธ ์‹œ์ ์— ํ™”๋ฉด์ด ์ค€๋น„๋ผ ์žˆ์ง€ ์•Š์„ ์ˆ˜ ์žˆ์–ด(์•ฑ ์‹คํ–‰ ์งํ›„ ์Šคํ”Œ๋ž˜์‹œยท๋กœ๊ทธ์ธ ๋“ฑ) ์ด๋™ํ•  ๊ณณ์„ ๋ณด๊ด€ํ–ˆ๋‹ค๊ฐ€ ์ ์šฉํ•œ๋‹ค +final class PushRouter { + + static let shared = PushRouter() + + private init() {} + + private var pendingRoute: PushRoute? + + /// ์•Œ๋ฆผ ์‘๋‹ต์„ ๋ฐ›์œผ๋ฉด ํ˜ธ์ถœ. ์•ฑ์ด ์ด๋ฏธ ํ™œ์„ฑ ์ƒํƒœ๋ฉด ๋ฐ”๋กœ, ์•„๋‹ˆ๋ฉด `sceneDidBecomeActive`์—์„œ ์ ์šฉํ•œ๋‹ค + func handle(userInfo: [AnyHashable: Any]) { + guard let route = PushRoute(userInfo: userInfo) else { return } + pendingRoute = route + if UIApplication.shared.applicationState == .active { + applyPendingRoute() + } + } + + /// ๋ณด๊ด€๋œ ์ด๋™์„ ์ ์šฉ. ํƒญ๋ฐ”๊ฐ€ ์•„์ง ์—†์œผ๋ฉด(์Šคํ”Œ๋ž˜์‹œยท๋กœ๊ทธ์ธ ์ค‘) ๊ทธ๋Œ€๋กœ ๋‘๊ณ , ํƒญ๋ฐ”๊ฐ€ ๋‚˜ํƒ€๋‚  ๋•Œ ๋‹ค์‹œ ์‹œ๋„ํ•œ๋‹ค + /// + /// ๋‹ค์Œ ๋Ÿฐ๋ฃจํ”„์—์„œ ์‹คํ–‰ํ•ด, ๊ฐ™์€ ์‹œ์ ์— ์˜ˆ์•ฝ๋œ "์ƒˆ๋กœ์šด ๋‚  โ†’ ์˜ค๋Š˜๋กœ ์ดˆ๊ธฐํ™”"(`didEnterNewDay`) ์ฒ˜๋ฆฌ๋ณด๋‹ค ๋’ค์— ์ ์šฉ๋˜๊ฒŒ ํ•œ๋‹ค + func applyPendingRoute() { + DispatchQueue.main.async { [weak self] in + guard let self, let route = self.pendingRoute, + let tabBarController = Self.currentTabBarController() else { return } + self.pendingRoute = nil + tabBarController.showHomeMenu(on: route.date, mealTime: route.mealTime) + } + } + + private static func currentTabBarController() -> CustomTabBarContainerController? { + let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene + let window = windowScene?.windows.first { $0.isKeyWindow } + return window?.rootViewController as? CustomTabBarContainerController + } +} diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/CustomTimeTabController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/CustomTimeTabController.swift index 6c36cd32..53be14ad 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/CustomTimeTabController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/CustomTimeTabController.swift @@ -25,6 +25,8 @@ final class CustomTimeTabController: BaseViewController { var todayDate: Date = .init() private var isProgrammaticScroll = false + /// ํ™”๋ฉด ๊ตฌ์„ฑ ์ „์— ์„ ํƒ๋œ ์‹œ๊ฐ„๋Œ€. ๊ตฌ์„ฑํ•  ๋•Œ ํ˜„์žฌ ์‹œ๊ฐ ๊ธฐ์ค€ ์‹œ๊ฐ„๋Œ€ ๋Œ€์‹  ์‚ฌ์šฉํ•œ๋‹ค + private var pendingIndex: Int? private var cancellables = Set() private let dateSubject = PassthroughSubject() @@ -187,7 +189,8 @@ final class CustomTimeTabController: BaseViewController { scrollView = scroll } - let initialIndex = getInitialTabIndex() + let initialIndex = pendingIndex ?? getInitialTabIndex() + pendingIndex = nil pageViewController.setViewControllers([viewControllers[initialIndex]], direction: .forward, animated: false) self.selectedIndex = initialIndex } @@ -256,6 +259,18 @@ final class CustomTimeTabController: BaseViewController { // MARK: - External API + /// ์™ธ๋ถ€์—์„œ ์‹œ๊ฐ„๋Œ€ ํƒญ ์„ ํƒ (0: ์•„์นจ, 1: ์ ์‹ฌ, 2: ์ €๋…) + func select(index: Int) { + guard viewControllers.indices.contains(index) else { return } + guard isViewLoaded else { + pendingIndex = index + return + } + guard selectedIndex != index else { return } + isProgrammaticScroll = true + selectedIndex = index + } + func updateDate(to date: Date) { todayDate = date dateSubject.send(date) diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift index 49b82306..d5516a0f 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift @@ -138,6 +138,16 @@ final class HomeViewController: BaseViewController { tabmanController.updateDate(to: today) } + /// ํ•™์‹ ์•Œ๋ฆผ์œผ๋กœ ์ง„์ž…: ํ•ด๋‹น ๋‚ ์งœ์™€ ์‹œ๊ฐ„๋Œ€(์—†์œผ๋ฉด ํ˜„์žฌ ์‹œ๊ฐ ๊ธฐ์ค€ ์‹œ๊ฐ„๋Œ€)๋กœ ์ด๋™ + func show(date: Date, mealTimeIndex: Int?) { + currentDate = date + homeCalendarView.setSelected(date: date) + tabmanController.updateDate(to: date) + if let mealTimeIndex { + tabmanController.select(index: mealTimeIndex) + } + } + // MARK: - Private Functions private func presentPromotionPopupIfNeeded() { diff --git a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift index bac90758..1ad69cc8 100644 --- a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift @@ -46,6 +46,12 @@ final class CustomTabBarContainerController: UITabBarController { ServerLanguageSync.sync() PushTokenManager.shared.sync() } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + // ์•ฑ์ด ๊บผ์ง„ ์ƒํƒœ์—์„œ ์•Œ๋ฆผ์œผ๋กœ ์‹คํ–‰๋œ ๊ฒฝ์šฐ, ์Šคํ”Œ๋ž˜์‹œยท๋กœ๊ทธ์ธ์„ ๊ฑฐ์ณ ํƒญ๋ฐ”๊ฐ€ ๋‚˜ํƒ€๋‚œ ๋’ค ์ด๋™ํ•œ๋‹ค + PushRouter.shared.applyPendingRoute() + } // MARK: - Setup @@ -135,6 +141,17 @@ final class CustomTabBarContainerController: UITabBarController { setTab(index: Tab.map.rawValue) } + /// ํ•™์‹ ์•Œ๋ฆผ โ†’ ํ•™์‹ ํƒญ์˜ ํ•ด๋‹น ๋‚ ์งœยท์‹œ๊ฐ„๋Œ€๋กœ ์ด๋™ (์ƒ์„ธ ํ™”๋ฉด์ด ์•„๋‹ˆ๋ผ ํ•™์‹ ํƒญ ์ž์ฒด) + public func showHomeMenu(on date: Date, mealTime: PushRoute.MealTime?) { + guard let navController = getNavController(at: Tab.home.rawValue), + let homeVC = navController.viewControllers.first as? HomeViewController else { return } + // ์•Œ๋ฆผ ํ™•์ธ ์ค‘ ๋–  ์žˆ๋˜ ์‹œํŠธยท์•Œ๋Ÿฟ์„ ๋‹ซ๊ณ , ํ•™์‹ ํƒญ ์œ„์— push๋œ ํ™”๋ฉด๋„ ์ •๋ฆฌํ•œ๋‹ค + presentedViewController?.dismiss(animated: false) + navController.popToRootViewController(animated: false) + selectedIndex = Tab.home.rawValue + homeVC.show(date: date, mealTimeIndex: mealTime?.tabIndex) + } + /// ํŠน์ • ์ธ๋ฑ์Šค์˜ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ public func getNavController(at index: Int) -> UINavigationController? { guard index < tabViewControllers.count else { return nil } diff --git a/EATSSU/App/Sources/Utility/Application/AppDelegate.swift b/EATSSU/App/Sources/Utility/Application/AppDelegate.swift index 60a31c39..57344a38 100644 --- a/EATSSU/App/Sources/Utility/Application/AppDelegate.swift +++ b/EATSSU/App/Sources/Utility/Application/AppDelegate.swift @@ -57,6 +57,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD LaunchSourceManager.shared.setSource(.localNotification) } + // ํ•™์‹ ์•Œ๋ฆผ์ด๋ฉด ํ•ด๋‹น ๋‚ ์งœยท์‹œ๊ฐ„๋Œ€์˜ ํ•™์‹ ํ™”๋ฉด์œผ๋กœ ์ด๋™ + PushRouter.shared.handle(userInfo: userInfo) + completionHandler() } diff --git a/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift b/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift index 8d8f03e9..87d67a6d 100644 --- a/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift +++ b/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift @@ -23,6 +23,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // ์•ฑ์˜ ์ตœ์ดˆ ์‹คํ–‰ ์†Œ์Šค๋ฅผ ํŒ๋ณ„ detectLaunchSource(connectionOptions: connectionOptions) + // ์•ฑ์ด ๊บผ์ง„ ์ƒํƒœ์—์„œ ์•Œ๋ฆผ์„ ๋ˆŒ๋Ÿฌ ์‹คํ–‰: ํƒญ๋ฐ”๊ฐ€ ์ค€๋น„๋˜๋ฉด ํ•ด๋‹น ํ•™์‹ ํ™”๋ฉด์œผ๋กœ ์ด๋™ + if let response = connectionOptions.notificationResponse { + PushRouter.shared.handle(userInfo: response.notification.request.content.userInfo) + } guard let windowScene = (scene as? UIWindowScene) else { return } window = UIWindow(windowScene: windowScene) // ์Šคํ”Œ๋ž˜์‹œ ํ™”๋ฉด ์„ค์ • @@ -67,6 +71,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // ์•ฑ์ด ์™„์ „ํžˆ ํฌ๊ทธ๋ผ์šด๋“œ์— ์ง„์ž…ํ–ˆ์„ ๋•Œ ์‹คํ–‰ ๊ฒฝ๋กœ ๋กœ๊น… ์ฒ˜๋ฆฌ handleForegroundTransition() checkAndNotifyNewDay() + // ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์•Œ๋ฆผ์„ ๋ˆŒ๋Ÿฌ ๋“ค์–ด์˜จ ๊ฒฝ์šฐ: ์ƒˆ๋กœ์šด ๋‚  ์ฒ˜๋ฆฌ(์˜ค๋Š˜๋กœ ์ดˆ๊ธฐํ™”) ๋’ค์— ์•Œ๋ฆผ์˜ ๋‚ ์งœ๋กœ ์ด๋™ํ•œ๋‹ค + PushRouter.shared.applyPendingRoute() } diff --git a/EATSSU/Tests/UnitTests/PushRouteTests.swift b/EATSSU/Tests/UnitTests/PushRouteTests.swift new file mode 100644 index 00000000..8183bedc --- /dev/null +++ b/EATSSU/Tests/UnitTests/PushRouteTests.swift @@ -0,0 +1,43 @@ +// +// PushRouteTests.swift +// EATSSUUnitTests +// +// Created by ํ™ฉ์ƒํ™˜ on 10/17/26. +// + +import XCTest + +@testable import EATSSU_DEV + +/// ์„œ๋ฒ„ ํ‘ธ์‹œ data โ†’ ํ•™์‹ ํ™”๋ฉด ์ด๋™ ์ •๋ณด ํ•ด์„ ๊ฒ€์ฆ +final class PushRouteTests: XCTestCase { + + private func components(of date: Date) -> DateComponents { + Calendar(identifier: .gregorian).dateComponents([.year, .month, .day], from: date) + } + + func test_์ฐœ_์•Œ๋ฆผ์€_๋‚ ์งœ์™€_์‹œ๊ฐ„๋Œ€๋ฅผ_์ฝ๋Š”๋‹ค() throws { + let route = try XCTUnwrap(PushRoute(userInfo: ["type": "MENU_FAVORITE", "date": "20261005", "timePart": "DINNER"])) + let date = components(of: route.date) + XCTAssertEqual([date.year, date.month, date.day], [2026, 10, 5]) + XCTAssertEqual(route.mealTime, .dinner) + XCTAssertEqual(route.mealTime?.tabIndex, 2) + } + + func test_์ผ๋ฐ˜_ํ•™์‹_์•Œ๋ฆผ์€_์‹œ๊ฐ„๋Œ€_์—†์ด_๋‚ ์งœ๋งŒ_์ฝ๋Š”๋‹ค() throws { + let route = try XCTUnwrap(PushRoute(userInfo: ["type": "DAILY_MENU", "date": "20261017"])) + XCTAssertNil(route.mealTime) + } + + func test_์•Œ_์ˆ˜_์—†๋Š”_ํƒ€์ž…์ด๋‚˜_์ž˜๋ชป๋œ_๋‚ ์งœ๋Š”_๋ฌด์‹œํ•œ๋‹ค() { + XCTAssertNil(PushRoute(userInfo: ["type": "PROMOTION", "date": "20261005"])) + XCTAssertNil(PushRoute(userInfo: ["type": "MENU_FAVORITE", "date": "2026-10-05"])) + XCTAssertNil(PushRoute(userInfo: ["type": "MENU_FAVORITE"])) + XCTAssertNil(PushRoute(userInfo: [:])) + } + + func test_์‹œ๊ฐ„๋Œ€_๊ฐ’์ด_์ด์ƒํ•˜๋ฉด_์‹œ๊ฐ„๋Œ€๋งŒ_๋น„์šด๋‹ค() throws { + let route = try XCTUnwrap(PushRoute(userInfo: ["type": "MENU_FAVORITE", "date": "20261005", "timePart": "BRUNCH"])) + XCTAssertNil(route.mealTime) + } +} From f55c630d376258422e1a3a9c1499a7353b83d409 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:22:20 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[#473]=20=EC=95=8C=EB=A6=BC=20=ED=86=A0?= =?UTF-8?q?=EA=B8=80=EC=9D=84=20=EB=B9=A0=EB=A5=B4=EA=B2=8C=20=EB=B0=94?= =?UTF-8?q?=EA=BF=94=EB=8F=84=20=ED=91=B8=EC=8B=9C=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=83=81=ED=83=9C=EA=B0=80=20=EC=96=B4?= =?UTF-8?q?=EA=B8=8B=EB=82=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=EC=9D=84=20=EC=88=9C=EC=84=9C=EB=8C=80?= =?UTF-8?q?=EB=A1=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Notification/PushTokenManager.swift | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/EATSSU/App/Sources/Notification/PushTokenManager.swift b/EATSSU/App/Sources/Notification/PushTokenManager.swift index 7c1e94fe..07764db5 100644 --- a/EATSSU/App/Sources/Notification/PushTokenManager.swift +++ b/EATSSU/App/Sources/Notification/PushTokenManager.swift @@ -32,9 +32,16 @@ final class PushTokenManager { /// ๋งˆ์ง€๋ง‰์œผ๋กœ ์„œ๋ฒ„์— ๋ฐ˜์˜ํ•œ ์ƒํƒœ. ์•ฑ ์‹คํ–‰ ์ค‘ ๊ฐ™์€ ์š”์ฒญ์„ ๋ฐ˜๋ณตํ•˜์ง€ ์•Š๊ฒŒ ํ•œ๋‹ค private var lastSynced: (token: String, isRegistered: Bool)? + /// ์š”์ฒญ ์ค‘์ธ์ง€. ๋“ฑ๋กยท์‚ญ์ œ ์š”์ฒญ์„ ํ•œ ๋ฒˆ์— ํ•˜๋‚˜๋งŒ ๋ณด๋‚ด ์„œ๋ฒ„์— ๋„์ฐฉํ•˜๋Š” ์ˆœ์„œ๊ฐ€ ๋’ค๋ฐ”๋€Œ์ง€ ์•Š๊ฒŒ ํ•œ๋‹ค + private var isSyncing = false + + /// ์š”์ฒญ ์ค‘์— ๋‹ค์‹œ ๋งž์ถฐ์•ผ ํ•  ์ผ์ด ์ƒ๊ฒผ๋Š”์ง€ (์˜ˆ: ํ† ๊ธ€์„ ๋น ๋ฅด๊ฒŒ ๊ป๋‹ค ์ผฌ). ์‘๋‹ต ํ›„ ํ•œ ๋ฒˆ ๋” ๋งž์ถ˜๋‹ค + private var needsResync = false + /// ๋กœ๊ทธ์•„์›ƒยทํƒˆํ‡ดยท์„ธ์…˜ ๋งŒ๋ฃŒ ์‹œ ํ˜ธ์ถœ. ๋‹ค์Œ ๋กœ๊ทธ์ธ ๊ณ„์ •์— ๋‹ค์‹œ ๋“ฑ๋ก๋˜๊ฒŒ ํ•œ๋‹ค func reset() { lastSynced = nil + needsResync = false } // MARK: - Sync @@ -42,13 +49,26 @@ final class PushTokenManager { /// ํ˜„์žฌ ์ƒํƒœ์— ๋งž๊ฒŒ ์„œ๋ฒ„์— ํ† ํฐ์„ ๋“ฑ๋กํ•˜๊ฑฐ๋‚˜ ์‚ญ์ œํ•œ๋‹ค /// ํ˜ธ์ถœ ์‹œ์ : ๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ์ง„์ž…, FCM ํ† ํฐ ๊ฐฑ์‹ , ์•Œ๋ฆผ ๊ถŒํ•œ ๊ฒฐ๊ณผ, ๋งˆ์ดํŽ˜์ด์ง€ ์•Œ๋ฆผ ํ† ๊ธ€ ๋ณ€๊ฒฝ func sync() { + guard Thread.isMainThread else { + DispatchQueue.main.async { self.sync() } + return + } guard TokenStore.hasAccessToken, let token = Messaging.messaging().fcmToken, !token.isEmpty else { return } + // ์ด์ „ ์š”์ฒญ์˜ ์‘๋‹ต ์ „์— ๋ฐ”๋€ ์ƒํƒœ๋Š” ์‘๋‹ต ํ›„ ๋ฐ˜์˜ํ•œ๋‹ค. ์‘๋‹ต ์ „์— ๋น„๊ตํ•˜๋ฉด ์•„์ง ๋ฐ˜์˜๋˜์ง€ ์•Š์€ ์ƒํƒœ์™€ ๋น„๊ตํ•˜๊ฒŒ ๋œ๋‹ค + guard !isSyncing else { + needsResync = true + return + } + isSyncing = true Task { @MainActor in let shouldRegister = await Self.isNotificationEnabled() - // ํ™•์ธํ•˜๋Š” ์‚ฌ์ด ๋กœ๊ทธ์•„์›ƒ๋์œผ๋ฉด ๋ณด๋‚ด์ง€ ์•Š๋Š”๋‹ค - guard TokenStore.hasAccessToken else { return } - if let lastSynced, lastSynced.token == token, lastSynced.isRegistered == shouldRegister { return } + // ํ™•์ธํ•˜๋Š” ์‚ฌ์ด ๋กœ๊ทธ์•„์›ƒ๋๊ฑฐ๋‚˜ ์ด๋ฏธ ๊ฐ™์€ ์ƒํƒœ๋ฉด ๋ณด๋‚ด์ง€ ์•Š๋Š”๋‹ค + let isAlreadySynced = lastSynced.map { $0.token == token && $0.isRegistered == shouldRegister } ?? false + guard TokenStore.hasAccessToken, !isAlreadySynced else { + finishSync() + return + } let target: PushTokenRouter = shouldRegister ? .register(token: token) : .unregister(token: token) // ์•ฑ ์ˆ˜๋ช… ๋™์•ˆ ์œ ์ง€๋˜๋Š” ์‹ฑ๊ธ€ํ„ด์ด๋ผ self๋ฅผ ๊ทธ๋Œ€๋กœ ์บก์ฒ˜ํ•œ๋‹ค @@ -60,10 +80,18 @@ final class PushTokenManager { // ๋‹ค์Œ ์ง„์ž…ยทํ† ํฐ ๊ฐฑ์‹  ๋•Œ ๋‹ค์‹œ ๋งž์ถ”๋ฏ€๋กœ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค print("ํ‘ธ์‹œ ํ† ํฐ \(shouldRegister ? "๋“ฑ๋ก" : "์‚ญ์ œ") ์‹คํŒจ: \(error.localizedDescription)") } + self.finishSync() } } } + private func finishSync() { + isSyncing = false + guard needsResync else { return } + needsResync = false + sync() + } + /// ๋กœ๊ทธ์•„์›ƒ ์ง์ „ ํ˜ธ์ถœ. ๊ณ„์ • ํ† ํฐ์ด ์ง€์›Œ์ง€๊ธฐ ์ „์— ์„œ๋ฒ„์˜ ํ‘ธ์‹œ ํ† ํฐ์„ ์‚ญ์ œํ•˜๊ณ  completion์„ ๋ถ€๋ฅธ๋‹ค. /// ์„œ๋ฒ„์— ๋กœ๊ทธ์•„์›ƒ API๊ฐ€ ์—†์–ด, ์ด ์š”์ฒญ์ด ์—†์œผ๋ฉด ๋กœ๊ทธ์•„์›ƒํ•œ ๊ธฐ๊ธฐ๋กœ ์ด์ „ ๊ณ„์ •์˜ ์•Œ๋ฆผ์ด ๊ณ„์† ๊ฐ„๋‹ค. /// ์‹คํŒจํ•˜๊ฑฐ๋‚˜ ์‘๋‹ต์ด ๋Šฆ์–ด๋„ `unregisterTimeout` ๋’ค์—๋Š” ๋กœ๊ทธ์•„์›ƒ์„ ์ง„ํ–‰ํ•œ๋‹ค. From 4a0f4a96ab5aa488e7542bd25b6ad88f8df8aea9 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:29:44 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[#473]=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=8B=9C=20=EC=A7=84=ED=96=89=20=EC=A4=91=EC=9D=B8?= =?UTF-8?q?=20=EB=93=B1=EB=A1=9D=20=EC=9A=94=EC=B2=AD=20=EB=92=A4=EC=97=90?= =?UTF-8?q?=20=ED=86=A0=ED=81=B0=EC=9D=84=20=EC=82=AD=EC=A0=9C=ED=95=98?= =?UTF-8?q?=EA=B3=A0,=20=EC=9D=B4=EC=A0=84=20=EA=B3=84=EC=A0=95=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=9D=B4=20=EB=8B=A4=EC=9D=8C=20=EA=B3=84?= =?UTF-8?q?=EC=A0=95=20=EC=83=81=ED=83=9C=EB=A5=BC=20=EB=B0=94=EA=BE=B8?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Notification/PushTokenManager.swift | 53 +++++++++++++++---- .../ViewController/MyPageViewController.swift | 4 ++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/EATSSU/App/Sources/Notification/PushTokenManager.swift b/EATSSU/App/Sources/Notification/PushTokenManager.swift index 07764db5..d7717849 100644 --- a/EATSSU/App/Sources/Notification/PushTokenManager.swift +++ b/EATSSU/App/Sources/Notification/PushTokenManager.swift @@ -38,10 +38,23 @@ final class PushTokenManager { /// ์š”์ฒญ ์ค‘์— ๋‹ค์‹œ ๋งž์ถฐ์•ผ ํ•  ์ผ์ด ์ƒ๊ฒผ๋Š”์ง€ (์˜ˆ: ํ† ๊ธ€์„ ๋น ๋ฅด๊ฒŒ ๊ป๋‹ค ์ผฌ). ์‘๋‹ต ํ›„ ํ•œ ๋ฒˆ ๋” ๋งž์ถ˜๋‹ค private var needsResync = false + /// ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ ์ค‘. ๋กœ๊ทธ์•„์›ƒ ์‚ญ์ œ ์š”์ฒญ ๋’ค์— ๋“ฑ๋ก ์š”์ฒญ์ด ๋‹ค์‹œ ๋‚˜๊ฐ€์ง€ ์•Š๊ฒŒ ๋ง‰๋Š”๋‹ค + private var isLoggingOut = false + + /// ์ง„ํ–‰ ์ค‘์ธ ์š”์ฒญ์ด ๋๋‚˜๋ฉด ์‹คํ–‰ํ•  ์ž‘์—… (๋กœ๊ทธ์•„์›ƒ ์‚ญ์ œ ์š”์ฒญ์„ ์ง„ํ–‰ ์ค‘์ธ ๋“ฑ๋ก ์š”์ฒญ ๋’ค๋กœ ๋ณด๋‚ธ๋‹ค) + private var onSyncFinished: (() -> Void)? + + /// `reset()`๋งˆ๋‹ค ์ฆ๊ฐ€. ์ดˆ๊ธฐํ™” ์ „์— ๋ณด๋‚ธ ์š”์ฒญ์˜ ์‘๋‹ต์ด ๋‹ค์Œ ๊ณ„์ •์˜ ์ƒํƒœ๋ฅผ ๋ฐ”๊พธ์ง€ ์•Š๊ฒŒ ํ•œ๋‹ค + private var sessionVersion = 0 + /// ๋กœ๊ทธ์•„์›ƒยทํƒˆํ‡ดยท์„ธ์…˜ ๋งŒ๋ฃŒ ์‹œ ํ˜ธ์ถœ. ๋‹ค์Œ ๋กœ๊ทธ์ธ ๊ณ„์ •์— ๋‹ค์‹œ ๋“ฑ๋ก๋˜๊ฒŒ ํ•œ๋‹ค func reset() { + sessionVersion += 1 lastSynced = nil + isSyncing = false needsResync = false + isLoggingOut = false + onSyncFinished = nil } // MARK: - Sync @@ -53,19 +66,24 @@ final class PushTokenManager { DispatchQueue.main.async { self.sync() } return } - guard TokenStore.hasAccessToken, let token = Messaging.messaging().fcmToken, !token.isEmpty else { return } + guard !isLoggingOut, + TokenStore.hasAccessToken, + let token = Messaging.messaging().fcmToken, !token.isEmpty else { return } // ์ด์ „ ์š”์ฒญ์˜ ์‘๋‹ต ์ „์— ๋ฐ”๋€ ์ƒํƒœ๋Š” ์‘๋‹ต ํ›„ ๋ฐ˜์˜ํ•œ๋‹ค. ์‘๋‹ต ์ „์— ๋น„๊ตํ•˜๋ฉด ์•„์ง ๋ฐ˜์˜๋˜์ง€ ์•Š์€ ์ƒํƒœ์™€ ๋น„๊ตํ•˜๊ฒŒ ๋œ๋‹ค guard !isSyncing else { needsResync = true return } isSyncing = true + let session = sessionVersion Task { @MainActor in let shouldRegister = await Self.isNotificationEnabled() - // ํ™•์ธํ•˜๋Š” ์‚ฌ์ด ๋กœ๊ทธ์•„์›ƒ๋๊ฑฐ๋‚˜ ์ด๋ฏธ ๊ฐ™์€ ์ƒํƒœ๋ฉด ๋ณด๋‚ด์ง€ ์•Š๋Š”๋‹ค + // ํ™•์ธํ•˜๋Š” ์‚ฌ์ด ๊ณ„์ •์ด ์ดˆ๊ธฐํ™”๋์œผ๋ฉด ์ด ์š”์ฒญ์€ ์ด์ „ ๊ณ„์ •์˜ ๊ฒƒ์ด๋ฏ€๋กœ ๋ฒ„๋ฆฐ๋‹ค + guard session == sessionVersion else { return } + // ๋กœ๊ทธ์•„์›ƒ์ด ์‹œ์ž‘๋๊ฑฐ๋‚˜ ์ด๋ฏธ ๊ฐ™์€ ์ƒํƒœ๋ฉด ๋ณด๋‚ด์ง€ ์•Š๋Š”๋‹ค let isAlreadySynced = lastSynced.map { $0.token == token && $0.isRegistered == shouldRegister } ?? false - guard TokenStore.hasAccessToken, !isAlreadySynced else { + guard !isLoggingOut, TokenStore.hasAccessToken, !isAlreadySynced else { finishSync() return } @@ -73,6 +91,7 @@ final class PushTokenManager { let target: PushTokenRouter = shouldRegister ? .register(token: token) : .unregister(token: token) // ์•ฑ ์ˆ˜๋ช… ๋™์•ˆ ์œ ์ง€๋˜๋Š” ์‹ฑ๊ธ€ํ„ด์ด๋ผ self๋ฅผ ๊ทธ๋Œ€๋กœ ์บก์ฒ˜ํ•œ๋‹ค NetworkService.shared.request(target, responseType: Bool.self, useAuth: true) { result in + guard session == self.sessionVersion else { return } switch result { case .success: self.lastSynced = (token, shouldRegister) @@ -87,6 +106,11 @@ final class PushTokenManager { private func finishSync() { isSyncing = false + if let onSyncFinished { + self.onSyncFinished = nil + onSyncFinished() + return + } guard needsResync else { return } needsResync = false sync() @@ -94,12 +118,16 @@ final class PushTokenManager { /// ๋กœ๊ทธ์•„์›ƒ ์ง์ „ ํ˜ธ์ถœ. ๊ณ„์ • ํ† ํฐ์ด ์ง€์›Œ์ง€๊ธฐ ์ „์— ์„œ๋ฒ„์˜ ํ‘ธ์‹œ ํ† ํฐ์„ ์‚ญ์ œํ•˜๊ณ  completion์„ ๋ถ€๋ฅธ๋‹ค. /// ์„œ๋ฒ„์— ๋กœ๊ทธ์•„์›ƒ API๊ฐ€ ์—†์–ด, ์ด ์š”์ฒญ์ด ์—†์œผ๋ฉด ๋กœ๊ทธ์•„์›ƒํ•œ ๊ธฐ๊ธฐ๋กœ ์ด์ „ ๊ณ„์ •์˜ ์•Œ๋ฆผ์ด ๊ณ„์† ๊ฐ„๋‹ค. - /// ์‹คํŒจํ•˜๊ฑฐ๋‚˜ ์‘๋‹ต์ด ๋Šฆ์–ด๋„ `unregisterTimeout` ๋’ค์—๋Š” ๋กœ๊ทธ์•„์›ƒ์„ ์ง„ํ–‰ํ•œ๋‹ค. + /// + /// - ์ง„ํ–‰ ์ค‘์ธ ๋“ฑ๋ก ์š”์ฒญ์ด ์žˆ์œผ๋ฉด ๊ทธ ์‘๋‹ต ๋’ค์— ์‚ญ์ œ๋ฅผ ๋ณด๋‚ด, ์„œ๋ฒ„์— ๋“ฑ๋ก์ด ์‚ญ์ œ๋ณด๋‹ค ๋Šฆ๊ฒŒ ๋„์ฐฉํ•˜์ง€ ์•Š๊ฒŒ ํ•œ๋‹ค + /// - ์‹คํŒจํ•˜๊ฑฐ๋‚˜ ์‘๋‹ต์ด ๋Šฆ์–ด๋„ `unregisterTimeout` ๋’ค์—๋Š” ๋กœ๊ทธ์•„์›ƒ์„ ์ง„ํ–‰ํ•œ๋‹ค func unregisterBeforeLogout(completion: @escaping () -> Void) { guard TokenStore.hasAccessToken, let token = Messaging.messaging().fcmToken, !token.isEmpty else { completion() return } + isLoggingOut = true + needsResync = false var didComplete = false let finish = { @@ -107,14 +135,21 @@ final class PushTokenManager { didComplete = true completion() } + DispatchQueue.main.asyncAfter(deadline: .now() + Self.unregisterTimeout, execute: finish) - NetworkService.shared.request(PushTokenRouter.unregister(token: token), responseType: Bool.self, useAuth: true) { result in - if case .failure(let error) = result { - print("๋กœ๊ทธ์•„์›ƒ ํ‘ธ์‹œ ํ† ํฐ ์‚ญ์ œ ์‹คํŒจ: \(error.localizedDescription)") + let unregister = { + NetworkService.shared.request(PushTokenRouter.unregister(token: token), responseType: Bool.self, useAuth: true) { result in + if case .failure(let error) = result { + print("๋กœ๊ทธ์•„์›ƒ ํ‘ธ์‹œ ํ† ํฐ ์‚ญ์ œ ์‹คํŒจ: \(error.localizedDescription)") + } + finish() } - finish() } - DispatchQueue.main.asyncAfter(deadline: .now() + Self.unregisterTimeout, execute: finish) + if isSyncing { + onSyncFinished = unregister + } else { + unregister() + } } // MARK: - Private diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift index c3c7b0f1..9039bf70 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift @@ -115,7 +115,11 @@ final class MyPageViewController: BaseViewController { style: .default, handler: { _ in // ์„œ๋ฒ„์— ๋กœ๊ทธ์•„์›ƒ API๊ฐ€ ์—†์–ด, ๊ณ„์ • ํ† ํฐ์„ ์ง€์šฐ๊ธฐ ์ „์— ์ด ๊ธฐ๊ธฐ์˜ ํ‘ธ์‹œ ํ† ํฐ์„ ์‚ญ์ œํ•ด์•ผ ์•Œ๋ฆผ์ด ๋ฉˆ์ถ˜๋‹ค + // ์‚ญ์ œ ์‘๋‹ต์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋™์•ˆ(์ตœ๋Œ€ 3์ดˆ) ๋‹ค๋ฅธ ํ™”๋ฉด ์กฐ์ž‘์œผ๋กœ ์ž‘์—…์ด ์‹œ์ž‘๋˜์ง€ ์•Š๊ฒŒ ํ„ฐ์น˜๋ฅผ ๋ง‰๋Š”๋‹ค + let window = self.view.window + window?.isUserInteractionEnabled = false PushTokenManager.shared.unregisterBeforeLogout { + window?.isUserInteractionEnabled = true AnalyticsIdentityManager.reset() AccountStorage.reset() From 2b89d4492a55dcda543a3d2b86bb85f8046dd702 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:29:44 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[#473]=20=EC=9D=BC=EB=B0=98=20=ED=95=99?= =?UTF-8?q?=EC=8B=9D=20=EC=95=8C=EB=A6=BC=EC=9D=80=20=ED=98=84=EC=9E=AC=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=EB=8C=80=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=ED=95=98=EA=B3=A0,=20=EC=98=A4=EB=9E=98=EB=90=9C=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=9D=B4=EB=8F=99=EA=B3=BC=20=EB=96=A0=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Sources/Notification/PushRoute.swift | 21 ++++++++++++------- .../CustomTimeTabController.swift | 5 +++++ .../ViewController/HomeViewController.swift | 4 +--- .../CustomTabBarContainerController.swift | 3 ++- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/EATSSU/App/Sources/Notification/PushRoute.swift b/EATSSU/App/Sources/Notification/PushRoute.swift index 4565b030..3949ad97 100644 --- a/EATSSU/App/Sources/Notification/PushRoute.swift +++ b/EATSSU/App/Sources/Notification/PushRoute.swift @@ -8,9 +8,9 @@ import UIKit /// ํ•™์‹ ์•Œ๋ฆผ์„ ๋ˆŒ๋ €์„ ๋•Œ ์ด๋™ํ•  ํ•™์‹ ํ™”๋ฉด (๋‚ ์งœ + ์‹œ๊ฐ„๋Œ€) -struct PushRoute: Equatable { +struct PushRoute { /// ํ•™์‹ ํƒญ ์‹œ๊ฐ„๋Œ€ ์ˆœ์„œ (์•„์นจยท์ ์‹ฌยท์ €๋…) - enum MealTime: String, CaseIterable { + enum MealTime: String { case morning = "MORNING" case lunch = "LUNCH" case dinner = "DINNER" @@ -57,12 +57,15 @@ final class PushRouter { private init() {} - private var pendingRoute: PushRoute? + private var pendingRoute: (route: PushRoute, receivedAt: Date)? + + /// ๋ณด๊ด€ํ•œ ์ด๋™์˜ ์œ ํšจ ์‹œ๊ฐ„. ๋กœ๊ทธ์ธ ํ™”๋ฉด์—์„œ ์•Œ๋ฆผ์„ ๋ˆ„๋ฅธ ๋’ค ํ•œ์ฐธ ์ง€๋‚˜ ๋กœ๊ทธ์ธํ•˜๋ฉด ์˜ˆ์ „ ๋‚ ์งœ๋กœ ํŠ€์ง€ ์•Š๊ฒŒ ํ•œ๋‹ค + private static let pendingRouteLifetime: TimeInterval = 5 * 60 /// ์•Œ๋ฆผ ์‘๋‹ต์„ ๋ฐ›์œผ๋ฉด ํ˜ธ์ถœ. ์•ฑ์ด ์ด๋ฏธ ํ™œ์„ฑ ์ƒํƒœ๋ฉด ๋ฐ”๋กœ, ์•„๋‹ˆ๋ฉด `sceneDidBecomeActive`์—์„œ ์ ์šฉํ•œ๋‹ค func handle(userInfo: [AnyHashable: Any]) { guard let route = PushRoute(userInfo: userInfo) else { return } - pendingRoute = route + pendingRoute = (route, Date()) if UIApplication.shared.applicationState == .active { applyPendingRoute() } @@ -73,10 +76,14 @@ final class PushRouter { /// ๋‹ค์Œ ๋Ÿฐ๋ฃจํ”„์—์„œ ์‹คํ–‰ํ•ด, ๊ฐ™์€ ์‹œ์ ์— ์˜ˆ์•ฝ๋œ "์ƒˆ๋กœ์šด ๋‚  โ†’ ์˜ค๋Š˜๋กœ ์ดˆ๊ธฐํ™”"(`didEnterNewDay`) ์ฒ˜๋ฆฌ๋ณด๋‹ค ๋’ค์— ์ ์šฉ๋˜๊ฒŒ ํ•œ๋‹ค func applyPendingRoute() { DispatchQueue.main.async { [weak self] in - guard let self, let route = self.pendingRoute, - let tabBarController = Self.currentTabBarController() else { return } + guard let self, let pending = self.pendingRoute else { return } + guard Date().timeIntervalSince(pending.receivedAt) < Self.pendingRouteLifetime else { + self.pendingRoute = nil + return + } + guard let tabBarController = Self.currentTabBarController() else { return } self.pendingRoute = nil - tabBarController.showHomeMenu(on: route.date, mealTime: route.mealTime) + tabBarController.showHomeMenu(on: pending.route.date, mealTime: pending.route.mealTime) } } diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/CustomTimeTabController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/CustomTimeTabController.swift index 53be14ad..6a1e7ba0 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/CustomTimeTabController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/CustomTimeTabController.swift @@ -259,6 +259,11 @@ final class CustomTimeTabController: BaseViewController { // MARK: - External API + /// ํ˜„์žฌ ์‹œ๊ฐ ๊ธฐ์ค€ ์‹œ๊ฐ„๋Œ€ ํƒญ (0: ์•„์นจ, 1: ์ ์‹ฌ, 2: ์ €๋…) + var currentTimeIndex: Int { + getInitialTabIndex() + } + /// ์™ธ๋ถ€์—์„œ ์‹œ๊ฐ„๋Œ€ ํƒญ ์„ ํƒ (0: ์•„์นจ, 1: ์ ์‹ฌ, 2: ์ €๋…) func select(index: Int) { guard viewControllers.indices.contains(index) else { return } diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift index d5516a0f..77a63b42 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift @@ -143,9 +143,7 @@ final class HomeViewController: BaseViewController { currentDate = date homeCalendarView.setSelected(date: date) tabmanController.updateDate(to: date) - if let mealTimeIndex { - tabmanController.select(index: mealTimeIndex) - } + tabmanController.select(index: mealTimeIndex ?? tabmanController.currentTimeIndex) } // MARK: - Private Functions diff --git a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift index 1ad69cc8..e64849bf 100644 --- a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift @@ -145,8 +145,9 @@ final class CustomTabBarContainerController: UITabBarController { public func showHomeMenu(on date: Date, mealTime: PushRoute.MealTime?) { guard let navController = getNavController(at: Tab.home.rawValue), let homeVC = navController.viewControllers.first as? HomeViewController else { return } - // ์•Œ๋ฆผ ํ™•์ธ ์ค‘ ๋–  ์žˆ๋˜ ์‹œํŠธยท์•Œ๋Ÿฟ์„ ๋‹ซ๊ณ , ํ•™์‹ ํƒญ ์œ„์— push๋œ ํ™”๋ฉด๋„ ์ •๋ฆฌํ•œ๋‹ค + // ์•Œ๋ฆผ ํ™•์ธ ์ค‘ ๋–  ์žˆ๋˜ ์‹œํŠธยท์•Œ๋Ÿฟยท๋‹ค์ด์–ผ๋กœ๊ทธ๋ฅผ ๋‹ซ๊ณ , ํ•™์‹ ํƒญ ์œ„์— push๋œ ํ™”๋ฉด๋„ ์ •๋ฆฌํ•œ๋‹ค presentedViewController?.dismiss(animated: false) + view.subviews.filter { $0 is EATSSUDialogView }.forEach { $0.removeFromSuperview() } navController.popToRootViewController(animated: false) selectedIndex = Tab.home.rawValue homeVC.show(date: date, mealTimeIndex: mealTime?.tabIndex)