-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] 소셜 로그인 및 자동 로그인 기능 구현 #147
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
Conversation
자동 로그인 상태를 따로 업데이트 해줄 경우가 없어 이를 고려해 삭제함
WalkthroughThis pull request makes extensive modifications across the project. It updates the build configuration to include new source files and remove obsolete ones while restructuring source groups. Changes span session management, authentication (for both Apple and Kakao), token storage, and logging improvements. The networking layer is adjusted for clearer error handling and response mapping, and the presentation layer now leverages Combine for reactive flows. Additionally, a new OAuth event manager is introduced, and the Info.plist is modified to support a custom URL scheme. Changes
Sequence Diagram(s)sequenceDiagram
participant SD as SceneDelegate
participant OAUTH as OAuthEventManager
participant Log as WableLogger
SD->>OAUTH: Subscribe to tokenExpiredSubject
OAUTH-->>SD: Token expired event emitted
SD->>SD: Call handleTokenExpired()
SD->>Log: Log token expiration
sequenceDiagram
participant User as User
participant LVC as LoginViewController
participant LVM as LoginViewModel
participant FUA as FetchUserAuthUseCase
participant TS as TokenStorage
User->>LVC: Tap login button
LVC->>LVM: Publish login trigger via tapPublisher
LVM->>FUA: Execute login use case
FUA->>TS: Save access & refresh tokens
TS-->>FUA: Return token string
FUA->>LVM: Return account data
LVM->>LVC: Emit loginSuccess event
LVC->>LVC: Navigate to home screen
Assessment against linked issues
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (2)
🧰 Additional context used🧬 Code Definitions (1)Wable-iOS/Infra/Local/KeychainStorage.swift (1)
🔇 Additional comments (6)
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
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.
Actionable comments posted: 9
🧹 Nitpick comments (16)
Wable-iOS/Infra/Network/OAuth/OAuthEventManager.swift (1)
11-15: Well-implemented OAuth event management mechanismThis new class provides a centralized way to handle OAuth token expiration events using Combine, which aligns with the PR objectives of implementing automatic login and token refresh logic.
Consider expanding this class to handle other OAuth-related events in the future, such as successful token refresh or authentication failures. This would provide a more comprehensive event management solution as the authentication system grows.
final class OAuthEventManager { static let shared = OAuthEventManager() let tokenExpiredSubject = PassthroughSubject<Void, Never>() + // Example of potential future additions: + // let tokenRefreshedSubject = PassthroughSubject<Void, Never>() + // let authenticationFailedSubject = PassthroughSubject<Error, Never>() }Wable-iOS/Core/Logger/WableLogger.swift (1)
21-25: Refined Log Message Formatting Changes
The new log format clearly emphasizes the line number and function context, which improves readability for debugging purposes. One minor suggestion: consider removing the extra space before the semicolon to keep the punctuation consistent, i.e., change" ; "to"; "if that aligns with your desired style.Wable-iOS/Presentation/Login/LoginViewModel.swift (1)
39-77: Consider refactoring duplicate login flow logic.The implementation for Kakao and Apple login flows are nearly identical, differing only in the platform parameter. This duplication can be refactored for better maintainability.
func transform(input: Input, cancelBag: CancelBag) -> Output { - input.kakaoLoginTrigger - .withUnretained(self) - .flatMap { owner, _ -> AnyPublisher<Account, WableError> in - return owner.fetchUserAuthUseCase.execute(platform: .kakao) - } - .sink( - receiveCompletion: { [weak self] completion in - if case .failure(let error) = completion { - self?.loginErrorSubject.send(error) - } - }, - receiveValue: { [weak self] account in - self?.loginSuccessSubject.send(account) - } - ) - .store(in: cancelBag) - - input.appleLoginTrigger - .withUnretained(self) - .flatMap { owner, _ -> AnyPublisher<Account, WableError> in - return owner.fetchUserAuthUseCase.execute(platform: .apple) - } - .sink( - receiveCompletion: { [weak self] completion in - if case .failure(let error) = completion { - self?.loginErrorSubject.send(error) - } - }, - receiveValue: { [weak self] account in - self?.loginSuccessSubject.send(account) - } - ) - .store(in: cancelBag) + // Helper function to handle login for any platform + let handleLogin = { [weak self] (trigger: AnyPublisher<Void, Never>, platform: SocialPlatform) in + trigger + .withUnretained(self!) + .flatMap { owner, _ -> AnyPublisher<Account, WableError> in + return owner.fetchUserAuthUseCase.execute(platform: platform) + } + .sink( + receiveCompletion: { completion in + if case .failure(let error) = completion { + self?.loginErrorSubject.send(error) + } + }, + receiveValue: { account in + self?.loginSuccessSubject.send(account) + } + ) + .store(in: cancelBag) + } + + // Handle login for each platform + handleLogin(input.kakaoLoginTrigger, .kakao) + handleLogin(input.appleLoginTrigger, .apple) return Output( account: loginSuccessSubject.eraseToAnyPublisher() ) }Wable-iOS/Infra/Network/APIProvider.swift (3)
9-9: Unnecessary UIKit import.The
UIKitimport appears to be unnecessary since no UIKit-specific features are used in this file.-import UIKit
21-34: Improved authentication setup with clear separation of components.The changes provide a clearer structure for authentication by separating the authenticator and credential components. This makes the code more maintainable and easier to understand.
However, consider initializing the credential with values from token storage if available, rather than empty strings:
let credential = OAuthCredential( - accessToken: "", - refreshToken: "", + accessToken: try? TokenStorage(keyChainStorage: KeychainStorage()).load(.wableAccessToken) ?? "", + refreshToken: try? TokenStorage(keyChainStorage: KeychainStorage()).load(.wableRefreshToken) ?? "", requiresRefresh: false )
36-46: Good implementation of automatic logout on token expiration.The addition of a logout handler that updates the user session and sends a token expiration event is a good approach for handling authentication failures. This aligns well with the PR objective of handling 401 re-login messages.
However, consider avoiding the creation of a new repository instance each time:
-let logoutHandler = { - let userSessionRepository = UserSessionRepositoryImpl(userDefaults: UserDefaultsStorage( - userDefaults: UserDefaults.standard, - jsonEncoder: JSONEncoder(), - jsonDecoder: JSONDecoder() - )) - - userSessionRepository.updateActiveUserID(nil) - - OAuthEventManager.shared.tokenExpiredSubject.send() -} +// Create the repository once during initialization +let userSessionRepository = UserSessionRepositoryImpl(userDefaults: UserDefaultsStorage( + userDefaults: UserDefaults.standard, + jsonEncoder: JSONEncoder(), + jsonDecoder: JSONDecoder() +)) + +let logoutHandler = { + userSessionRepository.updateActiveUserID(nil) + OAuthEventManager.shared.tokenExpiredSubject.send() +}Wable-iOS/Presentation/Home/HomeViewController.swift (1)
33-41: Unused parameters in closures and debug print.The implementation contains unused parameters in the closure and a debug print statement.
contentRepostiory.fetchContentList(cursor: -1) .receive(on: DispatchQueue.main) .withUnretained(self) - .sink { completion in + .sink { _ in WableLogger.log("fetchContentList 실행 완", for: .debug) - } receiveValue: { owner, list in - print(list) + } receiveValue: { owner, list in + // TODO: Update UI with content list + // For now, just log the result + WableLogger.log("Content list fetched: \(list.count) items", for: .debug) } .store(in: cancelBag)🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 36-36: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
[Warning] 38-38: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
Wable-iOS/Presentation/Login/LoginViewController.swift (3)
146-146: Refactor note for tab bar presentationPresenting a
UITabBarControllerdirectly from the login screen is straightforward. Optionally, consider a navigation-based approach if deeper flows or modals are needed.
154-181: Handle SwiftLint warning on line 178 (ternary usage)SwiftLint warns about using a ternary to call void-returning functions. Though it works, an if-else statement is clearer.
Proposed improvement:
- sessionInfo.isNewUser ? owner.navigateToOnboarding() : owner.navigateToHome() + if sessionInfo.isNewUser { + owner.navigateToOnboarding() + } else { + owner.navigateToHome() + }🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 178-178: Using ternary to call Void functions should be avoided
(void_function_in_ternary)
201-205: Simple navigation to HomeThis straightforward approach to present the
TabBarControllercovers the main app flow. Consider potential memory usage if you repeatedly stack multiple tab bar controllers.Wable-iOS/Infra/Network/OAuth/OAuthenticator.swift (2)
25-25: Validate URL presenceThe
guard let urlStringcheck is good practice. Consider logging or handling cases whereurlRequest.urlisnil.
78-82: Empty refresh implementationLeaving
refresh(...)blank can be intentional if token refresh is managed elsewhere. Document it to avoid confusion for future contributors.Wable-iOS/Infra/Network/MoyaLoggingPlugin.swift (3)
52-52: Clean body loggingAppending the request body is valuable for debugging. Watch for potentially sensitive data in logs.
68-70: Symmetrical handling on failureRe-checking for auth errors covers additional scenarios. Consider combining with success logic for code reuse.
103-142: Consider renamingcondtionand clarifying logicThe variable name
condtionis misspelled and can be confusing. Also, check for potential branching issues. Ifv1/auth/tokenappears in the URL, the guard will exit. Confirm that this is intended logic.Proposed name fix:
- guard let condtion = response.response?.url?.absoluteString.contains("v1/auth/token"), - response.statusCode == 401 && !condtion else { return } + guard let condition = response.response?.url?.absoluteString.contains("v1/auth/token"), + response.statusCode == 401 && !condition else { return }Wable-iOS/App/SceneDelegate.swift (1)
50-51: Unify closure capture usage when chaining.withUnretained(self)and[weak self].
Using.withUnretained(self)for thereceiveValueclosure and[weak self]for thecompletionclosure can lead to confusion or inconsistent ownership handling. Consider a uniform approach (e.g., consistently using[weak self]) to reduce the risk of mismanaged references and potential crashes.Also applies to: 55-57, 59-61
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (21)
Wable-iOS.xcodeproj/project.pbxproj(6 hunks)Wable-iOS/App/SceneDelegate.swift(4 hunks)Wable-iOS/Core/Logger/WableLogger.swift(1 hunks)Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift(2 hunks)Wable-iOS/Domain/RepositoryInterface/UserSessionRepository.swift(0 hunks)Wable-iOS/Domain/UseCase/Login/FetchUserAuthUseCase.swift(2 hunks)Wable-iOS/Infra/Auth/AppleAuthProvider.swift(1 hunks)Wable-iOS/Infra/Auth/KakaoAuthProvider.swift(1 hunks)Wable-iOS/Infra/Local/KeychainStorage.swift(3 hunks)Wable-iOS/Infra/Network/APIProvider.swift(3 hunks)Wable-iOS/Infra/Network/MoyaLoggingPlugin.swift(4 hunks)Wable-iOS/Infra/Network/OAuth/OAuthEventManager.swift(1 hunks)Wable-iOS/Infra/Network/OAuth/OAuthenticator.swift(2 hunks)Wable-iOS/Infra/Network/TargetType/BaseTargetType.swift(1 hunks)Wable-iOS/Infra/Network/TargetType/ContentTargetType.swift(1 hunks)Wable-iOS/Infra/Token/TokenStorage.swift(2 hunks)Wable-iOS/Presentation/Home/HomeViewController.swift(1 hunks)Wable-iOS/Presentation/Login/LoginViewController.swift(6 hunks)Wable-iOS/Presentation/Login/LoginViewModel.swift(1 hunks)Wable-iOS/Presentation/TabBar/TabBarController.swift(2 hunks)Wable-iOS/Resource/Info.plist(1 hunks)
💤 Files with no reviewable changes (1)
- Wable-iOS/Domain/RepositoryInterface/UserSessionRepository.swift
🧰 Additional context used
🧬 Code Definitions (8)
Wable-iOS/Infra/Auth/KakaoAuthProvider.swift (1)
Wable-iOS/Infra/Token/TokenStorage.swift (1)
save(26-28)
Wable-iOS/Domain/UseCase/Login/FetchUserAuthUseCase.swift (1)
Wable-iOS/Infra/Token/TokenStorage.swift (1)
save(26-28)
Wable-iOS/Infra/Network/OAuth/OAuthenticator.swift (1)
Wable-iOS/Infra/Token/TokenStorage.swift (1)
load(30-37)
Wable-iOS/Presentation/Login/LoginViewModel.swift (1)
Wable-iOS/Domain/UseCase/Login/FetchUserAuthUseCase.swift (1)
execute(25-55)
Wable-iOS/Infra/Auth/AppleAuthProvider.swift (1)
Wable-iOS/Infra/Token/TokenStorage.swift (1)
save(26-28)
Wable-iOS/Infra/Network/APIProvider.swift (1)
Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift (1)
updateActiveUserID(80-84)
Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift (1)
Wable-iOS/Infra/Token/TokenStorage.swift (1)
load(30-37)
Wable-iOS/App/SceneDelegate.swift (2)
Wable-iOS/Presentation/Login/LoginViewController.swift (1)
setupBinding(154-181)Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift (2)
checkAutoLogin(101-115)updateActiveUserID(80-84)
🪛 SwiftLint (0.57.0)
Wable-iOS/Presentation/Home/HomeViewController.swift
[Warning] 36-36: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
[Warning] 38-38: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
Wable-iOS/Presentation/Login/LoginViewController.swift
[Warning] 178-178: Using ternary to call Void functions should be avoided
(void_function_in_ternary)
🔇 Additional comments (46)
Wable-iOS/Infra/Network/TargetType/ContentTargetType.swift (1)
31-31: Endpoint path standardized for consistencyThe path has been updated to remove the "api" prefix, which makes it consistent with other endpoints in the file that also don't include this prefix.
Please verify that the backend API supports this updated endpoint path to ensure there won't be any runtime issues with API calls.
Wable-iOS/Infra/Network/TargetType/BaseTargetType.swift (1)
42-42:Details
❓ Verification inconclusive
Important change to response validation behavior
The validation type has been changed from
.successCodesto.none, which means the framework will no longer automatically validate HTTP status codes. This is a significant change in error handling behavior.This change appears to be related to custom handling of authentication errors (like 401 responses) mentioned in the PR objectives. However, disabling validation for all requests could cause issues if error responses aren't properly handled elsewhere.
🏁 Script executed:
#!/bin/bash # Check if there's custom error handling in the codebase that would handle HTTP errors echo "Searching for custom error handling that might replace the removed validation..." rg -A 5 "handleError|handleResponse|ResponseError|statusCode" --type swiftLength of output: 3074
Critical Change in Response Validation – Custom Error Handling in Place
The change in
BaseTargetType.swiftto return.nonedisables automatic HTTP status code validation. However, our investigation shows that custom error handling is implemented to compensate for this:
- MoyaLoggingPlugin.swift: Logs response status codes and explicitly checks for 401 responses to trigger token status updates.
- APIProvider.swift: Converts error responses by throwing custom network errors based on status code ranges.
While these mechanisms appear to cover critical scenarios (e.g., authentication errors), please verify that all endpoints using BaseTargetType are routed through these custom handlers to ensure unexpected error responses aren’t missed.
Wable-iOS/Infra/Auth/KakaoAuthProvider.swift (2)
54-55: Token type standardized and logging improvedChanged from storing as
.kakaoAccessTokento.loginAccessToken, likely to standardize token handling across different auth providers. Added logging for successful token storage.This change aligns with the PR objectives of implementing social login features and supports a consistent approach to token management across different authentication methods.
58-58: Enhanced error loggingAdded detailed error information to the log message, which will improve debugging of token storage issues.
Wable-iOS.xcodeproj/project.pbxproj (6)
140-141: New files added to support social login functionality.The project file has been updated to include two new Swift files that are central to the social login implementation described in the PR objectives:
LoginViewModel.swift: Likely handles the Kakao and Apple login business logicOAuthEventManager.swift: Manages OAuth events, particularly token expiration eventsThese additions align with the PR's focus on implementing social login features and automatic login functionality.
359-360: File references correctly added to project structure.The new files have been properly registered in the project's PBXFileReference section, which is necessary for Xcode to properly recognize and use them in the build process.
962-962: LoginViewModel properly organized in Login group.The new LoginViewModel.swift file has been appropriately placed in the Login group, which follows good project organization principles by keeping related files together.
979-979: OAuthEventManager correctly positioned in OAuth group.The OAuthEventManager.swift file has been appropriately placed in the OAuth group, which is a logical location given its role in managing OAuth-related events, such as token expiration handling mentioned in the PR objectives.
1576-1576: Build configuration updated for LoginViewModel.LoginViewModel.swift has been correctly added to the build phases, ensuring it will be compiled with the rest of the project.
1599-1599: Build configuration updated for OAuthEventManager.OAuthEventManager.swift has been correctly added to the build phases, ensuring it will be compiled with the rest of the project. Based on the PR objectives, this file likely handles the token refresh logic and 401 re-login messages that were moved from Interceptor to MoyaPlugin.
Wable-iOS/Resource/Info.plist (1)
9-19: URL scheme configuration correctly added for Kakao loginThe addition of
CFBundleURLTypeswith the Kakao URL scheme is essential for implementing social login functionality. This will handle the authentication callback from Kakao when the user completes the login flow.Wable-iOS/Domain/UseCase/Login/FetchUserAuthUseCase.swift (1)
14-14: Token storage dependency added correctlyAdding the TokenStorage instance is appropriate for the new token handling functionality.
Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift (1)
21-22: Improved code organization with MARKGood use of a section marker to improve code readability.
Wable-iOS/Presentation/TabBar/TabBarController.swift (2)
15-15: HomeViewController dependency updated for content repositoryThe HomeViewController initialization now includes a contentRepository parameter, which is appropriate if the view controller requires this dependency for data fetching.
46-46: Improved transition animation addedThe cross-dissolve transition style provides a smoother visual experience when presenting the tab bar controller modally, enhancing the overall UI.
Wable-iOS/Infra/Token/TokenStorage.swift (2)
13-13: Good refactoring from platform-specific to generic naming!Renaming
kakaoAccessTokentologinAccessTokenmakes the token type more generic and reusable across different social login platforms. This aligns well with supporting both Kakao and Apple login as mentioned in the PR objectives.
31-32: Improved token handling with proper data type conversion.The changes to handle token as
Datafirst and then convert toStringusing UTF-8 encoding is more robust than directly retrieving it as a String. This ensures proper encoding/decoding of the token data from KeychainStorage.Also applies to: 36-36
Wable-iOS/Presentation/Login/LoginViewModel.swift (2)
12-25: Well-structured ViewModel with clear separation of concerns.The ViewModel properly follows MVVM design principles with clear property declarations and dependency injection through the initializer. The use of subjects for publishing events is a good practice for reactive programming.
29-38: Good implementation of ViewModelType protocol.The Input/Output structs provide a clear contract for the ViewModel transformation function, making it easy to understand the data flow. This follows best practices for reactive ViewModels.
Wable-iOS/Infra/Network/APIProvider.swift (1)
59-63: Improved error handling with explicit closure.The changes to use an explicit closure instead of key path syntax (
map { $0.data }instead ofmap(\.data)) and an explicit self reference in thetryMapare more readable and maintain consistency in style.Wable-iOS/Presentation/Home/HomeViewController.swift (2)
15-16: Good use of dependency injection and resource management.Adding the content repository as a dependency and using a cancelBag for managing Combine subscriptions follows best practices for dependency injection and resource management.
20-28: Proper implementation of initializers.The implementation of the designated initializer with dependency injection and the required initializer with
fatalErroris a good practice for view controllers that are instantiated programmatically.Wable-iOS/Presentation/Login/LoginViewController.swift (8)
10-13: Good adoption of Combine & CombineCocoaImporting these frameworks is a solid step toward a reactive programming model.
18-20: Great approach with reactive propertiesDefining
viewModelandcancelBagas class properties clearly indicates a reactive architecture. This helps isolate responsibilities and manage Combine subscriptions effectively.
39-39: Kakao button layout is consistentThe configuration sets an appropriate background color and corner radius. This matches standard Kakao login styling guidelines.
46-51: Converted Apple button from native to custom UIButtonWhile this offers flexibility in styling, ensure that additional logic (e.g., sign-in with Apple flows) is handled elsewhere, since native
ASAuthorizationAppleIDButtonoffers built-in compliance.Would you like to confirm that the Apple sign-in process is properly invoked in this custom button’s tap handling?
63-71: Initializer ensures consistent ViewModel injectionThis constructor-based dependency injection is a good design choice to ensure that
LoginViewModelis always injected when initializingLoginViewController.
79-79: setupBinding invocation is well placedCalling
setupBindinginviewDidLoadcentralizes reactive subscription setup.
90-98: Convenient batch addition of UI subviewsUsing
view.addSubviews(...)keeps view setup succinct and easy to read.
183-199: Navigation to Onboarding is straightforwardThe code prompts a notice view and transitions to
LCKYearViewControllerif the user proceeds. Check if the user can dismiss or go back if they change their mind.Wable-iOS/Infra/Network/OAuth/OAuthenticator.swift (6)
18-20: Initializer simplificationRemoving
errorMonitorclarifies the authenticator’s responsibility. Dependency injection ofTokenStorageis straightforward and testable.
23-24: Descriptive doc commentWell-documented purpose for the
applymethod. Keep scoping consistent if you add more calls (e.g., for other token types).
30-37: Flexible token type determinationSelecting
loginAccessTokenorwableAccessTokenbased on the URL path is well-structured. Ensure server endpoints remain stable.
39-52: Separate handling for token refresh requestsInjecting both access and refresh tokens streamlines re-auth logic. Verify that the server expects "Refresh" header for the refresh token.
58-58: Effective catch-block loggingLogging the token load failure ensures visibility. Consider localized messaging or user notifications if critical.
67-67:didRequestshort-circuitReturning
falseis a clear approach if you intentionally skip automatic authentication error handling.Wable-iOS/Infra/Network/MoyaLoggingPlugin.swift (3)
14-20: Logout handler additionIntroducing a
LogoutHandlerclosure is a neat way to decouple logout logic. This fosters reusability and testability.
23-29: Constructor injection for logout and token storageAllowing
logoutHandlerandtokenStorageas optional parameters increases flexibility. Good approach for custom error handling or logging flows.
65-65: Check for auth error after successful responseGood approach to unify error checks. This ensures no missed cases.
Wable-iOS/Infra/Local/KeychainStorage.swift (1)
43-44: Good move aligning the key withkSecAttrAccount.
Storing the custom key inkSecAttrAccountis standard practice for keychain lookups and adds clarity to your storage logic.Wable-iOS/App/SceneDelegate.swift (6)
11-12: No concerns regarding Kakao import.
These added imports are valid for enabling Kakao login functionality.
18-21: Properties for token and login state look good.
IntroducingtokenStorage,tokenProvider, andloginRepositoryclarifies the separation of concerns for managing tokens and user sessions.
46-47: InvokingsetupBinding()is a good choice.
CallingsetupBinding()immediately after the splash screen ensures that token expiration events will be captured early in the app flow.
69-75: Kakao openURL handling looks correct.
Properly forwarding the Kakao login URL toAuthController.handleOpenUrlis essential for successful sign-in flow. Implementation here is concise and sound.
81-90: Login screen configuration is appropriate.
Setting upLoginViewControllerwith the newLoginViewModelthat references theFetchUserAuthUseCasefosters an organized architecture for authentication.
103-110: BindingtokenExpiredSubjectfor session management is clear.
Listening for token expiration events and taking immediate action keeps session state consistent and improves the user experience.
| .handleEvents(receiveOutput: { account in | ||
| do { | ||
| try self.tokenStorage.save(account.token.accessToken, for: .wableAccessToken) | ||
|
|
||
| WableLogger.log("액세스 토큰 저장 성공: \(account.token.accessToken)", for: .debug) | ||
| try self.tokenStorage.save(account.token.refreshToken, for: .wableRefreshToken) | ||
|
|
||
| WableLogger.log("리프레시 토큰 저장 성공: \(account.token.refreshToken)", for: .debug) | ||
| } catch { | ||
| WableLogger.log("토큰 저장 실패: \(error)", for: .debug) | ||
| } |
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.
Secure token storage implementation works but exposes sensitive data in logs
The implementation correctly saves both access and refresh tokens to secure storage, but consider removing the full token values from debug logs as they contain sensitive security information.
- WableLogger.log("액세스 토큰 저장 성공: \(account.token.accessToken)", for: .debug)
+ WableLogger.log("액세스 토큰 저장 성공", for: .debug)
- WableLogger.log("리프레시 토큰 저장 성공: \(account.token.refreshToken)", for: .debug)
+ WableLogger.log("리프레시 토큰 저장 성공", for: .debug)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .handleEvents(receiveOutput: { account in | |
| do { | |
| try self.tokenStorage.save(account.token.accessToken, for: .wableAccessToken) | |
| WableLogger.log("액세스 토큰 저장 성공: \(account.token.accessToken)", for: .debug) | |
| try self.tokenStorage.save(account.token.refreshToken, for: .wableRefreshToken) | |
| WableLogger.log("리프레시 토큰 저장 성공: \(account.token.refreshToken)", for: .debug) | |
| } catch { | |
| WableLogger.log("토큰 저장 실패: \(error)", for: .debug) | |
| } | |
| .handleEvents(receiveOutput: { account in | |
| do { | |
| try self.tokenStorage.save(account.token.accessToken, for: .wableAccessToken) | |
| WableLogger.log("액세스 토큰 저장 성공", for: .debug) | |
| try self.tokenStorage.save(account.token.refreshToken, for: .wableRefreshToken) | |
| WableLogger.log("리프레시 토큰 저장 성공", for: .debug) | |
| } catch { | |
| WableLogger.log("토큰 저장 실패: \(error)", for: .debug) | |
| } |
| guard let userSession = fetchActiveUserSession(), | ||
| userSession.isAutoLoginEnabled == true | ||
| else { | ||
| return .just(false) | ||
| } | ||
|
|
||
| func disableAutoLogin(for userID: Int) { | ||
| updateAutoLogin(enabled: false, forUserID: userID) | ||
| do { | ||
| let _ = try tokenStorage.load(.wableAccessToken), | ||
| _ = try tokenStorage.load(.wableRefreshToken) | ||
| return .just(true) | ||
| } catch { | ||
| return .fail(error) | ||
| } | ||
| } |
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.
🛠️ Refactor suggestion
Inconsistent error handling in auto-login check
The auto-login check has been simplified, but there's an inconsistency in the error handling approach. The guard statement returns .just(false) when auto-login is disabled, but the catch block returns .fail(error) when tokens can't be loaded. This creates two different failure paths for consumers of this method.
Consider standardizing the error handling approach:
func checkAutoLogin() -> AnyPublisher<Bool, Error> {
guard let userSession = fetchActiveUserSession(),
userSession.isAutoLoginEnabled == true
else {
return .just(false)
}
do {
let _ = try tokenStorage.load(.wableAccessToken),
_ = try tokenStorage.load(.wableRefreshToken)
return .just(true)
} catch {
- return .fail(error)
+ WableLogger.log("토큰 로드 실패: \(error)", for: .debug)
+ return .just(false)
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| guard let userSession = fetchActiveUserSession(), | |
| userSession.isAutoLoginEnabled == true | |
| else { | |
| return .just(false) | |
| } | |
| func disableAutoLogin(for userID: Int) { | |
| updateAutoLogin(enabled: false, forUserID: userID) | |
| do { | |
| let _ = try tokenStorage.load(.wableAccessToken), | |
| _ = try tokenStorage.load(.wableRefreshToken) | |
| return .just(true) | |
| } catch { | |
| return .fail(error) | |
| } | |
| } | |
| func checkAutoLogin() -> AnyPublisher<Bool, Error> { | |
| guard let userSession = fetchActiveUserSession(), | |
| userSession.isAutoLoginEnabled == true | |
| else { | |
| return .just(false) | |
| } | |
| do { | |
| let _ = try tokenStorage.load(.wableAccessToken), | |
| _ = try tokenStorage.load(.wableRefreshToken) | |
| return .just(true) | |
| } catch { | |
| WableLogger.log("토큰 로드 실패: \(error)", for: .debug) | |
| return .just(false) | |
| } | |
| } |
| func authorizationController( | ||
| controller: ASAuthorizationController, | ||
| didCompleteWithAuthorization authorization: ASAuthorization | ||
| ) { | ||
| guard let promise = self.promise else { return } | ||
|
|
||
| if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential { | ||
| try? tokenStorage.save("", for: .kakaoAccessToken) | ||
| promise(.success(appleIDCredential.user)) | ||
| } else { | ||
| promise(.failure(.failedToValidateAppleLogin)) | ||
| if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential, | ||
| let token = appleIDCredential.identityToken, | ||
| let tokenText = String(data: token, encoding: .utf8) { | ||
| WableLogger.log("애플 로그인 토큰 추출 완료", for: .debug) | ||
|
|
||
| do { | ||
| try tokenStorage.save(tokenText, for: .loginAccessToken) | ||
| WableLogger.log("애플 로그인 토큰 저장 완료", for: .debug) | ||
| promise(.success(appleIDCredential.fullName?.formatted())) | ||
| } catch { | ||
| WableLogger.log("애플 로그인 토큰 저장 중 오류 발생: \(error)", for: .error) | ||
| promise(.failure(.networkError)) | ||
| } |
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.
Handle missing credential and token scenarios more robustly.
Currently, if appleIDCredential or identityToken is missing, the code exits without returning a failure to the caller. This may lead to a dangling promise and indefinite waiting. Consider returning a failure result when credential or token extraction fails to prevent potential deadlocks or undefined states.
| guard let stringValue = value as? String, | ||
| let stringData = stringValue.data(using: .utf8) else { | ||
| throw LocalError.saveFailed | ||
| } |
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.
🛠️ Refactor suggestion
Refactor to handle all Encodable types.
Although the function signature suggests supporting any Encodable type, this block only supports String. Consider using JSONEncoder to convert generic Encodable objects to Data, ensuring that all valid types can be stored instead of just strings.
| throw LocalError.saveFailed | ||
| } | ||
|
|
||
| WableLogger.log("키체인에 데이터 저장 완료: \(stringValue)", for: .debug) |
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.
Avoid logging sensitive token data.
Printing the raw token string can lead to security and privacy leaks if logs are exposed. Consider removing or masking sensitive data from logs.
| if T.self == Data.self { | ||
| WableLogger.log("\(data)", for: .debug) | ||
| return data as? T | ||
| } |
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.
Avoid logging raw data for security.
Logging the raw Data may leak sensitive information if it contains tokens or personal data. Consider masking or removing it before logging.
| if T.self == String.self, | ||
| let stringValue = String(data: data, encoding: .utf8) { | ||
| WableLogger.log("\(stringValue)", for: .debug) | ||
| return stringValue as? T | ||
| } |
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.
Protect sensitive string values in logs.
As above, directly logging string values that might be tokens or credentials can be a security concern. Conceal or omit these details in production logs.
| func handleTokenExpired() { | ||
| userSessionRepository.updateActiveUserID(nil) | ||
| configureLoginScreen() | ||
|
|
||
| let toast = ToastView(status: .caution, message: "세션이 만료되었습니다. 다시 로그인해주세요.") | ||
| toast.show() | ||
| } |
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.
🛠️ Refactor suggestion
Consider removing stored tokens upon expiration.
When handling a token expiration, updating the active user ID to nil is helpful. However, removing the expired tokens from the keychain can further prevent accidental reuse.
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.
Actionable comments posted: 0
🧹 Nitpick comments (5)
Wable-iOS/Infra/Network/MoyaLoggingPlugin.swift (5)
107-110: Fix variable name typo in guard conditionThere's a typo in the variable name "condtion" which should be "condition".
- guard let condtion = response.response?.url?.absoluteString.contains("v1/auth/token"), - response.statusCode == 401 && !condtion else { return } + guard let condition = response.response?.url?.absoluteString.contains("v1/auth/token"), + response.statusCode == 401 && !condition else { return }
107-110: Improve readability of auth error check conditionThe guard condition is a bit complex and could be more readable by splitting it into separate conditions or using more descriptive variable names.
- guard let condtion = response.response?.url?.absoluteString.contains("v1/auth/token"), - response.statusCode == 401 && !condtion else { return } + // Only handle 401 errors that are not from the token endpoint itself + let isTokenEndpoint = response.response?.url?.absoluteString.contains("v1/auth/token") ?? false + let isAuthError = response.statusCode == 401 + + guard isAuthError && !isTokenEndpoint else { return }
114-115: Consider simplifying Combine operatorsUsing both
.withUnretained(self)and[weak self]in the closure seems redundant. You can use one or the other based on your needs.- tokenProvider.updateTokenStatus() - .withUnretained(self) - .sink { [weak self] completion in + tokenProvider.updateTokenStatus() + .sink { [weak self] completion inOr alternatively:
- tokenProvider.updateTokenStatus() - .withUnretained(self) - .sink { [weak self] completion in + tokenProvider.updateTokenStatus() + .withUnretained(self) + .sink { completion in
128-134: Consider extracting token saving logic to a helper methodThe token saving logic could be extracted to a helper method to improve readability and reusability.
+ private func saveTokens(_ token: OAuthToken) throws { + try tokenStorage.save(token.accessToken, for: .wableAccessToken) + try tokenStorage.save(token.refreshToken, for: .wableRefreshToken) + } } receiveValue: { owner, token in do { - try owner.tokenStorage.save(token.accessToken, for: .wableAccessToken) - try owner.tokenStorage.save(token.refreshToken, for: .wableRefreshToken) + try owner.saveTokens(token) } catch { WableLogger.log("토큰 재발급 중 문제 발생", for: .error) owner.logoutHandler?() } }
132-132: Enhance error logging with specific error informationThe current error logging doesn't include specific error details, which could make debugging more difficult.
- WableLogger.log("토큰 재발급 중 문제 발생", for: .error) + WableLogger.log("토큰 재발급 중 문제 발생: \(error.localizedDescription)", for: .error)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
Wable-iOS/Infra/Network/MoyaLoggingPlugin.swift(4 hunks)
🔇 Additional comments (4)
Wable-iOS/Infra/Network/MoyaLoggingPlugin.swift (4)
14-20: Properties added for auth error handling look goodThe new properties and type alias are well-structured to support the social login functionality. The
LogoutHandlertype alias makes the code more readable and the dependency injection pattern withtokenStorageprovides good flexibility.
23-29: Good initializer implementation with default parametersThe initializer with default parameters allows for flexible configuration while maintaining backward compatibility. This is a good practice for dependency injection.
52-52: Minor body output improvementThis minor change to use
bodyStringdirectly instead ofbodyString.descriptionis a small but good optimization.
65-65: Good implementation of auth error checking in both success and failure pathsAdding the auth error check in both response paths ensures comprehensive error handling regardless of the request outcome.
Also applies to: 68-70
Docstrings generation was requested by @youz2me. * #147 (comment) The following files were modified: * `Wable-iOS/App/SceneDelegate.swift` * `Wable-iOS/Core/Logger/WableLogger.swift` * `Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift` * `Wable-iOS/Domain/UseCase/Login/FetchUserAuthUseCase.swift` * `Wable-iOS/Infra/Auth/AppleAuthProvider.swift` * `Wable-iOS/Infra/Auth/KakaoAuthProvider.swift` * `Wable-iOS/Infra/Local/KeychainStorage.swift` * `Wable-iOS/Infra/Network/APIProvider.swift` * `Wable-iOS/Infra/Network/MoyaLoggingPlugin.swift` * `Wable-iOS/Infra/Network/OAuth/OAuthenticator.swift` * `Wable-iOS/Infra/Token/TokenStorage.swift` * `Wable-iOS/Presentation/Home/HomeViewController.swift` * `Wable-iOS/Presentation/Login/LoginViewController.swift` * `Wable-iOS/Presentation/Login/LoginViewModel.swift`
JinUng41
left a comment
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.
고생하셨습니다!
시퀀스 다이어그램을 통해 과정을 설명해주셔서 바로 이해할 수 있었습니다.
그리고 추가된 AI의 코드리뷰 또한 앞으로 개발함에 있어 많은 도움이 될 것 같아요.
저도 빨리 PR을 작성해 보고 싶네요.
끝으로, 중간중간 비동기 클로저 내에서 self 키워드를 직접 사용하는 부분이 보이는데요.
순환 참조와 메모리 누수의 위험성은 없는지 조금 더 신경 써주시면 좋겠습니다.
Wable-iOS/App/SceneDelegate.swift
Outdated
| if let url = URLContexts.first?.url { | ||
| if (AuthApi.isKakaoTalkLoginUrl(url)) { | ||
| _ = AuthController.handleOpenUrl(url: url) | ||
| } | ||
| } |
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.
사소하지만, guard 문을 이용하면 코드 뎁스를 줄일 수 있는 장점이 있습니다~
guard let url = URLContexts.first?.url,
AuthApi.isKakaoTalkLoginUrl(url)
else {
return
}
_ = AuthController.handleOpenUrl(url: url)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.
적용했습니다 !! 알려주셔서 감사합니닷 ㅎㅎ
| self.window?.rootViewController = LoginViewController( | ||
| viewModel: LoginViewModel( | ||
| useCase: FetchUserAuthUseCase( | ||
| loginRepository: loginRepository, | ||
| userSessionRepository: userSessionRepository | ||
| ) | ||
| ) | ||
| ) |
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.
이러한 코드는 제가 빠른 시일 내에 DI컨테이너를 공부하고 다시 알려드리겠습니다.
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.
ㅠㅠ 감사합니다
| func transform(input: Input, cancelBag: CancelBag) -> Output { | ||
| input.kakaoLoginTrigger | ||
| .withUnretained(self) | ||
| .flatMap { owner, _ -> AnyPublisher<Account, WableError> in | ||
| return owner.fetchUserAuthUseCase.execute(platform: .kakao) | ||
| } | ||
| .sink( | ||
| receiveCompletion: { [weak self] completion in | ||
| if case .failure(let error) = completion { | ||
| self?.loginErrorSubject.send(error) | ||
| } | ||
| }, | ||
| receiveValue: { [weak self] account in | ||
| self?.loginSuccessSubject.send(account) | ||
| } | ||
| ) | ||
| .store(in: cancelBag) | ||
|
|
||
| input.appleLoginTrigger | ||
| .withUnretained(self) | ||
| .flatMap { owner, _ -> AnyPublisher<Account, WableError> in | ||
| return owner.fetchUserAuthUseCase.execute(platform: .apple) | ||
| } | ||
| .sink( | ||
| receiveCompletion: { [weak self] completion in | ||
| if case .failure(let error) = completion { | ||
| self?.loginErrorSubject.send(error) | ||
| } | ||
| }, | ||
| receiveValue: { [weak self] account in | ||
| self?.loginSuccessSubject.send(account) | ||
| } | ||
| ) | ||
| .store(in: cancelBag) | ||
|
|
||
| return Output( | ||
| account: loginSuccessSubject.eraseToAnyPublisher() | ||
| ) | ||
| } |
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.
로그인 종류가 2가지라는 점을 제외하면, 그 후의 실행되어야 하는 내용은 같아 보입니다.
각 트리거에서 SocialPlatform으로 변환하고, 둘을 merge로 합쳐 로직을 한 곳에서 처리할 수 있을 것 같아요.
func transform(input: Input, cancelBag: CancelBag) -> Output {
// 각 로그인 트리거에서 해당 플랫폼을 매핑
let kakaoLogin = input.kakaoLoginTrigger
.map { SocialPlatform.kakao }
let appleLogin = input.appleLoginTrigger
.map { SocialPlatform.apple }
// 두 로그인 스트림 병합
Publishers.merge(kakaoLogin, appleLogin)
.withUnretained(self)
.flatMap { owner, platform -> AnyPublisher<Account, WableError> in
return owner.fetchUserAuthUseCase.execute(platform: platform)
}
.sink(
receiveCompletion: { [weak self] completion in
if case .failure(let error) = completion {
self?.loginErrorSubject.send(error)
}
},
receiveValue: { [weak self] account in
self?.loginSuccessSubject.send(account)
}
)
.store(in: cancelBag)
return Output(
account: loginSuccessSubject.eraseToAnyPublisher()
)
}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.
적용했습니다 !! 감사합니다 🥹
| modalPresentationStyle = .fullScreen | ||
| modalTransitionStyle = .crossDissolve |
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.
👍
| kSecAttrAccount as String: key, // key 값을 kSecAttrAccount에 사용해야 함 | ||
| kSecAttrService as String: Bundle.main.bundleIdentifier ?? "com.wable.Wable-iOS", |
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.
필요 없는 주석이라면, 지워도 좋을 것 같습니다.
"com.wable.Wable-iOS"와 같은 절대적인 문자열은 Constant로 묶어서 선언해도 좋을 것 같습니다.
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.
생각해보니 Bundle 익스텐션을 만들어놓았던 것을 까먹고 있었다는 걸 깨달아 ... Bundle 익스텐션에 추가해두었습니다!
// Bundle+.swift
extension Bundle {
static let identifier: String = {
guard let identifierString = main.bundleIdentifier else {
fatalError("Bundle identifier를 찾을 수 없습니다.")
}
return identifierString
}()
// KeychainStorage.swift
func getValue<T>(for key: String) throws -> T? where T : Decodable, T : Encodable {
var item: AnyObject?
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecAttrService as String: Bundle.identifier,
kSecReturnData as String: kCFBooleanTrue!,
kSecMatchLimit as String: kSecMatchLimitOne
]| return try JSONDecoder().decode(T.self, from: data) | ||
| } |
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.
KeychainStorage가 JSONDecoder를 소유한다면, 매번 객체 생성이 필요 없을 것 같아요.
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
📄 작업 내용
💻 주요 로직 시퀀스 다이어그램
앱 시작 및 자동 로그인 프로세스
sequenceDiagram participant SD as SceneDelegate participant OEM as OAuthEventManager participant USR as UserSessionRepository participant TS as TokenStorage participant TBC as TabBarController participant LVC as LoginViewController %% 초기화 및 이벤트 핸들러 등록 SD->>OEM: tokenExpiredSubject 구독 OEM-->>SD: 이벤트 구독 성공 %% 앱 시작 및 자동 로그인 프로세스 SD->>USR: checkAutoLogin() USR->>TS: 토큰 존재 확인 alt 자동 로그인 활성화 + 토큰 존재 TS-->>USR: 토큰 확인 완료 USR-->>SD: true 반환 SD->>TBC: present TabBarController else 자동 로그인 비활성화 또는 토큰 없음 TS-->>USR: 토큰 없음 또는 자동 로그인 비활성화 USR-->>SD: false 반환 SD->>LVC: present LoginViewController(viewModel) end로그인 후 API 호출 성공 시나리오
sequenceDiagram participant HVC as HomeViewController participant CR as ContentRepository participant API as APIProvider participant MLP as MoyaLoggingPlugin participant OA as OAuthenticator participant TS as TokenStorage %% API 호출 프로세스 (성공 시나리오) HVC->>CR: fetchContentList(cursor: -1) CR->>API: request() API->>MLP: willSend() MLP-->>API: 요청 로깅 API->>OA: apply(credential, to: request) OA->>TS: load(.wableAccessToken) TS-->>OA: accessToken 반환 OA-->>API: Authorization 헤더 설정 %% API 응답 처리 (성공) API-->>MLP: didReceive(result) MLP-->>API: 응답 로깅 API-->>CR: 성공 응답 반환 CR-->>HVC: 데이터 반환토큰 갱신 성공 시나리오
sequenceDiagram participant HVC as HomeViewController participant CR as ContentRepository participant API as APIProvider participant MLP as MoyaLoggingPlugin participant OTP as OAuthTokenProvider participant TS as TokenStorage %% API 응답 실패 (401) 후 토큰 갱신 성공 HVC->>CR: fetchContentList(cursor: -1) CR->>API: request() API-->>MLP: didReceive(result) - 401 에러 MLP->>MLP: checkForAuthError(response) Note over MLP: 401 에러 + URL 조건 검사 MLP->>OTP: updateTokenStatus() OTP-->>MLP: 새 토큰 반환 MLP->>TS: save(token.accessToken) MLP->>TS: save(token.refreshToken) Note over MLP,API: 다음 요청부터 새 토큰 사용 %% 재요청 (다음 요청 자동 재시도는 아님) HVC->>CR: fetchContentList(cursor: -1) 재요청 CR->>API: request() (새 토큰 사용) API-->>CR: 성공 응답 반환 CR-->>HVC: 데이터 반환토큰 갱신 실패 시나리오
sequenceDiagram participant HVC as HomeViewController participant CR as ContentRepository participant API as APIProvider participant MLP as MoyaLoggingPlugin participant OTP as OAuthTokenProvider participant TS as TokenStorage participant OEM as OAuthEventManager participant SD as SceneDelegate participant USR as UserSessionRepository %% API 응답 실패 (401) 후 토큰 갱신 실패 HVC->>CR: fetchContentList(cursor: -1) CR->>API: request() API-->>MLP: didReceive(result) - 401 에러 MLP->>MLP: checkForAuthError(response) Note over MLP: 401 에러 + URL 조건 검사 MLP->>OTP: updateTokenStatus() OTP-->>MLP: WableError.signinRequired 에러 MLP->>TS: delete(.wableAccessToken) MLP->>TS: delete(.wableRefreshToken) MLP->>MLP: logoutHandler() 호출 MLP->>OEM: tokenExpiredSubject.send() OEM-->>SD: 토큰 만료 이벤트 발생 SD->>SD: handleTokenExpired() SD->>USR: updateActiveUserID(nil) SD->>SD: configureLoginScreen() SD->>SD: ToastView("세션이 만료되었습니다").show()💻 주요 코드 설명
1. 소셜 로그인 구현
애플 로그인
ASAuthorizationController를 사용해 애플 로그인 요청을 처리하고Future타입으로 감싸Combine기반 비동기 작업을 수행했습니다.delegate메서드를 통해promise에 전달됩니다.카카오 로그인
Future와Combine을 통해 비동기 처리를 구현했습니다.2. 자동 로그인 구현
UserSessionRepository에서 저장된 사용자 세션을 확인하고 자동 로그인이 활성화되어 있는지와 토큰이 존재하는지 검증을 수행합니다.SceneDelegate에서 스플래시 화면 이후 자동 로그인 체크를 진행하고 결과에 따라 메인 화면 또는 로그인 화면으로 이동합니다.3. 토큰 갱신 로직 오류 해결
Alamofire의Interceptor에서 401 에러를 처리하려 했으나Interceptor에서는 통신 후의 값을 처리할 수 없기 때문에 401 재로그인 에러를 판별할 수 없는 문제가 있었습니다.MoyaLoggingPlugin으로 로직을 이동시켜checkForAuthError()에서 해당 역할을 수행하도록 코드를 개선했습니다.checkForAuthError()에서 401 에러를 감지하면 토큰 갱신을 시도하고, 실패할 경우 로그아웃 처리를 진행합니다.4. 토큰 만료 이벤트 처리
Combine의PassthroughSubject를 사용하여 이벤트를 발행합니다.SceneDelegate에서 토큰 만료 이벤트를 구독하고 이벤트가 발행되면handleTokenExpired메서드를 호출해 로그아웃 처리와 함께 사용자에게 토스트 메시지를 보여줍니다.5. KeyChainStorage 문제 수정
KeyChainStorage에서 값을 제대로 불러올 수 없었던 문제를 수정했습니다.📚 참고자료
Swift ) Moya Interceptor, Plugin - EEYatHo iOS
🔗 연결된 이슈
Summary by CodeRabbit