From 45d72fc884be4bea76c94c8cba9eaf6e7fd96925 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Wed, 6 Aug 2025 01:20:37 +0900 Subject: [PATCH 01/12] =?UTF-8?q?[#302]=20AuthService=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20=EB=B0=8F=20AuthInterceptor=20=EC=88=98=EC=A0=95,=20RxRelay?= =?UTF-8?q?=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Foundation/AuthInterceptor.swift | 11 ++-- .../Network/Foundation/TokenManager.swift | 4 +- EATSSU/App/Sources/Services/AuthService.swift | 62 +++++++++++++++++++ EATSSU/Project.swift | 3 +- Tuist/Package.swift | 2 + 5 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 EATSSU/App/Sources/Services/AuthService.swift diff --git a/EATSSU/App/Sources/Data/Network/Foundation/AuthInterceptor.swift b/EATSSU/App/Sources/Data/Network/Foundation/AuthInterceptor.swift index c740c9b7..0bba61c5 100644 --- a/EATSSU/App/Sources/Data/Network/Foundation/AuthInterceptor.swift +++ b/EATSSU/App/Sources/Data/Network/Foundation/AuthInterceptor.swift @@ -59,11 +59,12 @@ final class AuthInterceptor: RequestInterceptor { // Swift Concurrency 기반 비동기 재발급 처리 _Concurrency.Task { do { - try await TokenRefresher.shared.refreshIfNeeded() - await MainActor.run { completion(.retry) } - } catch { - await MainActor.run { completion(.doNotRetryWithError(error)) } - } + try await AuthService.shared.checkToken() + await MainActor.run { completion(.retry) } + } catch { + AuthService.shared.logout() + await MainActor.run { completion(.doNotRetry) } + } } } } 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/Services/AuthService.swift b/EATSSU/App/Sources/Services/AuthService.swift new file mode 100644 index 00000000..a9983a9c --- /dev/null +++ b/EATSSU/App/Sources/Services/AuthService.swift @@ -0,0 +1,62 @@ +// +// AuthService.swift +// EATSSU-DEV +// +// Created by 황상환 on 8/5/25. + +import Foundation +import RxSwift +import RxRelay + +enum AuthError: Error { + case tokenExpired +} + +/// 인증 상태 및 토큰 관리 서비스 +final class AuthService { + static let shared = AuthService() + private let disposeBag = DisposeBag() + private let relay = BehaviorRelay(value: false) + + /// 인증 상태 스트림 + var isAuthenticated: Observable { + relay.asObservable() + } + + private init() { + Task { + do { + try await self.checkToken() + } catch { + self.logout() + } + } + } + + /// 로그인 처리 + func login(accessToken: String, refreshToken: String) { + RealmService.shared.addToken( + accessToken: accessToken, + refreshToken: refreshToken + ) + relay.accept(true) + } + + /// 로그아웃 처리 + func logout() { + RealmService.shared.deleteAll(Token.self) + relay.accept(false) + } + + /// 토큰 유효성 검사 및 재발급 + func checkToken() async throws { + let token = RealmService.shared.getToken() + guard let payload = TokenManager.shared.decodePayload(token: token), + Date(timeIntervalSince1970: payload.exp) > Date() else { + throw AuthError.tokenExpired + } + + try await TokenRefresher.shared.refreshIfNeeded() + relay.accept(true) + } +} 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 From 07aba0d0bdabdcace17e338f55857525008dfc30 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Wed, 6 Aug 2025 01:38:00 +0900 Subject: [PATCH 02/12] =?UTF-8?q?[#302]=20SceneDelegate=EC=97=90=EC=84=9C?= =?UTF-8?q?=20.isAuthenticated=20=EA=B5=AC=EB=8F=85=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EB=A3=A8=ED=8A=B8=20=EB=B7=B0=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EATSSU/App/Sources/Services/AuthService.swift | 1 + .../Utility/Application/SceneDelegate.swift | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Services/AuthService.swift b/EATSSU/App/Sources/Services/AuthService.swift index a9983a9c..a92e4c22 100644 --- a/EATSSU/App/Sources/Services/AuthService.swift +++ b/EATSSU/App/Sources/Services/AuthService.swift @@ -5,6 +5,7 @@ // Created by 황상환 on 8/5/25. import Foundation + import RxSwift import RxRelay diff --git a/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift b/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift index d7656acf..a2fd1ae5 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,7 +27,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let splashVC = SplashViewController() window?.rootViewController = splashVC window?.makeKeyAndVisible() - + + observeAuthState() fetchNoticeAndConfigureRootViewController() checkForAppUpdate() } @@ -136,6 +139,21 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // MARK: - RootViewController & Update + private func observeAuthState() { + AuthService.shared.isAuthenticated + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] isAuth in + guard let self = self else { return } + if isAuth { + self.window?.rootViewController = HomeViewController() + } else { + let loginVC = LoginViewController() + self.window?.rootViewController = UINavigationController(rootViewController: loginVC) + } + }) + .disposed(by: disposeBag) + } + private func fetchNoticeAndConfigureRootViewController() { FirebaseRemoteConfig.shared.noticeCheck { [weak self] result in DispatchQueue.main.async { From 1d1a9bb08946e1965a50d0a2e4a3cb3a11629071 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Wed, 6 Aug 2025 02:06:31 +0900 Subject: [PATCH 03/12] =?UTF-8?q?[#302]=20AuthService=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=B4=EC=84=9C=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=20=EB=90=98=EA=B2=8C=EB=81=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewController/MyPageViewController.swift | 31 +++++++------------ EATSSU/App/Sources/Services/AuthService.swift | 5 +++ 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift index 1e6a8475..aa16757f 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift @@ -97,26 +97,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() + } alert.addAction(cancelAction) alert.addAction(fixAction) diff --git a/EATSSU/App/Sources/Services/AuthService.swift b/EATSSU/App/Sources/Services/AuthService.swift index a92e4c22..1c6ffcfa 100644 --- a/EATSSU/App/Sources/Services/AuthService.swift +++ b/EATSSU/App/Sources/Services/AuthService.swift @@ -36,6 +36,7 @@ final class AuthService { /// 로그인 처리 func login(accessToken: String, refreshToken: String) { + print("[AuthService] login() 호출됨") RealmService.shared.addToken( accessToken: accessToken, refreshToken: refreshToken @@ -45,19 +46,23 @@ final class AuthService { /// 로그아웃 처리 func logout() { + print("[AuthService] logout() 호출됨") RealmService.shared.deleteAll(Token.self) relay.accept(false) } /// 토큰 유효성 검사 및 재발급 func checkToken() async throws { + print("[AuthService] checkToken() 시작") let token = RealmService.shared.getToken() guard let payload = TokenManager.shared.decodePayload(token: token), Date(timeIntervalSince1970: payload.exp) > Date() else { + print("[AuthService] checkToken() 실패 → 토큰 만료됨") throw AuthError.tokenExpired } try await TokenRefresher.shared.refreshIfNeeded() + print("[AuthService] checkToken() 성공 → 토큰 유효") relay.accept(true) } } From 640cbcd99888d195c37b18b57ef87872ec76f1dd Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Wed, 6 Aug 2025 22:34:54 +0900 Subject: [PATCH 04/12] =?UTF-8?q?[#302]=20AuthService=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=EB=B0=8F=20=EA=B0=81=20VC=EC=97=90=EC=84=9C=20AuthService?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Foundation/AuthInterceptor.swift | 11 +-- .../ViewController/LoginViewController.swift | 97 +++++++++++-------- .../SetNickNameViewController.swift | 45 ++++----- .../ViewController/HomeViewController.swift | 20 ++-- .../MyReviewViewController.swift | 26 ++--- .../ViewController/ReviewViewController.swift | 2 +- .../SetRateViewController.swift | 24 ++--- EATSSU/App/Sources/Services/AuthService.swift | 73 ++++++++------ .../Sources/Services/UserInfoService.swift | 69 +++++++++++++ .../Utility/Application/AppDelegate.swift | 2 +- .../Utility/Application/SceneDelegate.swift | 52 +++++++--- 11 files changed, 272 insertions(+), 149 deletions(-) create mode 100644 EATSSU/App/Sources/Services/UserInfoService.swift diff --git a/EATSSU/App/Sources/Data/Network/Foundation/AuthInterceptor.swift b/EATSSU/App/Sources/Data/Network/Foundation/AuthInterceptor.swift index 0bba61c5..c740c9b7 100644 --- a/EATSSU/App/Sources/Data/Network/Foundation/AuthInterceptor.swift +++ b/EATSSU/App/Sources/Data/Network/Foundation/AuthInterceptor.swift @@ -59,12 +59,11 @@ final class AuthInterceptor: RequestInterceptor { // Swift Concurrency 기반 비동기 재발급 처리 _Concurrency.Task { do { - try await AuthService.shared.checkToken() - await MainActor.run { completion(.retry) } - } catch { - AuthService.shared.logout() - await MainActor.run { completion(.doNotRetry) } - } + try await TokenRefresher.shared.refreshIfNeeded() + await MainActor.run { completion(.retry) } + } catch { + await MainActor.run { completion(.doNotRetryWithError(error)) } + } } } } diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift index 2d87980b..207fac80 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift @@ -30,7 +30,8 @@ final class LoginViewController: BaseViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - handleAutoLogin() + showToastMessageIfNeeded() +// handleAutoLogin() } override func viewDidLoad() { @@ -82,13 +83,13 @@ final class LoginViewController: BaseViewController { } /// Realm에 저장된 토큰이 있는지 확인 후, 있으면 홈 화면으로 이동한다. - private func handleAutoLogin() { - guard hasStoredToken() else { return } - #if DEBUG - print("저장된 AccessToken: ", RealmService.shared.getToken()) - #endif - changeIntoHomeViewController() - } +// private func handleAutoLogin() { +// guard hasStoredToken() else { return } +// // 토큰이 남아 있으면 AuthService에 로그인 상태로 알리고, SceneDelegate가 화면 전환을 담당하게 +// let at = RealmService.shared.getToken() +// let rt = RealmService.shared.getRefreshToken() +// AuthService.shared.login(accessToken: at, refreshToken: rt) +// } private func hasStoredToken() -> Bool { !RealmService.shared.getToken().isEmpty @@ -108,18 +109,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) @@ -129,8 +137,9 @@ final class LoginViewController: BaseViewController { } private func showToastMessageIfNeeded() { - guard let toastMessage = self.toastMessage else { return } - view.showToast(message: toastMessage) + guard let message = toastMessage else { return } + view.showToast(message: message) + toastMessage = nil } // MARK: - 액션 메서드 @@ -172,7 +181,8 @@ final class LoginViewController: BaseViewController { @objc private func lookingWithNoSignInButtonDidTapped() { - changeIntoHomeViewController() + // 비로그인 모드 진입: 필요하다면 빈 토큰으로 로그인 상태만 세팅 + AuthService.shared.login(accessToken: "", refreshToken: "") } } @@ -192,14 +202,17 @@ 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() + // (닉네임 설정 화면으로 이동할 필요가 있으면, 아래 로직에서 분기 처리) +// getMyInfo() } catch { switch accountType { case .apple: @@ -257,25 +270,31 @@ 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) - } - } - } +// private func getMyInfo() { +// print("[디버깅] getMyInfo() 호출됨") // ✅ 추가 +// +// myProvider.request(.myInfo) { [weak self] result in +// guard let self else { return } +// switch result { +// case let .success(moyaResponse): +// do { +// print("[디버깅] myInfo API 응답 받음") // ✅ 추가 +// let responseData = try moyaResponse.map(BaseResponse.self) +// guard let responseData = responseData.result else { +// print("[디버깅] myInfo 결과 result가 nil임") // ✅ 추가 +// return +// } +// print("[디버깅] 닉네임 정보: \(responseData.nickname ?? "없음")") // ✅ 추가 +// handleNicknameCheck(info: responseData) +// } catch { +// print("[디버깅] 디코딩 에러: \(error.localizedDescription)") +// } +// case let .failure(error): +// print("[디버깅] myInfo API 실패: \(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..975bb7f9 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift @@ -106,16 +106,16 @@ final class SetNickNameViewController: BaseViewController { } } - 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)) - } - } - } +// 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 +129,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 +168,14 @@ extension SetNickNameViewController { print(err.localizedDescription) RealmService.shared.resetDB() - self.navigateToLogin() + AuthService.shared.logout() } case let .failure(err): print(err.localizedDescription) - + RealmService.shared.resetDB() - self.navigateToLogin() + // message 파라미터 없는 logout 호출로 변경 + AuthService.shared.logout() } } } diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift index ca7f787e..6e9d7fbb 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift @@ -118,7 +118,7 @@ final class HomeViewController: BaseViewController { message: "로그인 하시겠습니까?", preferredStyle: .alert) let confirmAction = UIAlertAction(title: "확인", style: .default) { [weak self] _ in - self?.navigateToLogin() + AuthService.shared.logout() } let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil) alert.addAction(confirmAction) @@ -126,15 +126,15 @@ final class HomeViewController: BaseViewController { present(alert, animated: true, completion: nil) } - private func navigateToLogin() { - let loginVC = LoginViewController() - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let sceneDelegate = windowScene.delegate as? SceneDelegate, - let window = sceneDelegate.window - { - window.replaceRootViewController(loginVC) - } - } +// private func navigateToLogin() { +// let loginVC = LoginViewController() +// if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, +// let sceneDelegate = windowScene.delegate as? SceneDelegate, +// let window = sceneDelegate.window +// { +// window.replaceRootViewController(loginVC) +// } +// } // MARK: - Firebase diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift index 4177f211..7f991b80 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift @@ -121,16 +121,16 @@ final class MyReviewViewController: BaseViewController { } } - 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)) - } - } - } +// 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 +171,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 +194,7 @@ extension MyReviewViewController { print(err.localizedDescription) RealmService.shared.resetDB() - self.navigateToLogin() + AuthService.shared.logout(message: "세션이 만료되었습니다. 다시 로그인해주세요.") } } }) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index f898af20..fbbd6b73 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -225,7 +225,7 @@ final class ReviewViewController: BaseViewController { } } else { showAlertControllerWithCancel(title: "로그인이 필요한 서비스입니다", message: "로그인 하시겠습니까?", confirmStyle: .default) { - self.pushToLoginVC() + AuthService.shared.logout() } } } diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 841f466a..235ad64a 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,21 +516,21 @@ extension SetRateViewController { print(err.localizedDescription) RealmService.shared.resetDB() - self.navigateToLogin() + AuthService.shared.logout(message: "세션이 만료되었습니다. 다시 로그인해주세요.") } } } - 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)) - } - } - } +// 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: - UIImagePickerControllerDelegate diff --git a/EATSSU/App/Sources/Services/AuthService.swift b/EATSSU/App/Sources/Services/AuthService.swift index 1c6ffcfa..43374fdf 100644 --- a/EATSSU/App/Sources/Services/AuthService.swift +++ b/EATSSU/App/Sources/Services/AuthService.swift @@ -18,51 +18,68 @@ final class AuthService { static let shared = AuthService() private let disposeBag = DisposeBag() private let relay = BehaviorRelay(value: false) + let logoutMessageRelay = PublishRelay() - /// 인증 상태 스트림 - var isAuthenticated: Observable { - relay.asObservable() - } - + var isAuthenticated: BehaviorRelay { relay } + private init() { - Task { - do { - try await self.checkToken() - } catch { - self.logout() - } - } + let hasToken = isTokenValid() + relay.accept(hasToken) } - /// 로그인 처리 func login(accessToken: String, refreshToken: String) { print("[AuthService] login() 호출됨") - RealmService.shared.addToken( - accessToken: accessToken, - refreshToken: refreshToken - ) + RealmService.shared.addToken(accessToken: accessToken, refreshToken: refreshToken) relay.accept(true) } - /// 로그아웃 처리 - func logout() { + func logout(message: String? = nil) { print("[AuthService] logout() 호출됨") RealmService.shared.deleteAll(Token.self) + if let message = message { + logoutMessageRelay.accept(message) + } relay.accept(false) } + + var logoutMessage: Observable { + logoutMessageRelay.asObservable() + } - /// 토큰 유효성 검사 및 재발급 - func checkToken() async throws { - print("[AuthService] checkToken() 시작") + + func isTokenValid() -> Bool { let token = RealmService.shared.getToken() - guard let payload = TokenManager.shared.decodePayload(token: token), - Date(timeIntervalSince1970: payload.exp) > Date() else { - print("[AuthService] checkToken() 실패 → 토큰 만료됨") - throw AuthError.tokenExpired + guard let payload = TokenManager.shared.decodePayload(token: token) else { + 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 { + 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 { + print("[AuthService] 토큰 재발급 실패") + return false + } } - try await TokenRefresher.shared.refreshIfNeeded() - print("[AuthService] checkToken() 성공 → 토큰 유효") + // 재발급 성공 또는 아직 유효하다면 로그인 상태로 전환 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 a2fd1ae5..abf36cff 100644 --- a/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift +++ b/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift @@ -28,9 +28,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window?.rootViewController = splashVC window?.makeKeyAndVisible() - observeAuthState() - fetchNoticeAndConfigureRootViewController() - checkForAppUpdate() + Task { + let ok = await AuthService.shared.checkAndRefreshTokenIfNeeded() + // relay.accept(true/false) 가 이미 호출된 뒤에 구독 시작 + await MainActor.run { + self.observeAuthState() + self.fetchNoticeAndConfigureRootViewController() + self.checkForAppUpdate() + } + } } func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { @@ -140,35 +146,53 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // 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 { return } + guard let self = self else { + print("[observeAuthState] self가 nil입니다. 종료") + return + } + + print("[observeAuthState] isAuthenticated 상태 변경 감지: \(isAuth)") + + let vc: UIViewController + if isAuth { - self.window?.rootViewController = HomeViewController() + print("[observeAuthState] 로그인 상태 → HomeViewController 생성") + UserInfoService.shared.fetchAndUpdateUserInfo() + let homeVC = HomeViewController() + vc = UINavigationController(rootViewController: homeVC) } else { + print("[observeAuthState] 비로그인 상태 → LoginViewController 생성") let loginVC = LoginViewController() - self.window?.rootViewController = UINavigationController(rootViewController: loginVC) + 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() { From 22216b924e790199230db9fc53c393a78d8130bb Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Thu, 7 Aug 2025 01:55:12 +0900 Subject: [PATCH 05/12] =?UTF-8?q?[#302]=20=EB=91=98=EB=9F=AC=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewController/LoginViewController.swift | 3 +-- .../ViewController/HomeViewController.swift | 23 ++++++++++--------- .../ViewController/ReviewViewController.swift | 14 +++++++---- .../Utility/Application/SceneDelegate.swift | 2 +- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift index 207fac80..33f8565a 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift @@ -181,8 +181,7 @@ final class LoginViewController: BaseViewController { @objc private func lookingWithNoSignInButtonDidTapped() { - // 비로그인 모드 진입: 필요하다면 빈 토큰으로 로그인 상태만 세팅 - AuthService.shared.login(accessToken: "", refreshToken: "") + changeIntoHomeViewController() } } diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift index 6e9d7fbb..9e4e472b 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift @@ -101,9 +101,10 @@ final class HomeViewController: BaseViewController { @objc private func didTapRightBarButton() { - if RealmService.shared.isAccessTokenPresent() { + if AuthService.shared.isTokenValid() { navigateToMyPage() } else { + AuthService.shared.logout() presentLoginAlert() } } @@ -118,7 +119,7 @@ final class HomeViewController: BaseViewController { message: "로그인 하시겠습니까?", preferredStyle: .alert) let confirmAction = UIAlertAction(title: "확인", style: .default) { [weak self] _ in - AuthService.shared.logout() + self?.navigateToLogin() } let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil) alert.addAction(confirmAction) @@ -126,15 +127,15 @@ final class HomeViewController: BaseViewController { present(alert, animated: true, completion: nil) } -// private func navigateToLogin() { -// let loginVC = LoginViewController() -// if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, -// let sceneDelegate = windowScene.delegate as? SceneDelegate, -// let window = sceneDelegate.window -// { -// window.replaceRootViewController(loginVC) -// } -// } + private func navigateToLogin() { + let loginVC = LoginViewController() + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let sceneDelegate = windowScene.delegate as? SceneDelegate, + let window = sceneDelegate.window + { + window.replaceRootViewController(loginVC) + } + } // MARK: - Firebase diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index fbbd6b73..4023daef 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -189,7 +189,7 @@ final class ReviewViewController: BaseViewController { // @objc func userTapReviewButton() { - if RealmService.shared.isAccessTokenPresent() { + if AuthService.shared.isTokenValid() { activityIndicatorView.isHidden = false DispatchQueue.global().async { // 백그라운드 스레드에서 작업을 수행 // 작업 완료 후 UI 업데이트를 메인 스레드에서 수행 @@ -224,15 +224,21 @@ final class ReviewViewController: BaseViewController { } } } else { + AuthService.shared.logout() showAlertControllerWithCancel(title: "로그인이 필요한 서비스입니다", message: "로그인 하시겠습니까?", confirmStyle: .default) { - AuthService.shared.logout() + self.navigateToLogin() } } } - 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/Utility/Application/SceneDelegate.swift b/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift index abf36cff..efb9d7d6 100644 --- a/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift +++ b/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift @@ -29,7 +29,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window?.makeKeyAndVisible() Task { - let ok = await AuthService.shared.checkAndRefreshTokenIfNeeded() + _ = await AuthService.shared.checkAndRefreshTokenIfNeeded() // relay.accept(true/false) 가 이미 호출된 뒤에 구독 시작 await MainActor.run { self.observeAuthState() From c8d0c86b2dce3f44b5443f06308e70cefb288bed Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Thu, 7 Aug 2025 18:13:47 +0900 Subject: [PATCH 06/12] =?UTF-8?q?[#302]=20=ED=86=A0=EC=8A=A4=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewController/LoginViewController.swift | 18 ++--- .../SetNickNameViewController.swift | 7 +- .../ViewController/HomeViewController.swift | 22 +++--- .../ViewController/MyPageViewController.swift | 5 +- .../ViewController/ReviewViewController.swift | 76 ++++++++++--------- EATSSU/App/Sources/Services/AuthService.swift | 17 +++-- .../Utility/Application/SceneDelegate.swift | 23 +++--- .../Utility/Base/BaseViewController.swift | 20 +++++ 8 files changed, 109 insertions(+), 79 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift index 33f8565a..beb56337 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,11 +27,11 @@ final class LoginViewController: BaseViewController { // MARK: - Life Cycle - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - showToastMessageIfNeeded() -// handleAutoLogin() - } +// override func viewWillAppear(_ animated: Bool) { +// super.viewWillAppear(animated) +// showToastMessageIfNeeded() +//// handleAutoLogin() +// } override func viewDidLoad() { super.viewDidLoad() @@ -135,12 +134,7 @@ final class LoginViewController: BaseViewController { print("⭐️⭐️ 토큰 저장 성공 ⭐️⭐️") #endif } - - private func showToastMessageIfNeeded() { - guard let message = toastMessage else { return } - view.showToast(message: message) - toastMessage = nil - } + // MARK: - 액션 메서드 diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift index 975bb7f9..33d43cf4 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift @@ -168,14 +168,13 @@ extension SetNickNameViewController { print(err.localizedDescription) RealmService.shared.resetDB() - AuthService.shared.logout() + AuthService.shared.logout(message: "세션이 만료되었습니다. 다시 로그인해주세요.") } case let .failure(err): print(err.localizedDescription) - + RealmService.shared.resetDB() - // message 파라미터 없는 logout 호출로 변경 - AuthService.shared.logout() + 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 9e4e472b..2b85ee15 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift @@ -104,7 +104,6 @@ final class HomeViewController: BaseViewController { if AuthService.shared.isTokenValid() { navigateToMyPage() } else { - AuthService.shared.logout() presentLoginAlert() } } @@ -115,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() + 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 aa16757f..fd71fc0b 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() { @@ -106,7 +107,7 @@ final class MyPageViewController: BaseViewController { let cancelAction = UIAlertAction(title: "취소하기", style: .default, handler: nil) let fixAction = UIAlertAction(title: "로그아웃", style: .default) { _ in - AuthService.shared.logout() + AuthService.shared.logout(message: "로그아웃이 완료되었습니다.") } alert.addAction(cancelAction) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index 4023daef..7ba6ea3c 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -189,45 +189,53 @@ final class ReviewViewController: BaseViewController { // @objc func userTapReviewButton() { - if AuthService.shared.isTokenValid() { + _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() + 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 { - AuthService.shared.logout() - showAlertControllerWithCancel(title: "로그인이 필요한 서비스입니다", message: "로그인 하시겠습니까?", confirmStyle: .default) { - self.navigateToLogin() - } } } diff --git a/EATSSU/App/Sources/Services/AuthService.swift b/EATSSU/App/Sources/Services/AuthService.swift index 43374fdf..a2597fb0 100644 --- a/EATSSU/App/Sources/Services/AuthService.swift +++ b/EATSSU/App/Sources/Services/AuthService.swift @@ -9,19 +9,17 @@ import Foundation import RxSwift import RxRelay -enum AuthError: Error { - case tokenExpired -} - /// 인증 상태 및 토큰 관리 서비스 final class AuthService { static let shared = AuthService() private let disposeBag = DisposeBag() private let relay = BehaviorRelay(value: false) - let logoutMessageRelay = PublishRelay() + private let logoutMessageRelay = BehaviorRelay(value: nil) + + var isAuthenticated: Observable { + relay.asObservable() + } - var isAuthenticated: BehaviorRelay { relay } - private init() { let hasToken = isTokenValid() relay.accept(hasToken) @@ -30,6 +28,7 @@ final class AuthService { func login(accessToken: String, refreshToken: String) { print("[AuthService] login() 호출됨") RealmService.shared.addToken(accessToken: accessToken, refreshToken: refreshToken) + relay.accept(false) relay.accept(true) } @@ -46,10 +45,10 @@ final class AuthService { logoutMessageRelay.asObservable() } - func isTokenValid() -> Bool { let token = RealmService.shared.getToken() guard let payload = TokenManager.shared.decodePayload(token: token) else { + logout() print("[AuthService] 디코딩 실패") return false } @@ -62,6 +61,7 @@ final class AuthService { let token = RealmService.shared.getToken() guard let payload = TokenManager.shared.decodePayload(token: token) else { + logout() print("[AuthService] 디코딩 실패") return false } @@ -73,6 +73,7 @@ final class AuthService { try await TokenRefresher.shared.refreshIfNeeded() print("[AuthService] 토큰 재발급 성공") } catch { + logout(message: "세션이 만료되었습니다. 다시 로그인해주세요.") print("[AuthService] 토큰 재발급 실패") return false } diff --git a/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift b/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift index efb9d7d6..4caa1ed4 100644 --- a/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift +++ b/EATSSU/App/Sources/Utility/Application/SceneDelegate.swift @@ -28,15 +28,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window?.rootViewController = splashVC window?.makeKeyAndVisible() - Task { - _ = await AuthService.shared.checkAndRefreshTokenIfNeeded() - // relay.accept(true/false) 가 이미 호출된 뒤에 구독 시작 - await MainActor.run { - self.observeAuthState() - self.fetchNoticeAndConfigureRootViewController() - self.checkForAppUpdate() - } - } + performInitialTasks() } func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { @@ -143,6 +135,19 @@ 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() { diff --git a/EATSSU/App/Sources/Utility/Base/BaseViewController.swift b/EATSSU/App/Sources/Utility/Base/BaseViewController.swift index 61de9b6b..a62909a2 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,19 @@ class BaseViewController: UIViewController { backButton.tintColor = EATSSUDesignAsset.Color.GrayScale.gray500.color navigationController?.navigationBar.topItem?.backBarButtonItem = backButton } + + private func observeLogoutMessage() { + AuthService.shared.logoutMessage + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] message in + self?.toastMessage = message + }) + .disposed(by: disposeBag) + } + + func showToastMessageIfNeeded() { + guard let message = toastMessage else { return } + view.showToast(message: message) + toastMessage = nil + } } From ef6bbc9599d4f0ef748f9311d6a46b89f809d1ac Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Thu, 7 Aug 2025 20:02:12 +0900 Subject: [PATCH 07/12] =?UTF-8?q?[#302]=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewController/LoginViewController.swift | 40 +------------------ .../SetNickNameViewController.swift | 11 ----- .../ViewController/MyPageViewController.swift | 2 +- .../MyReviewViewController.swift | 11 ----- .../SetRateViewController.swift | 11 ----- 5 files changed, 2 insertions(+), 73 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift index beb56337..5d5aac0e 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift @@ -80,16 +80,7 @@ final class LoginViewController: BaseViewController { Analytics.logEvent("LoginViewControllerLoad", parameters: nil) #endif } - - /// Realm에 저장된 토큰이 있는지 확인 후, 있으면 홈 화면으로 이동한다. -// private func handleAutoLogin() { -// guard hasStoredToken() else { return } -// // 토큰이 남아 있으면 AuthService에 로그인 상태로 알리고, SceneDelegate가 화면 전환을 담당하게 -// let at = RealmService.shared.getToken() -// let rt = RealmService.shared.getRefreshToken() -// AuthService.shared.login(accessToken: at, refreshToken: rt) -// } - + private func hasStoredToken() -> Bool { !RealmService.shared.getToken().isEmpty } @@ -204,8 +195,6 @@ extension LoginViewController { // 3) 로컬 매니저에 유저 정보 생성 _ = UserInfoManager.shared.createUserInfo(accountType: accountType) - // (닉네임 설정 화면으로 이동할 필요가 있으면, 아래 로직에서 분기 처리) -// getMyInfo() } catch { switch accountType { case .apple: @@ -261,33 +250,6 @@ extension LoginViewController { } } } - - /// 서버에서 현재 유저 정보를 조회 -// private func getMyInfo() { -// print("[디버깅] getMyInfo() 호출됨") // ✅ 추가 -// -// myProvider.request(.myInfo) { [weak self] result in -// guard let self else { return } -// switch result { -// case let .success(moyaResponse): -// do { -// print("[디버깅] myInfo API 응답 받음") // ✅ 추가 -// let responseData = try moyaResponse.map(BaseResponse.self) -// guard let responseData = responseData.result else { -// print("[디버깅] myInfo 결과 result가 nil임") // ✅ 추가 -// return -// } -// print("[디버깅] 닉네임 정보: \(responseData.nickname ?? "없음")") // ✅ 추가 -// handleNicknameCheck(info: responseData) -// } catch { -// print("[디버깅] 디코딩 에러: \(error.localizedDescription)") -// } -// case let .failure(error): -// print("[디버깅] myInfo API 실패: \(error.localizedDescription)") -// } -// } -// } - } // MARK: - 카카오 사용자 정보 가져오기 diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift index 33d43cf4..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 diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift index fd71fc0b..6ed66391 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift @@ -43,7 +43,7 @@ final class MyPageViewController: BaseViewController { mypageView.setUserInfo(nickname: nickName) showToastMessageIfNeeded() } - + // MARK: - Functions override func setCustomNavigationBar() { diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyReviewViewController.swift index 7f991b80..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 {} diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift index 235ad64a..a47a4d78 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/SetRateViewController.swift @@ -520,17 +520,6 @@ extension SetRateViewController { } } } - -// 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: - UIImagePickerControllerDelegate From 7bf71e28df67808a7fe2a6a9a9a73b2c31055448 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Thu, 7 Aug 2025 20:02:54 +0900 Subject: [PATCH 08/12] =?UTF-8?q?[#302]=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EATSSU/App/Sources/Services/AuthService.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/EATSSU/App/Sources/Services/AuthService.swift b/EATSSU/App/Sources/Services/AuthService.swift index a2597fb0..a0846305 100644 --- a/EATSSU/App/Sources/Services/AuthService.swift +++ b/EATSSU/App/Sources/Services/AuthService.swift @@ -28,7 +28,6 @@ final class AuthService { func login(accessToken: String, refreshToken: String) { print("[AuthService] login() 호출됨") RealmService.shared.addToken(accessToken: accessToken, refreshToken: refreshToken) - relay.accept(false) relay.accept(true) } From 53d9c0c90d9c02aab4589698afdfd08525cdc7d2 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Thu, 7 Aug 2025 20:07:25 +0900 Subject: [PATCH 09/12] =?UTF-8?q?[#302]=20Toast=EB=A9=94=EC=8B=9C=EC=A7=80?= =?UTF-8?q?=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MyPage/ViewController/MyPageViewController.swift | 2 +- EATSSU/App/Sources/Services/AuthService.swift | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift index 6ed66391..398666c6 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift @@ -107,7 +107,7 @@ final class MyPageViewController: BaseViewController { let cancelAction = UIAlertAction(title: "취소하기", style: .default, handler: nil) let fixAction = UIAlertAction(title: "로그아웃", style: .default) { _ in - AuthService.shared.logout(message: "로그아웃이 완료되었습니다.") + AuthService.shared.logout() } alert.addAction(cancelAction) diff --git a/EATSSU/App/Sources/Services/AuthService.swift b/EATSSU/App/Sources/Services/AuthService.swift index a0846305..0894f504 100644 --- a/EATSSU/App/Sources/Services/AuthService.swift +++ b/EATSSU/App/Sources/Services/AuthService.swift @@ -28,6 +28,8 @@ final class AuthService { 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) } From 486bd640955c760e806f021560fb914cec18d37d Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Thu, 7 Aug 2025 20:16:31 +0900 Subject: [PATCH 10/12] =?UTF-8?q?[#302]=20baseviewVC=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EATSSU/App/Sources/Utility/Base/BaseViewController.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/EATSSU/App/Sources/Utility/Base/BaseViewController.swift b/EATSSU/App/Sources/Utility/Base/BaseViewController.swift index a62909a2..053aa8d3 100644 --- a/EATSSU/App/Sources/Utility/Base/BaseViewController.swift +++ b/EATSSU/App/Sources/Utility/Base/BaseViewController.swift @@ -132,15 +132,20 @@ class BaseViewController: UIViewController { 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 + self?.toastMessage = message }) .disposed(by: disposeBag) } - + + /// toastMessage가 존재할 경우 화면에 토스트로 표시하고, 내부 상태를 초기화하는 함수 + /// - 로그인 만료 시 메시지를 한 번만 표시하도록 동작 func showToastMessageIfNeeded() { guard let message = toastMessage else { return } view.showToast(message: message) From bc2b4fe4fb35442f7c02b30a3ea9b001bb4ee1cf Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Thu, 7 Aug 2025 20:19:48 +0900 Subject: [PATCH 11/12] =?UTF-8?q?[#302]=20=ED=83=88=ED=87=B4=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=97=90=20authService=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewController/UserWithdrawViewController.swift | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) 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) } From c0462a1d6799fbcdee819caf65b3067fa686a0a7 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Thu, 7 Aug 2025 20:26:29 +0900 Subject: [PATCH 12/12] =?UTF-8?q?[#302]=20toast=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20nil=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Home/ViewController/HomeViewController.swift | 2 +- .../MyPage/ViewController/MyPageViewController.swift | 2 +- .../Review/ViewController/ReviewViewController.swift | 2 +- EATSSU/App/Sources/Services/AuthService.swift | 3 +++ 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift index 2b85ee15..f24c6b3b 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift @@ -120,7 +120,7 @@ final class HomeViewController: BaseViewController { preferredStyle: .alert ) let confirm = UIAlertAction(title: "확인", style: .default) { _ in - AuthService.shared.logout() + AuthService.shared.logout(message: nil) self.navigateToLogin() } let cancel = UIAlertAction(title: "취소", style: .cancel) diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift index 398666c6..01fc4264 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift @@ -107,7 +107,7 @@ final class MyPageViewController: BaseViewController { let cancelAction = UIAlertAction(title: "취소하기", style: .default, handler: nil) let fixAction = UIAlertAction(title: "로그아웃", style: .default) { _ in - AuthService.shared.logout() + AuthService.shared.logout(message: nil) } alert.addAction(cancelAction) diff --git a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift index 7ba6ea3c..6cdcc285 100644 --- a/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift +++ b/EATSSU/App/Sources/Presentation/Review/ViewController/ReviewViewController.swift @@ -198,7 +198,7 @@ final class ReviewViewController: BaseViewController { preferredStyle: .alert ) let confirm = UIAlertAction(title: "확인", style: .default) { _ in - AuthService.shared.logout() + AuthService.shared.logout(message: nil) self.navigateToLogin() } let cancel = UIAlertAction(title: "취소", style: .cancel) diff --git a/EATSSU/App/Sources/Services/AuthService.swift b/EATSSU/App/Sources/Services/AuthService.swift index 0894f504..c59ae144 100644 --- a/EATSSU/App/Sources/Services/AuthService.swift +++ b/EATSSU/App/Sources/Services/AuthService.swift @@ -36,8 +36,11 @@ final class AuthService { 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) }