diff --git a/Projects/Feature/Sources/Extension/GOMSRefreshToken.swift b/Projects/Feature/Sources/Extension/GOMSRefreshToken.swift index 2c01f60a..5e68e9b6 100644 --- a/Projects/Feature/Sources/Extension/GOMSRefreshToken.swift +++ b/Projects/Feature/Sources/Extension/GOMSRefreshToken.swift @@ -6,6 +6,8 @@ // Copyright © 2026 HARIBO. All rights reserved. // +import UIKit +import Foundation import Moya import Service @@ -68,17 +70,35 @@ public final class GOMSRefreshToken { } private func updateKeychainToken() { + let newAccessToken = reissuanceData?.accessToken ?? "" + let newRefreshToken = reissuanceData?.refreshToken ?? "" let accessTokenUpdated = keychain.updateItem( - token: reissuanceData?.accessToken ?? "", + token: newAccessToken, key: Const.KeyChainKey.accessToken ) let refreshTokenUpdated = keychain.updateItem( - token: reissuanceData?.refreshToken ?? "", + token: newRefreshToken, key: Const.KeyChainKey.refreshToken ) + // 새 accessToken의 JWT payload에서 role을 파싱해 authority 키체인도 갱신한다. + // 서버가 DB에서 role을 변경한 경우, 새로 발급된 토큰에는 변경된 role이 담기므로 + // 이 시점에 authority를 덮어써야 앱이 최신 권한을 반영할 수 있다. + if let payload = newAccessToken.split(separator: ".").dropFirst().first { + let base64url = String(payload) + let base64 = base64url + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + let paddedPayload = base64 + String(repeating: "=", count: (4 - base64.count % 4) % 4) + if let data = Data(base64Encoded: paddedPayload), + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let role = json["role"] as? String { + keychain.create(key: Const.KeyChainKey.authority, token: role) + } + } + if accessTokenUpdated && refreshTokenUpdated { print("keychain update success") } else { diff --git a/Projects/Feature/Sources/Scene/Main/Admin/View/AdminMainViewController.swift b/Projects/Feature/Sources/Scene/Main/Admin/View/AdminMainViewController.swift index 0142aac2..b9e84015 100644 --- a/Projects/Feature/Sources/Scene/Main/Admin/View/AdminMainViewController.swift +++ b/Projects/Feature/Sources/Scene/Main/Admin/View/AdminMainViewController.swift @@ -151,7 +151,7 @@ public class AdminMainViewController: BaseViewController, UICollectionViewDataSo super.viewWillAppear(animated) isVisible = true isClockOn = UserDefaults.standard.bool(forKey: "isClockOn") - fetchData() + syncRoleAndFetch() setupNavigationBar() } @@ -240,6 +240,35 @@ public class AdminMainViewController: BaseViewController, UICollectionViewDataSo } } + /// 앱 재진입 또는 화면 진입 시 최신 role을 서버에서 확인해 화면을 전환한다. + /// 1. reissue로 토큰을 갱신 (새 JWT에 최신 role 포함) + /// 2. /member/myrole로 현재 role 확인 + /// 3. role이 ROLE_STUDENT로 바뀌었으면 MainViewController로 전환 + private func syncRoleAndFetch() { + viewModel.gomsRefreshToken.tokenReissuance { [weak self] success in + guard let self else { return } + if success { + self.viewModel.getProfile { [weak self] authority in + guard let self else { return } + DispatchQueue.main.async { + guard self.isVisible else { return } + if authority == "ROLE_STUDENT" { + let mainVC = MainViewController() + self.navigationController?.setViewControllers([mainVC], animated: false) + return + } + self.fetchData() + } + } + } else { + DispatchQueue.main.async { [weak self] in + guard let self, self.isVisible else { return } + self.fetchData() + } + } + } + } + private func fetchData() { let group = DispatchGroup() diff --git a/Projects/Feature/Sources/Scene/Main/User/View/MainViewController.swift b/Projects/Feature/Sources/Scene/Main/User/View/MainViewController.swift index dc89aa3b..38e80175 100644 --- a/Projects/Feature/Sources/Scene/Main/User/View/MainViewController.swift +++ b/Projects/Feature/Sources/Scene/Main/User/View/MainViewController.swift @@ -259,15 +259,7 @@ public final class MainViewController: BaseViewController, UICollectionViewDataS super.viewWillAppear(animated) isVisible = true isClockOn = UserDefaults.standard.bool(forKey: "isClockOn") - - mainViewModel.getLateList { [weak self] in - self?.mainViewModel.getOutingList { [weak self] in - self?.setup() - } - } - - fetchData() - + syncRoleAndFetch() self.navigationController?.navigationBar.prefersLargeTitles = false self.navigationItem.hidesBackButton = true self.navigationController?.setNavigationBarHidden(true, animated: false) @@ -409,6 +401,32 @@ public final class MainViewController: BaseViewController, UICollectionViewDataS } } + /// 화면 진입 시 토큰을 갱신하고 최신 role을 확인해 필요하면 화면을 전환한다. + private func syncRoleAndFetch() { + mainViewModel.gomsRefreshToken.tokenReissuance { [weak self] success in + guard let self else { return } + if success { + self.mainViewModel.getProfile { [weak self] authority in + guard let self else { return } + DispatchQueue.main.async { + guard self.isVisible else { return } + if authority == "ROLE_STUDENT_COUNCIL" { + let adminVC = AdminMainViewController() + self.navigationController?.setViewControllers([adminVC], animated: false) + return + } + self.fetchData() + } + } + } else { + DispatchQueue.main.async { [weak self] in + guard let self, self.isVisible else { return } + self.fetchData() + } + } + } + } + private func fetchData() { let group = DispatchGroup()