diff --git a/EATSSU/App/Sources/Data/Network/Foundation/TokenManager.swift b/EATSSU/App/Sources/Data/Network/Foundation/TokenManager.swift index 88ea20bc..b237d4c6 100644 --- a/EATSSU/App/Sources/Data/Network/Foundation/TokenManager.swift +++ b/EATSSU/App/Sources/Data/Network/Foundation/TokenManager.swift @@ -11,7 +11,7 @@ struct TokenPayload: Decodable { let exp: TimeInterval } -final class TokenManager { +class TokenManager { static let shared = TokenManager() private init() {} @@ -41,7 +41,7 @@ final class TokenManager { } /// JWT Payload 디코딩 - private func decodePayload(token: String) -> TokenPayload? { + func decodePayload(token: String) -> TokenPayload? { let parts = token.split(separator: ".") guard parts.count == 3 else { return nil } diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift index 2d87980b..5d5aac0e 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift @@ -18,7 +18,6 @@ final class LoginViewController: BaseViewController { // MARK: - Properties public static let isVacationPeriod = false - public var toastMessage: String? private let authProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) private let myProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) @@ -28,10 +27,11 @@ final class LoginViewController: BaseViewController { // MARK: - Life Cycle - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - handleAutoLogin() - } +// override func viewWillAppear(_ animated: Bool) { +// super.viewWillAppear(animated) +// showToastMessageIfNeeded() +//// handleAutoLogin() +// } override func viewDidLoad() { super.viewDidLoad() @@ -80,16 +80,7 @@ final class LoginViewController: BaseViewController { Analytics.logEvent("LoginViewControllerLoad", parameters: nil) #endif } - - /// Realm에 저장된 토큰이 있는지 확인 후, 있으면 홈 화면으로 이동한다. - private func handleAutoLogin() { - guard hasStoredToken() else { return } - #if DEBUG - print("저장된 AccessToken: ", RealmService.shared.getToken()) - #endif - changeIntoHomeViewController() - } - + private func hasStoredToken() -> Bool { !RealmService.shared.getToken().isEmpty } @@ -108,18 +99,25 @@ final class LoginViewController: BaseViewController { /// 닉네임 설정이 필요한지 확인 후, 필요하면 닉네임 설정 화면으로, 아니면 홈 화면으로 이동한다. private func handleNicknameCheck(info: MyInfoResponse) { if let nickname = info.nickname { - // 사용자의 닉네임을 업데이트하고 홈 화면으로 이동 - if let currentUserInfo = UserInfoManager.shared.getCurrentUserInfo() { - UserInfoManager.shared.updateNickname(for: currentUserInfo, nickname: nickname) + print("[디버깅] 닉네임 존재함: \(nickname)") + + let currentUser = UserInfoManager.shared.getCurrentUserInfo() + print("[디버깅] currentUserInfo: \(String(describing: currentUser))") + + if let u = currentUser { + UserInfoManager.shared.updateNickname(for: u, nickname: nickname) + print("[디버깅] 닉네임 업데이트 완료") + } else { + print("[디버깅] currentUserInfo가 nil임") } - changeIntoHomeViewController() } else { - // 닉네임 설정이 필요한 경우 + print("[디버깅] 닉네임이 없음 → 닉네임 설정 화면으로 이동") let setNicknameVC = SetNickNameViewController() navigationController?.pushViewController(setNicknameVC, animated: true) } } + /// 토큰을 Realm에 저장하고, 디버깅 로그를 출력한다. private func storeTokensAndPrintDebugLogs(accessToken: String, refreshToken: String) { RealmService.shared.addToken(accessToken: accessToken, refreshToken: refreshToken) @@ -127,11 +125,7 @@ final class LoginViewController: BaseViewController { print("⭐️⭐️ 토큰 저장 성공 ⭐️⭐️") #endif } - - private func showToastMessageIfNeeded() { - guard let toastMessage = self.toastMessage else { return } - view.showToast(message: toastMessage) - } + // MARK: - 액션 메서드 @@ -192,14 +186,15 @@ extension LoginViewController { let accessToken = data.accessToken let refreshToken = data.refreshToken - // 토큰을 로컬에 저장 + // 1) 토큰 저장 storeTokensAndPrintDebugLogs(accessToken: accessToken, refreshToken: refreshToken) - // 로컬 매니저에 유저 정보 생성 + // 2) 인증 상태 업데이트 (SceneDelegate.observeAuthState가 화면 전환 처리) + AuthService.shared.login(accessToken: accessToken, refreshToken: refreshToken) // + + // 3) 로컬 매니저에 유저 정보 생성 _ = UserInfoManager.shared.createUserInfo(accountType: accountType) - // 닉네임 등 정보를 확인하기 위해 프로필 조회 - getMyInfo() } catch { switch accountType { case .apple: @@ -255,27 +250,6 @@ extension LoginViewController { } } } - - /// 서버에서 현재 유저 정보를 조회 - private func getMyInfo() { - myProvider.request(.myInfo) { [weak self] result in - guard let self else { return } - switch result { - case let .success(moyaResponse): - do { - let responseData = try moyaResponse.map(BaseResponse.self) - guard let responseData = responseData.result else { - return - } - handleNicknameCheck(info: responseData) - } catch { - print(error.localizedDescription) - } - case let .failure(error): - print(error.localizedDescription) - } - } - } } // MARK: - 카카오 사용자 정보 가져오기 diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift index 119db2fb..94bcca62 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift @@ -105,17 +105,6 @@ final class SetNickNameViewController: BaseViewController { currentKeyboardHeight = 0.0 } } - - private func navigateToLogin() { - let loginVC = LoginViewController() - loginVC.toastMessage = "세션이 만료되었습니다. 다시 로그인해주세요." - DispatchQueue.main.async { - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) { - keyWindow.replaceRootViewController(UINavigationController(rootViewController: loginVC)) - } - } - } } // MARK: - Network @@ -129,23 +118,17 @@ extension SetNickNameViewController { UserInfoManager.shared.updateNickname(for: currentUserInfo, nickname: nickname) } self.showAlertController(title: "완료", message: "닉네임 설정이 완료되었습니다.", style: .cancel) { - if let myPageViewController = self.navigationController?.viewControllers.first(where: { $0 is MyPageViewController }) { - self.navigationController?.popToViewController(myPageViewController, animated: true) - } else { - let homeViewController = HomeViewController() - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) - { - keyWindow.replaceRootViewController(UINavigationController(rootViewController: homeViewController)) - } - } + // 인증 상태만 업데이트 → SceneDelegate.observeAuthState()가 Home으로 전환 + let at = RealmService.shared.getToken() + let rt = RealmService.shared.getRefreshToken() + AuthService.shared.login(accessToken: at, refreshToken: rt) } case let .failure(err): print(err.localizedDescription) - + RealmService.shared.resetDB() - self.navigateToLogin() + AuthService.shared.logout(message: "세션이 만료되었습니다. 다시 로그인해주세요.") } } } @@ -174,13 +157,13 @@ extension SetNickNameViewController { print(err.localizedDescription) RealmService.shared.resetDB() - self.navigateToLogin() + AuthService.shared.logout(message: "세션이 만료되었습니다. 다시 로그인해주세요.") } case let .failure(err): print(err.localizedDescription) RealmService.shared.resetDB() - self.navigateToLogin() + AuthService.shared.logout(message: "세션이 만료되었습니다. 다시 로그인해주세요.") } } } diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift index ca7f787e..f24c6b3b 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift @@ -101,7 +101,7 @@ final class HomeViewController: BaseViewController { @objc private func didTapRightBarButton() { - if RealmService.shared.isAccessTokenPresent() { + if AuthService.shared.isTokenValid() { navigateToMyPage() } else { presentLoginAlert() @@ -114,16 +114,19 @@ final class HomeViewController: BaseViewController { } private func presentLoginAlert() { - let alert = UIAlertController(title: "로그인이 필요한 서비스입니다", - message: "로그인 하시겠습니까?", - preferredStyle: .alert) - let confirmAction = UIAlertAction(title: "확인", style: .default) { [weak self] _ in - self?.navigateToLogin() + let alert = UIAlertController( + title: "로그인이 필요한 서비스입니다", + message: "로그인 하시겠습니까?", + preferredStyle: .alert + ) + let confirm = UIAlertAction(title: "확인", style: .default) { _ in + AuthService.shared.logout(message: nil) + self.navigateToLogin() } - let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil) - alert.addAction(confirmAction) - alert.addAction(cancelAction) - present(alert, animated: true, completion: nil) + let cancel = UIAlertAction(title: "취소", style: .cancel) + alert.addAction(confirm) + alert.addAction(cancel) + present(alert, animated: true) } private func navigateToLogin() { diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift index 1e6a8475..01fc4264 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift @@ -41,8 +41,9 @@ final class MyPageViewController: BaseViewController { nickName = UserInfoManager.shared.getCurrentUserInfo()?.nickname ?? "실패" mypageView.setUserInfo(nickname: nickName) + showToastMessageIfNeeded() } - + // MARK: - Functions override func setCustomNavigationBar() { @@ -97,26 +98,17 @@ final class MyPageViewController: BaseViewController { /// 로그아웃 Alert를 스크린에 표시하는 메소드 private func logoutShowAlert() { - let alert = UIAlertController(title: "로그아웃", - message: "정말 로그아웃 하시겠습니까?", - preferredStyle: UIAlertController.Style.alert) - - let cancelAction = UIAlertAction(title: "취소하기", - style: .default, - handler: nil) - - let fixAction = UIAlertAction(title: "로그아웃", - style: .default, - handler: { _ in - RealmService.shared.resetDB() - - 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 alert = UIAlertController( + title: "로그아웃", + message: "정말 로그아웃 하시겠습니까?", + preferredStyle: .alert + ) + + let cancelAction = UIAlertAction(title: "취소하기", style: .default, handler: nil) + + let fixAction = UIAlertAction(title: "로그아웃", style: .default) { _ in + AuthService.shared.logout(message: nil) + } alert.addAction(cancelAction) alert.addAction(fixAction) diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift index 4177f211..c9d14468 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift @@ -120,17 +120,6 @@ final class MyReviewViewController: BaseViewController { noMyReviewImageView.isHidden = true } } - - private func navigateToLogin() { - let loginVC = LoginViewController() - loginVC.toastMessage = "세션이 만료되었습니다. 다시 로그인해주세요." - DispatchQueue.main.async { - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) { - keyWindow.replaceRootViewController(UINavigationController(rootViewController: loginVC)) - } - } - } } extension MyReviewViewController: UITableViewDelegate {} @@ -171,13 +160,13 @@ extension MyReviewViewController { print(err.localizedDescription) RealmService.shared.resetDB() - self.navigateToLogin() + AuthService.shared.logout(message: "세션이 만료되었습니다. 다시 로그인해주세요.") } case let .failure(err): print(err.localizedDescription) RealmService.shared.resetDB() - self.navigateToLogin() + AuthService.shared.logout(message: "세션이 만료되었습니다. 다시 로그인해주세요.") } } } @@ -194,7 +183,7 @@ extension MyReviewViewController { print(err.localizedDescription) RealmService.shared.resetDB() - self.navigateToLogin() + AuthService.shared.logout(message: "세션이 만료되었습니다. 다시 로그인해주세요.") } } }) diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/UserWithdrawViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/UserWithdrawViewController.swift index 272d0025..9a8677ae 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/UserWithdrawViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/UserWithdrawViewController.swift @@ -129,15 +129,9 @@ extension UserWithdrawViewController { do { let responseData = try moyaResponse.map(BaseResponse.self) guard let data = responseData.result, data else { return } - + RealmService.shared.resetDB() - let loginViewController = LoginViewController() - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) - { - loginViewController.toastMessage = "탈퇴 처리가 완료되었습니다." - keyWindow.replaceRootViewController(UINavigationController(rootViewController: loginViewController)) - } + AuthService.shared.logout(message: "탈퇴 처리가 완료되었습니다.") } catch let err { print(err.localizedDescription) } diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index f898af20..6cdcc285 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -189,50 +189,64 @@ final class ReviewViewController: BaseViewController { // @objc func userTapReviewButton() { - if RealmService.shared.isAccessTokenPresent() { + _Concurrency.Task { + let valid = await AuthService.shared.checkAndRefreshTokenIfNeeded() + if !valid { + let alert = UIAlertController( + title: "로그인이 필요한 서비스입니다", + message: "로그인 하시겠습니까?", + preferredStyle: .alert + ) + let confirm = UIAlertAction(title: "확인", style: .default) { _ in + AuthService.shared.logout(message: nil) + self.navigateToLogin() + } + let cancel = UIAlertAction(title: "취소", style: .cancel) + alert.addAction(confirm) + alert.addAction(cancel) + self.present(alert, animated: true) + return + } + // 토큰 유효 시 기존 리뷰 작성 로직 activityIndicatorView.isHidden = false - DispatchQueue.global().async { // 백그라운드 스레드에서 작업을 수행 - // 작업 완료 후 UI 업데이트를 메인 스레드에서 수행 - DispatchQueue.main.async { [self] in - // 고정메뉴인지 판별(메뉴 ID List에 nil값 들어옴) - if menuIDList == nil { - let setRateViewController = SetRateViewController() - menuIDList = [menuID] - setRateViewController.dataBind(list: menuNameList, - idList: menuIDList ?? [], - reviewList: nil, - currentPage: 0) - activityIndicatorView.stopAnimating() - navigationController?.pushViewController(setRateViewController, animated: true) + DispatchQueue.global().async { + DispatchQueue.main.async { + if self.menuIDList == nil { + let vc = SetRateViewController() + self.menuIDList = [self.menuID] + vc.dataBind(list: self.menuNameList, + idList: self.menuIDList ?? [], + reviewList: nil, + currentPage: 0) + self.activityIndicatorView.stopAnimating() + self.navigationController?.pushViewController(vc, animated: true) + } else if self.menuIDList?.count == 1 { + let vc = SetRateViewController() + vc.dataBind(list: self.menuNameList, + idList: self.menuIDList ?? [], + reviewList: nil, + currentPage: 0) + self.activityIndicatorView.stopAnimating() + self.navigationController?.pushViewController(vc, animated: true) } else { - // 고정메뉴이고, 메뉴가 1개일때 선택창으로 안가고 바로 작성창으로 가도록 - if menuIDList?.count == 1 { - let setRateViewController = SetRateViewController() - setRateViewController.dataBind(list: menuNameList, - idList: menuIDList ?? [], - reviewList: nil, - currentPage: 0) - activityIndicatorView.stopAnimating() - navigationController?.pushViewController(setRateViewController, animated: true) - } else { - let choiceMenuViewController = ChoiceMenuViewController() - choiceMenuViewController.menuDataBind(menuList: menuNameList, idList: menuIDList ?? []) - activityIndicatorView.stopAnimating() - navigationController?.pushViewController(choiceMenuViewController, animated: true) - } + let vc = ChoiceMenuViewController() + vc.menuDataBind(menuList: self.menuNameList, idList: self.menuIDList ?? []) + self.activityIndicatorView.stopAnimating() + self.navigationController?.pushViewController(vc, animated: true) } } } - } else { - showAlertControllerWithCancel(title: "로그인이 필요한 서비스입니다", message: "로그인 하시겠습니까?", confirmStyle: .default) { - self.pushToLoginVC() - } } } - private func pushToLoginVC() { + private func navigateToLogin() { let loginVC = LoginViewController() - navigationController?.pushViewController(loginVC, animated: true) + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let sceneDelegate = windowScene.delegate as? SceneDelegate, + let window = sceneDelegate.window + { + window.replaceRootViewController(loginVC) + } } func makeDictionary() { diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 841f466a..a47a4d78 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -500,7 +500,7 @@ extension SetRateViewController { debugPrint(err.localizedDescription) RealmService.shared.resetDB() - self.navigateToLogin() + AuthService.shared.logout(message: "세션이 만료되었습니다. 다시 로그인해주세요.") } } } @@ -516,18 +516,7 @@ extension SetRateViewController { print(err.localizedDescription) RealmService.shared.resetDB() - self.navigateToLogin() - } - } - } - - private func navigateToLogin() { - let loginVC = LoginViewController() - loginVC.toastMessage = "세션이 만료되었습니다. 다시 로그인해주세요." - DispatchQueue.main.async { - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) { - keyWindow.replaceRootViewController(UINavigationController(rootViewController: loginVC)) + AuthService.shared.logout(message: "세션이 만료되었습니다. 다시 로그인해주세요.") } } } diff --git a/EATSSU/App/Sources/Services/AuthService.swift b/EATSSU/App/Sources/Services/AuthService.swift new file mode 100644 index 00000000..c59ae144 --- /dev/null +++ b/EATSSU/App/Sources/Services/AuthService.swift @@ -0,0 +1,90 @@ +// +// AuthService.swift +// EATSSU-DEV +// +// Created by 황상환 on 8/5/25. + +import Foundation + +import RxSwift +import RxRelay + +/// 인증 상태 및 토큰 관리 서비스 +final class AuthService { + static let shared = AuthService() + private let disposeBag = DisposeBag() + private let relay = BehaviorRelay(value: false) + private let logoutMessageRelay = BehaviorRelay(value: nil) + + var isAuthenticated: Observable { + relay.asObservable() + } + + private init() { + let hasToken = isTokenValid() + relay.accept(hasToken) + } + + func login(accessToken: String, refreshToken: String) { + print("[AuthService] login() 호출됨") + RealmService.shared.addToken(accessToken: accessToken, refreshToken: refreshToken) + logoutMessageRelay.accept(nil) + relay.accept(false) + relay.accept(true) + } + + func logout(message: String? = nil) { + print("[AuthService] logout() 호출됨") + RealmService.shared.deleteAll(Token.self) + + if let message = message { + logoutMessageRelay.accept(message) + } else { + logoutMessageRelay.accept(nil) + } + relay.accept(false) + } + + var logoutMessage: Observable { + logoutMessageRelay.asObservable() + } + + func isTokenValid() -> Bool { + let token = RealmService.shared.getToken() + guard let payload = TokenManager.shared.decodePayload(token: token) else { + logout() + print("[AuthService] 디코딩 실패") + return false + } + print("[AuthService] exp: \(payload.exp), now: \(Date().timeIntervalSince1970)") + return Date(timeIntervalSince1970: payload.exp) > Date() + } + + func checkAndRefreshTokenIfNeeded() async -> Bool { + print("[AuthService] checkAndRefreshTokenIfNeeded() 시작") + + let token = RealmService.shared.getToken() + guard let payload = TokenManager.shared.decodePayload(token: token) else { + logout() + print("[AuthService] 디코딩 실패") + return false + } + print("[AuthService] exp: \(payload.exp), now: \(Date().timeIntervalSince1970)") + + // 만료됐어도 isTokenExpiringSoon()이 true이므로 재발급 시도 + if TokenManager.shared.isTokenExpiringSoon() { + do { + try await TokenRefresher.shared.refreshIfNeeded() + print("[AuthService] 토큰 재발급 성공") + } catch { + logout(message: "세션이 만료되었습니다. 다시 로그인해주세요.") + print("[AuthService] 토큰 재발급 실패") + return false + } + } + + // 재발급 성공 또는 아직 유효하다면 로그인 상태로 전환 + relay.accept(true) + return true + } +} diff --git a/EATSSU/App/Sources/Services/UserInfoService.swift b/EATSSU/App/Sources/Services/UserInfoService.swift new file mode 100644 index 00000000..80d397b2 --- /dev/null +++ b/EATSSU/App/Sources/Services/UserInfoService.swift @@ -0,0 +1,69 @@ +// +// UserInfoService.swift +// EATSSU +// +// Created by 황상환 on 8/6/25. +// + +import Foundation +import Moya +import RealmSwift + +final class UserInfoService { + static let shared = UserInfoService() + + private let provider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) + + private init() {} + + /// 서버에서 유저 정보를 조회하고, nickname이 있으면 UserInfoManager에 저장 + func fetchAndUpdateUserInfo(completion: (() -> Void)? = nil) { + print("[UserInfoService] 유저 정보 조회 시작") + + provider.request(.myInfo) { result in + switch result { + case let .success(response): + do { + let responseData = try response.map(BaseResponse.self) + guard let data = responseData.result else { + print("[UserInfoService] result가 nil임") + return + } + + if let nickname = data.nickname { + print("[UserInfoService] 닉네임: \(nickname)") + + if let _ = UserInfoManager.shared.getCurrentUserInfo() { + DispatchQueue.global(qos: .userInitiated).async { + autoreleasepool { + let realm = try! Realm() + guard let user = realm.objects(UserInfo.self).first else { + print("[UserInfoService] 백그라운드에서 user 조회 실패") + return + } + + try! realm.write { + user.nickname = nickname + } + + print("[UserInfoService] 닉네임 업데이트 완료 (Realm 백그라운드)") + } + } + } else { + print("[UserInfoService] currentUserInfo가 nil임") + } + } else { + print("[UserInfoService] 닉네임이 없음 → 설정이 필요한 유저") + } + + completion?() + } catch { + print("[UserInfoService] 디코딩 실패: \(error.localizedDescription)") + } + + case let .failure(error): + print("[UserInfoService] 요청 실패: \(error.localizedDescription)") + } + } + } +} diff --git a/EATSSU/App/Sources/Utility/Application/AppDelegate.swift b/EATSSU/App/Sources/Utility/Application/AppDelegate.swift index 246ddc5b..e7e4addc 100644 --- a/EATSSU/App/Sources/Utility/Application/AppDelegate.swift +++ b/EATSSU/App/Sources/Utility/Application/AppDelegate.swift @@ -22,7 +22,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD handleAppleSignIn() initializeKakaoSDK() setupDebugConfigurations() - TokenManager.refreshIfNeededAsync() + UNUserNotificationCenter.current().delegate = self return true diff --git a/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift b/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift index d7656acf..4caa1ed4 100644 --- a/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift +++ b/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift @@ -10,9 +10,11 @@ import UIKit import Intents import KakaoSDKAuth +import RxSwift class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? + private let disposeBag = DisposeBag() // MARK: - UIWindowSceneDelegate Methods @@ -25,9 +27,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let splashVC = SplashViewController() window?.rootViewController = splashVC window?.makeKeyAndVisible() - - fetchNoticeAndConfigureRootViewController() - checkForAppUpdate() + + performInitialTasks() } func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { @@ -134,23 +135,69 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { LaunchSourceManager.shared.logIfNeeded() } + // MARK: - Initial Async Tasks + + private func performInitialTasks() { + Task { + _ = await AuthService.shared.checkAndRefreshTokenIfNeeded() + await MainActor.run { + observeAuthState() + fetchNoticeAndConfigureRootViewController() + checkForAppUpdate() + } + } + } + // MARK: - RootViewController & Update + private func observeAuthState() { + print("[observeAuthState] Auth 상태 구독 시작") + + AuthService.shared.isAuthenticated + .distinctUntilChanged() + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] isAuth in + guard let self = self else { + print("[observeAuthState] self가 nil입니다. 종료") + return + } + + print("[observeAuthState] isAuthenticated 상태 변경 감지: \(isAuth)") + + let vc: UIViewController + + if isAuth { + print("[observeAuthState] 로그인 상태 → HomeViewController 생성") + UserInfoService.shared.fetchAndUpdateUserInfo() + let homeVC = HomeViewController() + vc = UINavigationController(rootViewController: homeVC) + } else { + print("[observeAuthState] 비로그인 상태 → LoginViewController 생성") + let loginVC = LoginViewController() + vc = UINavigationController(rootViewController: loginVC) + } + + print("[observeAuthState] 루트 뷰컨트롤러 변경 적용") + self.window?.rootViewController = vc + }) + .disposed(by: disposeBag) + } + + private func fetchNoticeAndConfigureRootViewController() { FirebaseRemoteConfig.shared.noticeCheck { [weak self] result in + // result가 nil이면 노티스가 없는 경우이므로 아무 동작도 하지 않음 + guard let self = self, let notice = result, !notice.isEmpty else { return } DispatchQueue.main.async { - self?.configureRootViewController(with: result) + // 공지 문구가 있을 때만 루트 교체 + self.configureRootViewController(with: notice) } } } - private func configureRootViewController(with noticeMessage: String?) { - let rootViewController: UIViewController = if let notice = noticeMessage, !notice.isEmpty { - UINavigationController(rootViewController: NoticeViewController(noticeMessage: notice)) - } else { - UINavigationController(rootViewController: LoginViewController()) - } - window?.rootViewController = rootViewController + private func configureRootViewController(with noticeMessage: String) { + let newRoot = UINavigationController(rootViewController: NoticeViewController(noticeMessage: noticeMessage)) + window?.rootViewController = newRoot } private func checkForAppUpdate() { diff --git a/EATSSU/App/Sources/Utility/Base/BaseViewController.swift b/EATSSU/App/Sources/Utility/Base/BaseViewController.swift index 61de9b6b..053aa8d3 100644 --- a/EATSSU/App/Sources/Utility/Base/BaseViewController.swift +++ b/EATSSU/App/Sources/Utility/Base/BaseViewController.swift @@ -13,6 +13,8 @@ import EATSSUDesign import Moya import SnapKit +import RxSwift +import RxRelay /// EATSSU 앱에서 Controller로 사용하는 BaseViewController 클래스입니다. /// @@ -27,6 +29,8 @@ import SnapKit class BaseViewController: UIViewController { // MARK: - Properties + var toastMessage: String? + private let disposeBag = DisposeBag() private(set) lazy var className: String = type(of: self).description().components(separatedBy: ".").last ?? "" // MARK: - Initialize @@ -52,6 +56,7 @@ class BaseViewController: UIViewController { setLayout() setButtonEvent() setCustomNavigationBar() + observeLogoutMessage() view.backgroundColor = .systemBackground } @@ -126,4 +131,24 @@ class BaseViewController: UIViewController { backButton.tintColor = EATSSUDesignAsset.Color.GrayScale.gray500.color navigationController?.navigationBar.topItem?.backBarButtonItem = backButton } + + /// AuthService의 logoutMessage를 구독하여 로그아웃 메시지를 저장하는 함수 + /// - 로그인 상태가 해제될 때 전달되는 메시지를 받아서 toastMessage에 저장 + /// - 이후 viewDidAppear에서 showToastMessageIfNeeded()로 직접 표시됨 + private func observeLogoutMessage() { + AuthService.shared.logoutMessage + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] message in + self?.toastMessage = message + }) + .disposed(by: disposeBag) + } + + /// toastMessage가 존재할 경우 화면에 토스트로 표시하고, 내부 상태를 초기화하는 함수 + /// - 로그인 만료 시 메시지를 한 번만 표시하도록 동작 + func showToastMessageIfNeeded() { + guard let message = toastMessage else { return } + view.showToast(message: message) + toastMessage = nil + } } diff --git a/EATSSU/Project.swift b/EATSSU/Project.swift index b62c4bb5..f5548691 100644 --- a/EATSSU/Project.swift +++ b/EATSSU/Project.swift @@ -93,6 +93,7 @@ let project = Project( .external(name: "GoogleAppMeasurement"), .external(name: "Realm"), .external(name: "RealmSwift"), + .external(name: "RxRelay"), .external(name: "FirebaseCrashlytics"), .external(name: "FirebaseAnalytics"), .external(name: "FirebaseRemoteConfig"), @@ -128,6 +129,7 @@ let project = Project( .external(name: "GoogleAppMeasurement"), .external(name: "Realm"), .external(name: "RealmSwift"), + .external(name: "RxRelay"), .external(name: "FirebaseCrashlytics"), .external(name: "FirebaseAnalytics"), .external(name: "FirebaseRemoteConfig"), @@ -135,7 +137,6 @@ let project = Project( .external(name: "KakaoSDKUser"), .external(name: "KakaoSDKCommon"), .external(name: "KakaoSDKTalk"), - // EATSSU 내장 라이브러리 .project(target: "EATSSUDesign", path: .relativeToRoot("../EATSSUDesign"), condition: .none), ], diff --git a/Tuist/Package.swift b/Tuist/Package.swift index 83137b14..642ab518 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -33,6 +33,8 @@ import PackageDescription // RxSwift "RxSwift": .framework, + "RxRelay": .framework, + ] ) #endif