Skip to content

Conversation

@yurim830
Copy link
Collaborator

@yurim830 yurim830 commented Oct 14, 2025

🐿️ Pull Requests

🪵 작업 브랜치

🥔 작업 내용

TestFlight - v2.1.3(2025.1014.1518)

1. 토큰 로그 구현

자동로그인이 풀리는 원인이 클라이언트에 있는지 서버에 있는지 파악하기 위해 토큰 로그를 볼 수 있는 기능을 추가했습니다.

[버튼 위치]
로그인 화면 좌측 상단(1번 사진 빨간색 위치)에 투명한 버튼을 숨겨두었습니다.
터치하면 2번 사진처럼 토큰 로그가 보여집니다.

[상세 기능]

  • 로그는 최대 100줄만 저장되며, 100줄 초과 시, 오래된 순으로 삭제됩니다.
  • 로그 화면의 "Clear logs" 버튼 클릭 시, 모든 로그가 삭제됩니다.
  • 로그는 디버그 빌드 및 TestFlight 배포에서만 볼 수 있습니다. (AppStore 앱에서는 기능 안함)

[로그 종류]

        // 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로 변경했습니다.

📸 스크린샷

1. 로그 버튼 위치 2. 로그 뷰 3. gif

💥 To be sure

  • 모든 뷰가 잘 실행되는지 다시 한 번 체크해주세요 !

🌰 Resolve issue

@yurim830 yurim830 requested a review from cirtuare October 14, 2025 06:13
@yurim830 yurim830 self-assigned this Oct 14, 2025
@yurim830 yurim830 linked an issue Oct 14, 2025 that may be closed by this pull request
1 task
@yurim830 yurim830 added 🍋‍🟩 chore 기타 작업들 🥑 유림 유림 🌀 feature 새로운 기능 개발 and removed 🍋‍🟩 chore 기타 작업들 labels Oct 16, 2025
@yurim830 yurim830 requested a review from Copilot December 20, 2025 08:47
Copy link
Contributor

Copilot AI left a 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 TokenLogger singleton 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 UserDefaultsManager to UserDefaultsUtils across 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.

Comment on lines +16 to +40


// 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)
Copy link

Copilot AI Dec 20, 2025

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.

Suggested change
// 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)
}

Copilot uses AI. Check for mistakes.
UserDefaultsUtils.removeTokens()
TokenLogger.shared.log(.refreshFailed(error: error.localizedDescription))
continuation.resume(returning: false)
} else {
Copy link

Copilot AI Dec 20, 2025

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.

Suggested change
} else {
} else {
TokenLogger.shared.log(.refreshFailed(error: error.localizedDescription))

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +54
private func formattedDate() -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return formatter.string(from: Date())
}
Copy link

Copilot AI Dec 20, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +201 to +209
_ = 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)
}
Copy link

Copilot AI Dec 20, 2025

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.

Copilot uses AI. Check for mistakes.
// 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 {
Copy link

Copilot AI Dec 20, 2025

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.

Suggested change
guard let lastRefresh = UserDefaultsUtils.get(Date.self, forKey: .lastTokenRefreshDate) else {
guard let lastRefresh = UserDefaultsUtils.get(Date.self, forKey: .lastTokenRefreshDate) else {
TokenLogger.shared.log(.tokenExpired)

Copilot uses AI. Check for mistakes.
}

AuthManager.shared.updateLastTokenRefreshDate()
TokenLogger.shared.log(.saved(tokenPrefix: String(data.accessToken.prefix(10))))
Copy link

Copilot AI Dec 20, 2025

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.

Suggested change
TokenLogger.shared.log(.saved(tokenPrefix: String(data.accessToken.prefix(10))))
TokenLogger.shared.log(.saved(tokenPrefix: String(data.accessToken.prefix(6))))

Copilot uses AI. Check for mistakes.
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))))
Copy link

Copilot AI Dec 20, 2025

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.

Suggested change
TokenLogger.shared.log(.refreshSucceeded(tokenPrefix: String(data.accessToken.prefix(10))))
TokenLogger.shared.log(.refreshSucceeded(tokenPrefix: String(data.accessToken.prefix(6))))

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🌀 feature 새로운 기능 개발 🥑 유림 유림

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 자동로그인 버그 추적을 위한 Token logger 구현

2 participants