-
Notifications
You must be signed in to change notification settings - Fork 1
[FEAT] 자동로그인 디버깅을 위한 토큰 로그 구현 (#276) #278
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
acbb4c7 to
d1a7160
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements a token logging system to help debug auto-login issues by tracking token lifecycle events. The PR also includes a refactoring that renames UserDefaultsManager to UserDefaultsUtils to better reflect its utility nature.
Key Changes:
- Added
TokenLoggersingleton for tracking token state changes and operations (saved, cleared, refreshed, expired) - Implemented a hidden debug button on the login screen that displays token logs (visible only in Debug and TestFlight builds)
- Refactored
UserDefaultsManagertoUserDefaultsUtilsacross the entire codebase for better naming consistency
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
ACON-iOS/ACON-iOS/Global/Utils/TokenLogger.swift |
New utility for logging token lifecycle events with timestamp and automatic log rotation (max 100 entries) |
ACON-iOS/ACON-iOS/Presentation/Login/View/TokenLogViewController.swift |
New view controller for displaying token logs with clear functionality |
ACON-iOS/ACON-iOS/Global/Settings/Config/BuildConfig.swift |
New build configuration utility to detect debug vs release builds |
ACON-iOS/ACON-iOS/Presentation/Login/View/LoginViewController.swift |
Added hidden debug button (top-left) to access token logs in debug/TestFlight builds |
ACON-iOS/ACON-iOS/Presentation/Login/ViewModel/LoginViewModel.swift |
Integrated token logging on successful login; renamed UserDefaultsManager references |
ACON-iOS/ACON-iOS/Presentation/Splash/View/SplashViewController.swift |
Added token state logging on splash screen; renamed UserDefaultsManager references |
ACON-iOS/ACON-iOS/Global/Service/AuthManager.swift |
Integrated token logging for refresh success/failure cases; renamed UserDefaultsManager references |
ACON-iOS/ACON-iOS/Global/Utils/UserDefaultsUtils.swift |
Renamed from UserDefaultsManager; added tokenLogs key and integrated logging on token clear/reset |
ACON-iOS/ACON-iOS/Global/Utils/Enums/HeaderType.swift |
Updated to use UserDefaultsUtils instead of UserDefaultsManager |
ACON-iOS/ACON-iOS/Presentation/Profile/ViewModel/SettingViewModel.swift |
Updated to use UserDefaultsUtils (logout flow) |
ACON-iOS/ACON-iOS/Presentation/Profile/View/ProfileSettingViewController.swift |
Updated to use UserDefaultsUtils (error handling) |
ACON-iOS/ACON-iOS/Presentation/Withdrawal/ViewModel/WithdrawalViewModel.swift |
Updated to use UserDefaultsUtils (withdrawal flow) |
ACON-iOS/ACON-iOS/Presentation/Withdrawal/View/WithdrawalConfirmationViewController.swift |
Updated to use UserDefaultsUtils (error handling) |
ACON-iOS/ACON-iOS/Presentation/Preference/ViewModel/PreferenceViewModel.swift |
Updated to use UserDefaultsUtils (preference setting) |
ACON-iOS/ACON-iOS/Presentation/Preference/View/PreferenceViewController.swift |
Updated to use UserDefaultsUtils (seen flag) |
ACON-iOS/ACON-iOS/Presentation/Tutorial/View/TutorialContainerViewController.swift |
Updated to use UserDefaultsUtils (tutorial seen flag) |
ACON-iOS/ACON-iOS/Presentation/LocalVerification/ViewModel/LocalVerificationViewModel.swift |
Updated to use UserDefaultsUtils (verification flag) |
ACON-iOS/ACON-iOS/Presentation/LocalVerification/View/LocalVerificationViewController.swift |
Updated to use UserDefaultsUtils (seen and alert date flags) |
ACON-iOS/ACON-iOS/Presentation/LocalVerification/View/VerificationReminderViewController.swift |
Updated to use UserDefaultsUtils (alert date) |
ACON-iOS/ACON-iOS/Presentation/SpotList/View/SpotListViewController.swift |
Updated to use UserDefaultsUtils (alert date retrieval) |
ACON-iOS/ACON-iOS/Global/Protocols/Serviceable.swift |
Updated to use UserDefaultsUtils (token removal on reissue failure) |
ACON-iOS/ACON-iOS/Global/Settings/Info.plist |
Version bump from 2.1.2 to 2.1.3 and build number update |
ACON-iOS/ACON-iOS.xcodeproj/project.pbxproj |
Project file updates for new files and renamed references; version bump |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
|
|
||
| // MARK: - Public | ||
|
|
||
| func log(_ event: TokenLogEvent) { | ||
| var logs = loadLogs() | ||
| let timestamp = formattedDate() | ||
| let entry = "[\(timestamp)] \(event.message)" | ||
| logs.append(entry) | ||
|
|
||
| // NOTE: 오래된 로그 제거 | ||
| if logs.count > maxLogCount { | ||
| logs = Array(logs.suffix(maxLogCount)) | ||
| } | ||
|
|
||
| saveLogs(logs) | ||
| print("🪵 TokenLog:", entry) | ||
| } | ||
|
|
||
| func loadLogs() -> [String] { | ||
| UserDefaultsUtils.get([String].self, forKey: .tokenLogs) ?? [] | ||
| } | ||
|
|
||
| func clearLogs() { | ||
| UserDefaultsUtils.remove(forKey: .tokenLogs) |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TokenLogger singleton's log() method is not thread-safe. Multiple threads could simultaneously call log(), leading to race conditions when reading, modifying, and saving the logs array. Since token-related operations can happen on different threads (e.g., network callbacks, UI operations), consider adding thread synchronization using a serial DispatchQueue or other synchronization mechanism.
| // MARK: - Public | |
| func log(_ event: TokenLogEvent) { | |
| var logs = loadLogs() | |
| let timestamp = formattedDate() | |
| let entry = "[\(timestamp)] \(event.message)" | |
| logs.append(entry) | |
| // NOTE: 오래된 로그 제거 | |
| if logs.count > maxLogCount { | |
| logs = Array(logs.suffix(maxLogCount)) | |
| } | |
| saveLogs(logs) | |
| print("🪵 TokenLog:", entry) | |
| } | |
| func loadLogs() -> [String] { | |
| UserDefaultsUtils.get([String].self, forKey: .tokenLogs) ?? [] | |
| } | |
| func clearLogs() { | |
| UserDefaultsUtils.remove(forKey: .tokenLogs) | |
| private let queue = DispatchQueue(label: "TokenLogger.queue") | |
| // MARK: - Public | |
| func log(_ event: TokenLogEvent) { | |
| queue.sync { | |
| var logs = UserDefaultsUtils.get([String].self, forKey: .tokenLogs) ?? [] | |
| let timestamp = formattedDate() | |
| let entry = "[\(timestamp)] \(event.message)" | |
| logs.append(entry) | |
| // NOTE: 오래된 로그 제거 | |
| if logs.count > maxLogCount { | |
| logs = Array(logs.suffix(maxLogCount)) | |
| } | |
| UserDefaultsUtils.set(logs, forKey: .tokenLogs) | |
| print("🪵 TokenLog:", entry) | |
| } | |
| } | |
| func loadLogs() -> [String] { | |
| queue.sync { | |
| UserDefaultsUtils.get([String].self, forKey: .tokenLogs) ?? [] | |
| } | |
| } | |
| func clearLogs() { | |
| queue.sync { | |
| UserDefaultsUtils.remove(forKey: .tokenLogs) | |
| } |
| UserDefaultsUtils.removeTokens() | ||
| TokenLogger.shared.log(.refreshFailed(error: error.localizedDescription)) | ||
| continuation.resume(returning: false) | ||
| } else { |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a token refresh fails with a requestErr case but the error code is not 40088, the failure is not logged. This makes it harder to debug auto-login issues when non-40088 errors occur. Consider adding a log entry for this case as well, similar to line 58 and 64.
| } else { | |
| } else { | |
| TokenLogger.shared.log(.refreshFailed(error: error.localizedDescription)) |
| private func formattedDate() -> String { | ||
| let formatter = DateFormatter() | ||
| formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" | ||
| return formatter.string(from: Date()) | ||
| } |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a new DateFormatter instance on every log call can be expensive. DateFormatter initialization is relatively slow. Consider creating a static or cached DateFormatter instance to improve performance, especially since logging may happen frequently during debugging sessions.
| _ = UIButton().then { | ||
| navigationBarView.addSubview($0) | ||
| $0.snp.makeConstraints { | ||
| $0.size.equalTo(20) | ||
| $0.leading.centerY.equalToSuperview() | ||
| } | ||
| $0.backgroundColor = .clear | ||
| $0.addTarget(self, action: #selector(presentTokenLogVC), for: .touchUpInside) | ||
| } |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The UIButton instance is created but immediately discarded using the underscore pattern. While the button is added to the navigationBarView's hierarchy which retains it, this pattern is unusual and potentially confusing. Consider storing the button as a property or using a more conventional approach without the underscore assignment.
| // NOTE: access token이 만료되었으면 true | ||
| func needsTokenRefresh() -> Bool { | ||
| guard let lastRefresh = UserDefaultsManager.get(Date.self, forKey: .lastTokenRefreshDate) else { | ||
| guard let lastRefresh = UserDefaultsUtils.get(Date.self, forKey: .lastTokenRefreshDate) else { |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When needsTokenRefresh() returns true because lastTokenRefreshDate is nil (line 79), no token log is generated. This means the first-time login scenario after app installation won't be logged with a clear state. Consider adding TokenLogger.shared.log(.tokenExpired) or a more specific log event before returning true on line 79.
| guard let lastRefresh = UserDefaultsUtils.get(Date.self, forKey: .lastTokenRefreshDate) else { | |
| guard let lastRefresh = UserDefaultsUtils.get(Date.self, forKey: .lastTokenRefreshDate) else { | |
| TokenLogger.shared.log(.tokenExpired) |
| } | ||
|
|
||
| AuthManager.shared.updateLastTokenRefreshDate() | ||
| TokenLogger.shared.log(.saved(tokenPrefix: String(data.accessToken.prefix(10)))) |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logging the first 10 characters of access tokens, even in debug/TestFlight builds, could pose a security risk. While this is limited to non-production builds, consider whether this amount of token information is necessary for debugging. A shorter prefix (e.g., 4-6 characters) or a hash of the token would be safer while still allowing token changes to be tracked.
| TokenLogger.shared.log(.saved(tokenPrefix: String(data.accessToken.prefix(10)))) | |
| TokenLogger.shared.log(.saved(tokenPrefix: String(data.accessToken.prefix(6)))) |
| UserDefaultsUtils.set(data.accessToken, forKey: .accessToken) | ||
| UserDefaultsUtils.set(data.refreshToken, forKey: .refreshToken) | ||
| AuthManager.shared.updateLastTokenRefreshDate() | ||
| TokenLogger.shared.log(.refreshSucceeded(tokenPrefix: String(data.accessToken.prefix(10)))) |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logging the first 10 characters of access tokens, even in debug/TestFlight builds, could pose a security risk. While this is limited to non-production builds, consider whether this amount of token information is necessary for debugging. A shorter prefix (e.g., 4-6 characters) or a hash of the token would be safer while still allowing token changes to be tracked.
| TokenLogger.shared.log(.refreshSucceeded(tokenPrefix: String(data.accessToken.prefix(10)))) | |
| TokenLogger.shared.log(.refreshSucceeded(tokenPrefix: String(data.accessToken.prefix(6)))) |
🐿️ Pull Requests
🪵 작업 브랜치
🥔 작업 내용
TestFlight - v2.1.3(2025.1014.1518)
1. 토큰 로그 구현
자동로그인이 풀리는 원인이 클라이언트에 있는지 서버에 있는지 파악하기 위해 토큰 로그를 볼 수 있는 기능을 추가했습니다.
[버튼 위치]
로그인 화면 좌측 상단(1번 사진 빨간색 위치)에 투명한 버튼을 숨겨두었습니다.
터치하면 2번 사진처럼 토큰 로그가 보여집니다.
[상세 기능]
[로그 종류]
// NOTE: 상태 로그 (스플래시 진입 시점 토큰 상태) case .valid: /// 액세스토큰 유효 (-> 자동로그인) return "✅ has valid access token" case .noToken: /// 토큰 없음 (로그아웃 상태) return "⚠️ no token found" case .tokenExpired: /// 액세스토큰 만료 return "⏱️ access token expired — requesting refresh" // NOTE: 액션 로그 case .saved(let tokenPrefix): /// 로그인 성공 시 return "💾 saved new access token (prefix: \(tokenPrefix))" case .cleared: /// 로그아웃/탈퇴 시 return "🗑️ Access token cleared" case .refreshSucceeded(let tokenPrefix): /// 액세스토큰 갱신 성공 시 return "💾 token refresh succeeded (prefix: \(tokenPrefix))" case .refreshFailed(let error): /// 액세스토큰 갱신 실패 시 return "❌ Token refresh failed: \(error ?? "unknown error")"2. 기타 변경사항
UserDefaultsManager가 단순 기능 모듈로 작동하고 있어,UserDefaultsUtils로 명칭을 변경하고, 폴더도 Global>Service에서 Global>Utils로 변경했습니다.📸 스크린샷
💥 To be sure
🌰 Resolve issue