diff --git a/ACON-iOS/ACON-iOS.xcodeproj/project.pbxproj b/ACON-iOS/ACON-iOS.xcodeproj/project.pbxproj index 310d2802..17bfc138 100644 --- a/ACON-iOS/ACON-iOS.xcodeproj/project.pbxproj +++ b/ACON-iOS/ACON-iOS.xcodeproj/project.pbxproj @@ -47,7 +47,7 @@ 15304FC22E33C57A00EFCDEF /* CafeFeatureSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15304FC12E33C57A00EFCDEF /* CafeFeatureSelectionViewController.swift */; }; 15304FC82E33EFA600EFCDEF /* SpotUploadSizeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15304FC72E33EFA600EFCDEF /* SpotUploadSizeType.swift */; }; 1530CC792DDFC0D100EB4AEC /* SpotNoImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1530CC782DDFC0D100EB4AEC /* SpotNoImageContentView.swift */; }; - 153B78252E74090700B772F9 /* UserDefaultsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 153B78242E74090700B772F9 /* UserDefaultsManager.swift */; }; + 153B78252E74090700B772F9 /* UserDefaultsUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 153B78242E74090700B772F9 /* UserDefaultsUtils.swift */; }; 15432E7D2E41ECCA0005DB90 /* PostSpotUploadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15432E7C2E41ECCA0005DB90 /* PostSpotUploadRequest.swift */; }; 15432E802E41ECDD0005DB90 /* SpotUploadTargetType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15432E7F2E41ECDD0005DB90 /* SpotUploadTargetType.swift */; }; 15432E822E41ECF40005DB90 /* SpotUploadService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15432E812E41ECF40005DB90 /* SpotUploadService.swift */; }; @@ -137,6 +137,9 @@ 15E71E422DF5A44B0020689D /* OpeningTimeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15E71E412DF5A44A0020689D /* OpeningTimeView.swift */; }; 15F15E832DBFD17A002F81E2 /* Pretendard-Light.otf in Resources */ = {isa = PBXBuildFile; fileRef = 15F15E822DBFD17A002F81E2 /* Pretendard-Light.otf */; }; 15F15E852DBFD206002F81E2 /* LanguageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15F15E842DBFD206002F81E2 /* LanguageType.swift */; }; + 15F1F6C42E9A5AB30014669E /* TokenLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15F1F6C32E9A5AB30014669E /* TokenLogger.swift */; }; + 15F1F6C62E9A5ED80014669E /* TokenLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15F1F6C52E9A5ED80014669E /* TokenLogViewController.swift */; }; + 15F1F6C82E9D8C530014669E /* BuildConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15F1F6C72E9D8C530014669E /* BuildConfig.swift */; }; 15F45F842D6664C900BD6B9C /* LoginLockOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15F45F832D6664C900BD6B9C /* LoginLockOverlayView.swift */; }; 15F45F862D66877200BD6B9C /* LoadingAnimatedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15F45F852D66877200BD6B9C /* LoadingAnimatedButton.swift */; }; 4C6B2F332D65A15D0089BCB6 /* WithdrawalTargetType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C6B2F312D65A15D0089BCB6 /* WithdrawalTargetType.swift */; }; @@ -374,7 +377,7 @@ 15304FC12E33C57A00EFCDEF /* CafeFeatureSelectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CafeFeatureSelectionViewController.swift; sourceTree = ""; }; 15304FC72E33EFA600EFCDEF /* SpotUploadSizeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotUploadSizeType.swift; sourceTree = ""; }; 1530CC782DDFC0D100EB4AEC /* SpotNoImageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotNoImageContentView.swift; sourceTree = ""; }; - 153B78242E74090700B772F9 /* UserDefaultsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsManager.swift; sourceTree = ""; }; + 153B78242E74090700B772F9 /* UserDefaultsUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsUtils.swift; sourceTree = ""; }; 15432E7C2E41ECCA0005DB90 /* PostSpotUploadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSpotUploadRequest.swift; sourceTree = ""; }; 15432E7F2E41ECDD0005DB90 /* SpotUploadTargetType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotUploadTargetType.swift; sourceTree = ""; }; 15432E812E41ECF40005DB90 /* SpotUploadService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotUploadService.swift; sourceTree = ""; }; @@ -462,6 +465,9 @@ 15E71E412DF5A44A0020689D /* OpeningTimeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpeningTimeView.swift; sourceTree = ""; }; 15F15E822DBFD17A002F81E2 /* Pretendard-Light.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-Light.otf"; sourceTree = ""; }; 15F15E842DBFD206002F81E2 /* LanguageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageType.swift; sourceTree = ""; }; + 15F1F6C32E9A5AB30014669E /* TokenLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenLogger.swift; sourceTree = ""; }; + 15F1F6C52E9A5ED80014669E /* TokenLogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenLogViewController.swift; sourceTree = ""; }; + 15F1F6C72E9D8C530014669E /* BuildConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildConfig.swift; sourceTree = ""; }; 15F45F832D6664C900BD6B9C /* LoginLockOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginLockOverlayView.swift; sourceTree = ""; }; 15F45F852D66877200BD6B9C /* LoadingAnimatedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingAnimatedButton.swift; sourceTree = ""; }; 4C6B2F2E2D65A15D0089BCB6 /* WithdrawalRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithdrawalRequest.swift; sourceTree = ""; }; @@ -1225,6 +1231,7 @@ 15BFF8B52D665A5B00909C4F /* Debug.xcconfig */, 15BFF8B62D665A5B00909C4F /* Release.xcconfig */, 74B25C312D2FF022008BDCB7 /* Shared.xcconfig */, + 15F1F6C72E9D8C530014669E /* BuildConfig.swift */, 74B25C2B2D2FEE8C008BDCB7 /* Config.swift */, ); path = Config; @@ -1340,7 +1347,6 @@ 741429662D5D550400B69528 /* AppVersionManager.swift */, 7462D0242D4262EF00580464 /* AuthManager.swift */, 1515CE0C2E00823B00A559A2 /* DeepLinkManager.swift */, - 153B78242E74090700B772F9 /* UserDefaultsManager.swift */, 150B4B292E7EE4BB00C34C76 /* PhotoManager.swift */, ); path = Service; @@ -1597,15 +1603,17 @@ 748D6F792D2BCCEF007690B4 /* Utils */ = { isa = PBXGroup; children = ( - 74C782182E01504B0053AA86 /* NavigatonUtils.swift */, - 152F8D162DF252560064022B /* MapRedirect */, + 74054ECD2D32549800D1CDE4 /* ACLocationManager.swift */, 746F5BF12DE00CE90081569B /* GlassBorderAttributes.swift */, 74E38D332D69B468009449D4 /* LocationUtils.swift */, - 74054ECD2D32549800D1CDE4 /* ACLocationManager.swift */, + 152F8D162DF252560064022B /* MapRedirect */, 74054EC92D32533800D1CDE4 /* MultitaskDelegate.swift */, + 74C782182E01504B0053AA86 /* NavigatonUtils.swift */, + 748D6F7F2D2BCD8F007690B4 /* ObservablePattern.swift */, 748D6F812D2BCDB0007690B4 /* ScreenUtils.swift */, 155BC2912DF09EB200E1744E /* ShadowColorCache.swift */, - 748D6F7F2D2BCD8F007690B4 /* ObservablePattern.swift */, + 15F1F6C32E9A5AB30014669E /* TokenLogger.swift */, + 153B78242E74090700B772F9 /* UserDefaultsUtils.swift */, 748D6F7B2D2BCCFF007690B4 /* Enums */, ); path = Utils; @@ -1694,6 +1702,7 @@ 748ECA6D2D3196ED00BBC981 /* LoginViewController.swift */, 15CD25832D40364F00320006 /* LoginModalView.swift */, 15CD25812D40364400320006 /* LoginModalViewController.swift */, + 15F1F6C52E9A5ED80014669E /* TokenLogViewController.swift */, ); path = View; sourceTree = ""; @@ -2219,6 +2228,7 @@ 74DDBA8B2DC45E3B00BF9824 /* ACButtonType.swift in Sources */, 150BA5812E56F8580058A06B /* LimitedSpotsTutorialViewController.swift in Sources */, 156AA72A2D6510E1005B8DCE /* GetNicknameValidityRequest.swift in Sources */, + 15F1F6C82E9D8C530014669E /* BuildConfig.swift in Sources */, 74D297F82D63467900DDEE31 /* ImageService.swift in Sources */, 15A147212D5B256D003793EE /* LocalVerificationFlowType.swift in Sources */, 15A167E22E3D722B00062C49 /* AlbumFlowType.swift in Sources */, @@ -2328,11 +2338,12 @@ 1576806E2DFA06A70079B255 /* SpotListSkeletonHeader.swift in Sources */, 74BF92122D391FFE00B923E3 /* LocalMapViewController.swift in Sources */, D6E8168E2D6228F5001E4EBF /* WithdrawalViewController.swift in Sources */, + 15F1F6C42E9A5AB30014669E /* TokenLogger.swift in Sources */, 740968B72DEC3CA700AFF624 /* VerifiedAreasCollectionViewCell.swift in Sources */, 740EE8282E059875007A5DCF /* GetAppUpdateRequest.swift in Sources */, 15304FC02E33BAA900EFCDEF /* ValueRatingViewController.swift in Sources */, 15F45F842D6664C900BD6B9C /* LoginLockOverlayView.swift in Sources */, - 153B78252E74090700B772F9 /* UserDefaultsManager.swift in Sources */, + 153B78252E74090700B772F9 /* UserDefaultsUtils.swift in Sources */, 746261692D3EA33300A4E84F /* PostReviewRequest.swift in Sources */, 15CD257E2D3FF4F200320006 /* SpotListTargetType.swift in Sources */, 15A246142DE79D7E00469272 /* NoMatchingSpotType.swift in Sources */, @@ -2355,6 +2366,7 @@ 74C914D12D64DBBB00BC13E1 /* ImageType.swift in Sources */, 74BF92152D391FFE00B923E3 /* LocalVerificationView.swift in Sources */, 151FBFBA2DBD2F1B005F4D0D /* ACFontType.swift in Sources */, + 15F1F6C62E9A5ED80014669E /* TokenLogViewController.swift in Sources */, 15AA6D1A2D68C4AD008021C6 /* ReviewVerificationErrorType.swift in Sources */, 741429652D5D470200B69528 /* SettingViewModel.swift in Sources */, 15CD256B2D3D61F500320006 /* GlassmorphismView.swift in Sources */, @@ -2468,7 +2480,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2025.1004.1036; + CURRENT_PROJECT_VERSION = 2025.1014.1518; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KX5Q77JSUF; @@ -2487,7 +2499,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.2; + MARKETING_VERSION = 2.1.3; PRODUCT_BUNDLE_IDENTIFIER = "com.ACON.ACON-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2511,7 +2523,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2025.1004.1036; + CURRENT_PROJECT_VERSION = 2025.1014.1518; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KX5Q77JSUF; GENERATE_INFOPLIST_FILE = NO; @@ -2529,7 +2541,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.2; + MARKETING_VERSION = 2.1.3; PRODUCT_BUNDLE_IDENTIFIER = "com.ACON.ACON-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/ACON-iOS/ACON-iOS/Global/Protocols/Serviceable.swift b/ACON-iOS/ACON-iOS/Global/Protocols/Serviceable.swift index 531f68d4..62f2f396 100644 --- a/ACON-iOS/ACON-iOS/Global/Protocols/Serviceable.swift +++ b/ACON-iOS/ACON-iOS/Global/Protocols/Serviceable.swift @@ -23,13 +23,13 @@ extension Serviceable { print("❄️ 기존 서버통신 다시 성공") retryAction() } else { - UserDefaultsManager.removeTokens() + UserDefaultsUtils.removeTokens() NavigationUtils.navigateToSplash() } } } catch { DispatchQueue.main.async { - UserDefaultsManager.removeTokens() + UserDefaultsUtils.removeTokens() NavigationUtils.navigateToSplash() } } diff --git a/ACON-iOS/ACON-iOS/Global/Service/AuthManager.swift b/ACON-iOS/ACON-iOS/Global/Service/AuthManager.swift index 808df14b..253cd110 100644 --- a/ACON-iOS/ACON-iOS/Global/Service/AuthManager.swift +++ b/ACON-iOS/ACON-iOS/Global/Service/AuthManager.swift @@ -16,49 +16,52 @@ final class AuthManager { private let refreshInterval: TimeInterval = 3 * 60 * 60 var hasToken: Bool { - return UserDefaultsManager.get(String.self, forKey: .accessToken) != nil + return UserDefaultsUtils.get(String.self, forKey: .accessToken) != nil } var hasVerifiedArea: Bool { - return UserDefaultsManager.get(Bool.self, forKey: .hasVerifiedArea) ?? false + return UserDefaultsUtils.get(Bool.self, forKey: .hasVerifiedArea) ?? false } var hasPreference: Bool { - return UserDefaultsManager.get(Bool.self, forKey: .hasPreference) ?? false + return UserDefaultsUtils.get(Bool.self, forKey: .hasPreference) ?? false } var hasSeenTutorial: Bool { - return UserDefaultsManager.get(Bool.self, forKey: .hasSeenTutorial) ?? false + return UserDefaultsUtils.get(Bool.self, forKey: .hasSeenTutorial) ?? false } var hasSeenLocalVerification: Bool { - return UserDefaultsManager.get(Bool.self, forKey: .hasSeenLocalVerification) ?? false + return UserDefaultsUtils.get(Bool.self, forKey: .hasSeenLocalVerification) ?? false } var hasSeenPreference: Bool { - return UserDefaultsManager.get(Bool.self, forKey: .hasSeenPreference) ?? false + return UserDefaultsUtils.get(Bool.self, forKey: .hasSeenPreference) ?? false } func handleTokenRefresh() async throws -> Bool { - let refreshToken = UserDefaultsManager.get(String.self, forKey: .refreshToken) ?? "" + let refreshToken = UserDefaultsUtils.get(String.self, forKey: .refreshToken) ?? "" return try await withCheckedThrowingContinuation { continuation in ACService.shared.authService.postReissue(PostReissueRequest(refreshToken: refreshToken)) { response in switch response { case .success(let data): print("❄️ token refreshed success") - UserDefaultsManager.set(data.accessToken, forKey: .accessToken) - UserDefaultsManager.set(data.refreshToken, forKey: .refreshToken) + 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)))) continuation.resume(returning: true) case .requestErr(let error): if error.code == 40088 { print("❄️ remove token") - UserDefaultsManager.removeTokens() + UserDefaultsUtils.removeTokens() + TokenLogger.shared.log(.refreshFailed(error: error.localizedDescription)) continuation.resume(returning: false) } else { continuation.resume(returning: false) } default: + TokenLogger.shared.log(.refreshFailed(error: "unknown")) continuation.resume(returning: false) } } @@ -67,16 +70,17 @@ final class AuthManager { func updateLastTokenRefreshDate() { let now = Date() - UserDefaultsManager.set(now, forKey: .lastTokenRefreshDate) + UserDefaultsUtils.set(now, forKey: .lastTokenRefreshDate) } // 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 { return true } let elapsed = Date().timeIntervalSince(lastRefresh) print("❄️ token elapsed: \(elapsed) / \(refreshInterval) seconds") + TokenLogger.shared.log(elapsed >= refreshInterval ? .tokenExpired : .valid) return elapsed >= refreshInterval } diff --git a/ACON-iOS/ACON-iOS/Global/Settings/Config/BuildConfig.swift b/ACON-iOS/ACON-iOS/Global/Settings/Config/BuildConfig.swift new file mode 100644 index 00000000..115ce372 --- /dev/null +++ b/ACON-iOS/ACON-iOS/Global/Settings/Config/BuildConfig.swift @@ -0,0 +1,18 @@ +// +// BuildConfig.swift +// ACON-iOS +// +// Created by 김유림 on 10/14/25. +// + +import Foundation + +enum BuildConfig { + + #if DEBUG + static let isDebug = true + #else + static let isDebug = false + #endif + +} diff --git a/ACON-iOS/ACON-iOS/Global/Settings/Info.plist b/ACON-iOS/ACON-iOS/Global/Settings/Info.plist index cf22bfe6..5a687299 100644 --- a/ACON-iOS/ACON-iOS/Global/Settings/Info.plist +++ b/ACON-iOS/ACON-iOS/Global/Settings/Info.plist @@ -29,7 +29,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 2.1.2 + 2.1.3 CFBundleURLTypes @@ -54,7 +54,7 @@ CFBundleVersion - 2025.1004.1036 + 2025.1014.1518 GADApplicationIdentifier ${GAD_APPLICATION_IDENTIFIER} GAD_AD_UNIT_ID diff --git a/ACON-iOS/ACON-iOS/Global/Utils/Enums/HeaderType.swift b/ACON-iOS/ACON-iOS/Global/Utils/Enums/HeaderType.swift index eef2e60b..a5be1c3e 100644 --- a/ACON-iOS/ACON-iOS/Global/Utils/Enums/HeaderType.swift +++ b/ACON-iOS/ACON-iOS/Global/Utils/Enums/HeaderType.swift @@ -18,7 +18,7 @@ enum HeaderType { } static func headerWithToken() -> [String: String] { - if let token = UserDefaultsManager.get(String.self, forKey: .accessToken) { + if let token = UserDefaultsUtils.get(String.self, forKey: .accessToken) { return ["Content-Type" : "application/json", "Authorization" : "Bearer " + token] } else { return basicHeader @@ -26,7 +26,7 @@ enum HeaderType { } static func tokenOnly() -> [String:String] { - if let token = UserDefaultsManager.get(String.self, forKey: .accessToken) { + if let token = UserDefaultsUtils.get(String.self, forKey: .accessToken) { return ["Authorization" : "Bearer " + token] } else { return noHeader diff --git a/ACON-iOS/ACON-iOS/Global/Utils/TokenLogger.swift b/ACON-iOS/ACON-iOS/Global/Utils/TokenLogger.swift new file mode 100644 index 00000000..835bc879 --- /dev/null +++ b/ACON-iOS/ACON-iOS/Global/Utils/TokenLogger.swift @@ -0,0 +1,96 @@ +// +// TokenLogger.swift +// ACON-iOS +// +// Created by 김유림 on 10/11/25. +// + +import Foundation + +final class TokenLogger { + + static let shared = TokenLogger() + private init() {} + + private let maxLogCount = 100 + + + // 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) + } + + + // MARK: - Private + + private func saveLogs(_ logs: [String]) { + UserDefaultsUtils.set(logs, forKey: .tokenLogs) + } + + private func formattedDate() -> String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + return formatter.string(from: Date()) + } + +} + + +// MARK: - Enum + +enum TokenLogEvent { + + // 상태 로그 + case valid + case noToken + case tokenExpired + + // 액션 로그 + case saved(tokenPrefix: String) + case cleared + case refreshSucceeded(tokenPrefix: String) + case refreshFailed(error: String?) + + var message: String { + switch self { + // 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")" + } + } + +} diff --git a/ACON-iOS/ACON-iOS/Global/Service/UserDefaultsManager.swift b/ACON-iOS/ACON-iOS/Global/Utils/UserDefaultsUtils.swift similarity index 82% rename from ACON-iOS/ACON-iOS/Global/Service/UserDefaultsManager.swift rename to ACON-iOS/ACON-iOS/Global/Utils/UserDefaultsUtils.swift index 442ff7c7..832f7a2e 100644 --- a/ACON-iOS/ACON-iOS/Global/Service/UserDefaultsManager.swift +++ b/ACON-iOS/ACON-iOS/Global/Utils/UserDefaultsUtils.swift @@ -1,5 +1,5 @@ // -// UserDefaultsManager.swift +// UserDefaultsUtils.swift // ACON-iOS // // Created by 김유림 on 9/12/25. @@ -7,7 +7,7 @@ import Foundation -struct UserDefaultsManager { +struct UserDefaultsUtils { enum Keys: String, CaseIterable { case accessToken @@ -22,6 +22,8 @@ struct UserDefaultsManager { case lastTokenRefreshDate case lastLocalVerificationAlertDate + + case tokenLogs // NOTE: 초기화되면 안 됨 } @@ -52,20 +54,25 @@ struct UserDefaultsManager { /// - `hasSeenTutorial` /// - `hasSeenLocalVerification` /// - `hasSeenPreference` + /// - 토큰 로그는 유지됩니다. + /// - `tokenLogs` static func resetAppUserDefaults() { for key in Keys.allCases { if key == .hasSeenTutorial || key == .hasSeenLocalVerification - || key == .hasSeenPreference { continue } + || key == .hasSeenPreference + || key == .tokenLogs { continue } remove(forKey: key) } + TokenLogger.shared.log(.cleared) } static func removeTokens() { [Keys.accessToken, Keys.refreshToken].forEach { remove(forKey: $0) } + TokenLogger.shared.log(.cleared) } } diff --git a/ACON-iOS/ACON-iOS/Presentation/LocalVerification/View/LocalVerificationViewController.swift b/ACON-iOS/ACON-iOS/Presentation/LocalVerification/View/LocalVerificationViewController.swift index 5a249c6c..02d1f223 100644 --- a/ACON-iOS/ACON-iOS/Presentation/LocalVerification/View/LocalVerificationViewController.swift +++ b/ACON-iOS/ACON-iOS/Presentation/LocalVerification/View/LocalVerificationViewController.swift @@ -41,7 +41,7 @@ class LocalVerificationViewController: BaseNavViewController { if self.localVerificationViewModel.flowType == .onboarding { self.setSkipButton() { let now = Date() - UserDefaultsManager.set(now, forKey: .lastLocalVerificationAlertDate) + UserDefaultsUtils.set(now, forKey: .lastLocalVerificationAlertDate) NavigationUtils.naviateToOnboardingPreference() } @@ -61,7 +61,7 @@ class LocalVerificationViewController: BaseNavViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - UserDefaultsManager.set(true, forKey: .hasSeenLocalVerification) + UserDefaultsUtils.set(true, forKey: .hasSeenLocalVerification) } diff --git a/ACON-iOS/ACON-iOS/Presentation/LocalVerification/View/VerificationReminderViewController.swift b/ACON-iOS/ACON-iOS/Presentation/LocalVerification/View/VerificationReminderViewController.swift index b83625c7..4202f0c9 100644 --- a/ACON-iOS/ACON-iOS/Presentation/LocalVerification/View/VerificationReminderViewController.swift +++ b/ACON-iOS/ACON-iOS/Presentation/LocalVerification/View/VerificationReminderViewController.swift @@ -38,7 +38,7 @@ private extension VerificationReminderViewController { @objc func cancelButtonTapped() { let now = Date() - UserDefaultsManager.set(now, forKey: .lastLocalVerificationAlertDate) + UserDefaultsUtils.set(now, forKey: .lastLocalVerificationAlertDate) dismiss(animated: true) } diff --git a/ACON-iOS/ACON-iOS/Presentation/LocalVerification/ViewModel/LocalVerificationViewModel.swift b/ACON-iOS/ACON-iOS/Presentation/LocalVerification/ViewModel/LocalVerificationViewModel.swift index 6fba6414..97dbfac2 100644 --- a/ACON-iOS/ACON-iOS/Presentation/LocalVerification/ViewModel/LocalVerificationViewModel.swift +++ b/ACON-iOS/ACON-iOS/Presentation/LocalVerification/ViewModel/LocalVerificationViewModel.swift @@ -68,7 +68,7 @@ class LocalVerificationViewModel: Serviceable { case .success: self?.onPostLocalAreaSuccess.value = true if !AuthManager.shared.hasVerifiedArea { - UserDefaultsManager.set(true, forKey: .hasVerifiedArea) + UserDefaultsUtils.set(true, forKey: .hasVerifiedArea) } case .requestErr(let error): self?.onPostLocalAreaSuccess.value = false diff --git a/ACON-iOS/ACON-iOS/Presentation/Login/View/LoginViewController.swift b/ACON-iOS/ACON-iOS/Presentation/Login/View/LoginViewController.swift index c7db86fe..990537ac 100644 --- a/ACON-iOS/ACON-iOS/Presentation/Login/View/LoginViewController.swift +++ b/ACON-iOS/ACON-iOS/Presentation/Login/View/LoginViewController.swift @@ -35,6 +35,10 @@ class LoginViewController: BaseNavViewController { ? NavigationUtils.navigateToTabBar() : NavigationUtils.navigateToTutorial() } + + if BuildConfig.isDebug || Bundle.main.isTestFlight { + setTokenLogButton() + } } override func viewWillAppear(_ animated: Bool) { @@ -54,7 +58,10 @@ class LoginViewController: BaseNavViewController { } } } - + + + // MARK: - UI Setting + override func setHierarchy() { super.setHierarchy() @@ -184,3 +191,28 @@ extension LoginViewController: ASAuthorizationControllerDelegate { } } + + +// MARK: - Set Token Log Button + +private extension LoginViewController { + + func setTokenLogButton() { + _ = 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) + } + } + + @objc + func presentTokenLogVC() { + let tokenLogVC = TokenLogViewController() + self.present(tokenLogVC, animated: true) + } + +} diff --git a/ACON-iOS/ACON-iOS/Presentation/Login/View/TokenLogViewController.swift b/ACON-iOS/ACON-iOS/Presentation/Login/View/TokenLogViewController.swift new file mode 100644 index 00000000..a7463ec0 --- /dev/null +++ b/ACON-iOS/ACON-iOS/Presentation/Login/View/TokenLogViewController.swift @@ -0,0 +1,78 @@ +// +// TokenLogViewController.swift +// ACON-iOS +// +// Created by 김유림 on 10/11/25. +// + +import UIKit + +final class TokenLogViewController: BaseViewController { + + // MARK: - UI Properties + + private let textView = UITextView() + private let clearButton = UIButton(type: .system) + + + // MARK: - Life Cycles + + override func viewDidLoad() { + super.viewDidLoad() + + addTarget() + loadLogs() + } + + // MARK: - UI Settings + + override func setHierarchy() { + super.setHierarchy() + + view.addSubviews(clearButton, textView) + } + + override func setLayout() { + super.setLayout() + + clearButton.snp.makeConstraints { + $0.top.equalToSuperview().offset(12) + $0.trailing.equalToSuperview().inset(ScreenUtils.horizontalInset) + } + + textView.snp.makeConstraints { + $0.top.equalTo(clearButton.snp.bottom).offset(8) + $0.horizontalEdges.equalToSuperview().inset(12) + $0.bottom.equalTo(view.safeAreaLayoutGuide) + } + } + + override func setStyle() { + super.setStyle() + + textView.do { + $0.isEditable = false + $0.font = .monospacedSystemFont(ofSize: 13, weight: .regular) + } + + clearButton.setTitle("Clear Logs", for: .normal) + } + + private func addTarget() { + clearButton.addTarget(self, action: #selector(clearLogs), for: .touchUpInside) + } + + @objc private func clearLogs() { + TokenLogger.shared.clearLogs() + loadLogs() + } + + + // MARK: - Helper + + private func loadLogs() { + let logs = TokenLogger.shared.loadLogs().reversed() + textView.text = logs.joined(separator: "\n\n") + } + +} diff --git a/ACON-iOS/ACON-iOS/Presentation/Login/ViewModel/LoginViewModel.swift b/ACON-iOS/ACON-iOS/Presentation/Login/ViewModel/LoginViewModel.swift index 130fd8dd..9baaa1be 100644 --- a/ACON-iOS/ACON-iOS/Presentation/Login/ViewModel/LoginViewModel.swift +++ b/ACON-iOS/ACON-iOS/Presentation/Login/ViewModel/LoginViewModel.swift @@ -54,18 +54,19 @@ class LoginViewModel: Serviceable { ACService.shared.authService.postLogin(PostLoginRequest(socialType: socialType, idToken: idToken)){ [weak self] response in switch response { case .success(let data): - UserDefaultsManager.set(data.accessToken, forKey: .accessToken) - UserDefaultsManager.set(data.refreshToken, forKey: .refreshToken) - UserDefaultsManager.set(data.hasVerifiedArea, forKey: .hasVerifiedArea) - UserDefaultsManager.set(data.hasPreference, forKey: .hasPreference) + UserDefaultsUtils.set(data.accessToken, forKey: .accessToken) + UserDefaultsUtils.set(data.refreshToken, forKey: .refreshToken) + UserDefaultsUtils.set(data.hasVerifiedArea, forKey: .hasVerifiedArea) + UserDefaultsUtils.set(data.hasPreference, forKey: .hasPreference) // NOTE: 기존 유저가 앱 재설치 시 서비스 온보딩 노출 X // NOTE: 기존 유저인지는 취향탐색 또는 지역인증을 했는지로 판단 - if !(UserDefaultsManager.get(Bool.self, forKey: .hasSeenTutorial) ?? false) { - UserDefaultsManager.set((data.hasPreference || data.hasVerifiedArea), forKey: .hasSeenTutorial) + if !(UserDefaultsUtils.get(Bool.self, forKey: .hasSeenTutorial) ?? false) { + UserDefaultsUtils.set((data.hasPreference || data.hasVerifiedArea), forKey: .hasSeenTutorial) } AuthManager.shared.updateLastTokenRefreshDate() + TokenLogger.shared.log(.saved(tokenPrefix: String(data.accessToken.prefix(10)))) AmplitudeManager.shared.setUserID(data.externalUUID) AmplitudeManager.shared.setUserProperty(userProperties: ["id": data.externalUUID]) diff --git a/ACON-iOS/ACON-iOS/Presentation/Preference/View/PreferenceViewController.swift b/ACON-iOS/ACON-iOS/Presentation/Preference/View/PreferenceViewController.swift index 63571380..adb5867c 100644 --- a/ACON-iOS/ACON-iOS/Presentation/Preference/View/PreferenceViewController.swift +++ b/ACON-iOS/ACON-iOS/Presentation/Preference/View/PreferenceViewController.swift @@ -54,7 +54,7 @@ class PreferenceViewController: BaseViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - UserDefaultsManager.set(true, forKey: .hasSeenPreference) + UserDefaultsUtils.set(true, forKey: .hasSeenPreference) } diff --git a/ACON-iOS/ACON-iOS/Presentation/Preference/ViewModel/PreferenceViewModel.swift b/ACON-iOS/ACON-iOS/Presentation/Preference/ViewModel/PreferenceViewModel.swift index 65cd23f3..7ce11065 100644 --- a/ACON-iOS/ACON-iOS/Presentation/Preference/ViewModel/PreferenceViewModel.swift +++ b/ACON-iOS/ACON-iOS/Presentation/Preference/ViewModel/PreferenceViewModel.swift @@ -23,7 +23,7 @@ class PreferenceViewModel: Serviceable { case .success: onPutPreferenceSuccess.value = true if !AuthManager.shared.hasPreference { - UserDefaultsManager.set(true, forKey: .hasPreference) + UserDefaultsUtils.set(true, forKey: .hasPreference) } case .reIssueJWT: self.handleReissue { diff --git a/ACON-iOS/ACON-iOS/Presentation/Profile/View/ProfileSettingViewController.swift b/ACON-iOS/ACON-iOS/Presentation/Profile/View/ProfileSettingViewController.swift index d39a54ed..e17011a9 100644 --- a/ACON-iOS/ACON-iOS/Presentation/Profile/View/ProfileSettingViewController.swift +++ b/ACON-iOS/ACON-iOS/Presentation/Profile/View/ProfileSettingViewController.swift @@ -82,7 +82,7 @@ extension ProfileSettingViewController { NavigationUtils.navigateToSplash() } else { self?.showServerErrorAlert { - UserDefaultsManager.removeTokens() + UserDefaultsUtils.removeTokens() NavigationUtils.navigateToSplash() } } diff --git a/ACON-iOS/ACON-iOS/Presentation/Profile/ViewModel/SettingViewModel.swift b/ACON-iOS/ACON-iOS/Presentation/Profile/ViewModel/SettingViewModel.swift index a663bca7..41493c48 100644 --- a/ACON-iOS/ACON-iOS/Presentation/Profile/ViewModel/SettingViewModel.swift +++ b/ACON-iOS/ACON-iOS/Presentation/Profile/ViewModel/SettingViewModel.swift @@ -12,13 +12,13 @@ final class SettingViewModel: Serviceable { var onPostLogoutSuccess: ObservablePattern = ObservablePattern(nil) func postLogout() { - let refreshToken = UserDefaultsManager.get(String.self, forKey: .refreshToken) ?? "" + let refreshToken = UserDefaultsUtils.get(String.self, forKey: .refreshToken) ?? "" ACService.shared.authService.postLogout( PostLogoutRequest(refreshToken: refreshToken)) { result in switch result { case .success: - UserDefaultsManager.resetAppUserDefaults() + UserDefaultsUtils.resetAppUserDefaults() AmplitudeManager.shared.reset() self.onPostLogoutSuccess.value = true case .reIssueJWT: diff --git a/ACON-iOS/ACON-iOS/Presentation/Splash/View/SplashViewController.swift b/ACON-iOS/ACON-iOS/Presentation/Splash/View/SplashViewController.swift index 87bdb89c..2c561c84 100644 --- a/ACON-iOS/ACON-iOS/Presentation/Splash/View/SplashViewController.swift +++ b/ACON-iOS/ACON-iOS/Presentation/Splash/View/SplashViewController.swift @@ -34,9 +34,14 @@ class SplashViewController: BaseViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - if AuthManager.shared.needsTokenRefresh() { - refreshToken() + if AuthManager.shared.hasToken { + if AuthManager.shared.needsTokenRefresh() { + refreshToken() + } + } else { + TokenLogger.shared.log(.noToken) } + } override func viewDidAppear(_ animated: Bool) { @@ -189,14 +194,14 @@ private extension SplashViewController { print("❄️ 토큰 갱신 성공") } else { print("❄️ 토큰 갱신 실패") - UserDefaultsManager.resetAppUserDefaults() + UserDefaultsUtils.resetAppUserDefaults() NavigationUtils.navigateToLoginVC() } } } catch { DispatchQueue.main.async { print("❄️ 토큰 갱신 실패 catch") - UserDefaultsManager.resetAppUserDefaults() + UserDefaultsUtils.resetAppUserDefaults() NavigationUtils.navigateToLoginVC() } } diff --git a/ACON-iOS/ACON-iOS/Presentation/SpotList/View/SpotListViewController.swift b/ACON-iOS/ACON-iOS/Presentation/SpotList/View/SpotListViewController.swift index 2c3f3cce..04dce204 100644 --- a/ACON-iOS/ACON-iOS/Presentation/SpotList/View/SpotListViewController.swift +++ b/ACON-iOS/ACON-iOS/Presentation/SpotList/View/SpotListViewController.swift @@ -749,7 +749,7 @@ private extension SpotListViewController { guard AuthManager.shared.hasToken else { return } guard !AuthManager.shared.hasVerifiedArea else { return } - let lastAlertTime = UserDefaultsManager.get(Date.self, forKey: .lastLocalVerificationAlertDate) + let lastAlertTime = UserDefaultsUtils.get(Date.self, forKey: .lastLocalVerificationAlertDate) let now = Date() if let lastTime = lastAlertTime { diff --git a/ACON-iOS/ACON-iOS/Presentation/Tutorial/View/TutorialContainerViewController.swift b/ACON-iOS/ACON-iOS/Presentation/Tutorial/View/TutorialContainerViewController.swift index 08152d9e..891ef6f0 100644 --- a/ACON-iOS/ACON-iOS/Presentation/Tutorial/View/TutorialContainerViewController.swift +++ b/ACON-iOS/ACON-iOS/Presentation/Tutorial/View/TutorialContainerViewController.swift @@ -44,7 +44,7 @@ class TutorialContainerViewController: BaseViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - UserDefaultsManager.set(true, forKey: .hasSeenTutorial) + UserDefaultsUtils.set(true, forKey: .hasSeenTutorial) } diff --git a/ACON-iOS/ACON-iOS/Presentation/Withdrawal/View/WithdrawalConfirmationViewController.swift b/ACON-iOS/ACON-iOS/Presentation/Withdrawal/View/WithdrawalConfirmationViewController.swift index edfd59d6..16ed8e18 100644 --- a/ACON-iOS/ACON-iOS/Presentation/Withdrawal/View/WithdrawalConfirmationViewController.swift +++ b/ACON-iOS/ACON-iOS/Presentation/Withdrawal/View/WithdrawalConfirmationViewController.swift @@ -43,7 +43,7 @@ extension WithdrawalConfirmationViewController { AmplitudeManager.shared.reset() } else { self.showServerErrorAlert { - UserDefaultsManager.removeTokens() + UserDefaultsUtils.removeTokens() NavigationUtils.navigateToSplash() } } diff --git a/ACON-iOS/ACON-iOS/Presentation/Withdrawal/ViewModel/WithdrawalViewModel.swift b/ACON-iOS/ACON-iOS/Presentation/Withdrawal/ViewModel/WithdrawalViewModel.swift index 6bfb851f..28919742 100644 --- a/ACON-iOS/ACON-iOS/Presentation/Withdrawal/ViewModel/WithdrawalViewModel.swift +++ b/ACON-iOS/ACON-iOS/Presentation/Withdrawal/ViewModel/WithdrawalViewModel.swift @@ -42,7 +42,7 @@ final class WithdrawalViewModel: Serviceable { } func postWithdrawal() { - let refreshToken = UserDefaultsManager.get(String.self, forKey: .refreshToken) ?? "" + let refreshToken = UserDefaultsUtils.get(String.self, forKey: .refreshToken) ?? "" guard let reasonText = selectedOption.value else { return } @@ -50,7 +50,7 @@ final class WithdrawalViewModel: Serviceable { WithdrawalRequest(reason: reasonText, refreshToken: refreshToken)) { result in switch result { case .success: - UserDefaultsManager.resetAppUserDefaults() + UserDefaultsUtils.resetAppUserDefaults() self.onSuccessPostWithdrawal.value = true case .reIssueJWT: self.handleReissue { [weak self] in