Skip to content

Conversation

@youz2me
Copy link
Member

@youz2me youz2me commented Mar 27, 2025

👻 PULL REQUEST

📄 작업 내용

  • 카카오 로그인, 애플 로그인을 구현했어요.
  • 자동 로그인 및 토큰 갱신 로직을 구현했어요.
    • Interceptor에서 처리가 블가능하던 401 재로그인 메시지 분기 처리를 MoyaPlugin으로 옮겨 구현했어요.
  • KeyChainStorage에서 값을 제대로 불러올 수 없었던 오류를 수정했어요.
구현 내용 카카오 로그인 (기존 유저) 애플 로그인 (신규 유저)
IPhone 13 mini

💻 주요 로직 시퀀스 다이어그램

앱 시작 및 자동 로그인 프로세스

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
Loading

로그인 후 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: 데이터 반환
Loading

토큰 갱신 성공 시나리오

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: 데이터 반환
Loading

토큰 갱신 실패 시나리오

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()
Loading

💻 주요 코드 설명

1. 소셜 로그인 구현

애플 로그인

// AppleAuthProvider.swift
func authenticate() -> AnyPublisher<String?, WableError> {
    return Future<String?, WableError> { promise in
        self.promise = promise
        
        let request = ASAuthorizationAppleIDProvider().createRequest()
        request.requestedScopes = [.fullName]
        
        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        authorizationController.delegate = self
        authorizationController.presentationContextProvider = self
        authorizationController.performRequests()
    }
    .eraseToAnyPublisher()
}
  • ASAuthorizationController를 사용해 애플 로그인 요청을 처리하고 Future 타입으로 감싸 Combine 기반 비동기 작업을 수행했습니다.
  • 인증 결과는 delegate 메서드를 통해 promise에 전달됩니다.

카카오 로그인

// KakaoAuthProvider.swift
func authenticate() -> AnyPublisher<String?, WableError> {
    return Future<String?, WableError> { promise in
        if KakaoUserAPI.isKakaoTalkLoginAvailable() {
            KakaoUserAPI.shared.loginWithKakaoTalk { [weak self] (oauthToken, error) in
                self?.handleKakaoAuthResult(oauthToken: oauthToken, error: error, promise: promise)
            }
        } else {
            KakaoUserAPI.shared.loginWithKakaoAccount { [weak self] (oauthToken, error) in
                self?.handleKakaoAuthResult(oauthToken: oauthToken, error: error, promise: promise)
            }
        }
    }
    .eraseToAnyPublisher()
}
  • 카카오톡 앱이 설치되어 있으면 앱을, 없으면 웹뷰를 통해 로그인할 수 있도록 분기 처리를 진행했습니다.
  • 애플 로그인과 마찬가지로 FutureCombine을 통해 비동기 처리를 구현했습니다.

2. 자동 로그인 구현

// UserSessionRepositoryImpl.swift
func checkAutoLogin() -> AnyPublisher<Bool, Error> {
    guard let userSession = fetchActiveUserSession(),
          userSession.isAutoLoginEnabled == true else {
        return .just(false)
    }
    
    do {
        let _ = try tokenStorage.load(.wableAccessToken)
        let _ = try tokenStorage.load(.wableRefreshToken)
        return .just(true)
    } catch {
        return .fail(error)
    }
}
  • UserSessionRepository에서 저장된 사용자 세션을 확인하고 자동 로그인이 활성화되어 있는지와 토큰이 존재하는지 검증을 수행합니다.
  • 두 조건이 모두 충족되면 true를 반환하여 자동 로그인을 진행합니다.
// SceneDelegate.swift
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0) {
    self.userSessionRepository.checkAutoLogin()
        .withUnretained(self)
        .sink { [weak self] completion in
            switch completion {
            case .finished:
                break
            case .failure(let error):
                WableLogger.log("자동 로그인 여부 체크 실패: \(error)", for: .error)
                self?.configureLoginScreen()
            }
        } receiveValue: { owner, isAutologinEnabled in
            WableLogger.log("자동 로그인 여부 체크 성공: \(isAutologinEnabled)", for: .debug)
            isAutologinEnabled ? owner.configureMainScreen() : owner.configureLoginScreen()
        }
        .store(in: self.cancelBag)
}
  • 이후 SceneDelegate에서 스플래시 화면 이후 자동 로그인 체크를 진행하고 결과에 따라 메인 화면 또는 로그인 화면으로 이동합니다.

3. 토큰 갱신 로직 오류 해결

// MoyaLoggingPlugin.swift
private func checkForAuthError(_ response: Response) {
    guard let condtion = response.response?.url?.absoluteString.contains("v1/auth/token"),
          response.statusCode == 401 && !condtion else { return }
    
    let tokenProvider = OAuthTokenProvider()
    
    tokenProvider.updateTokenStatus()
        .withUnretained(self)
        .sink { [weak self] completion in
            switch completion {
            case .finished:
                break
            case .failure(let error):
                if error == .signinRequired {
                    try? self?.tokenStorage.delete(.wableAccessToken)
                    try? self?.tokenStorage.delete(.wableRefreshToken)
                    
                    self?.logoutHandler?()
                }
            }
        } receiveValue: { owner, token in
            do {
                try owner.tokenStorage.save(token.accessToken, for: .wableAccessToken)
                try owner.tokenStorage.save(token.refreshToken, for: .wableRefreshToken)
            } catch {
                WableLogger.log("토큰 재발급 중 문제 발생", for: .error)
                
                owner.logoutHandler?()
            }
        }
        .store(in: cancelBag)
}
  • AlamofireInterceptor에서 401 에러를 처리하려 했으나 Interceptor에서는 통신 후의 값을 처리할 수 없기 때문에 401 재로그인 에러를 판별할 수 없는 문제가 있었습니다.
  • 이를 해결하기 위해 MoyaLoggingPlugin으로 로직을 이동시켜 checkForAuthError()에서 해당 역할을 수행하도록 코드를 개선했습니다.
    • checkForAuthError()에서 401 에러를 감지하면 토큰 갱신을 시도하고, 실패할 경우 로그아웃 처리를 진행합니다.

4. 토큰 만료 이벤트 처리

// OAuthEventManager.swift
final class OAuthEventManager {
    static let shared = OAuthEventManager()
    
    let tokenExpiredSubject = PassthroughSubject<Void, Never>()
}
  • 토큰 만료 이벤트를 발행하기 위한 싱글톤 클래스를 생성했습니다.
  • CombinePassthroughSubject를 사용하여 이벤트를 발행합니다.
// SceneDelegate.swift
func setupBinding() {
    OAuthEventManager.shared.tokenExpiredSubject
        .receive(on: DispatchQueue.main)
        .sink { [weak self] _ in
            self?.handleTokenExpired()
        }
        .store(in: cancelBag)
}

func handleTokenExpired() {
    userSessionRepository.updateActiveUserID(nil)
    configureLoginScreen()
    
    let toast = ToastView(status: .caution, message: "세션이 만료되었습니다. 다시 로그인해주세요.")
    toast.show()
}
  • SceneDelegate에서 토큰 만료 이벤트를 구독하고 이벤트가 발행되면 handleTokenExpired 메서드를 호출해 로그아웃 처리와 함께 사용자에게 토스트 메시지를 보여줍니다.

5. KeyChainStorage 문제 수정

// TokenStorage.swift
func load(_ tokenType: TokenType) throws -> String {
    guard let token: String = try keychainStorage.getValue(for: tokenType.rawValue) else {
        throw TokenError.dataConversionFailed
    }
    
    return token
}
// 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,
        kSecAttrService as String: key,
        kSecReturnData as String: kCFBooleanTrue!,
        kSecMatchLimit as String: kSecMatchLimitOne
    ]
    
    let status = SecItemCopyMatching(query as CFDictionary, &item)
    
    guard status == errSecSuccess,
          let data = item as? Data
    else {
        throw LocalError.dataNotFound
    }
    
    return data as? T
}
  • KeyChainStorage에서 값을 제대로 불러올 수 없었던 문제를 수정했습니다.
  • Data 타입을 직접 반환하도록 해 변환 과정의 오류를 해결했습니다.

📚 참고자료

Swift ) Moya Interceptor, Plugin - EEYatHo iOS

🔗 연결된 이슈

Summary by CodeRabbit

  • New Features
    • Enhanced login flows with smoother Kakao and Apple sign-ins, streamlining the transition from onboarding to the home screen.
    • Proactive session management now notifies users of expiration with a toast message and directs them back to login.
  • Improvements
    • Optimized content loading on the home screen for a fresher experience.
    • Refined modal transition animations for a more polished look.
    • Strengthened authentication token management for improved reliability.

@youz2me youz2me added ✨ feat 기능 또는 객체 구현 🦉 유진 🛌🛌🛌🛌🛌🛌🛌🛌🛌🛌 labels Mar 27, 2025
@youz2me youz2me requested a review from JinUng41 March 27, 2025 17:02
@youz2me youz2me self-assigned this Mar 27, 2025
@coderabbitai
Copy link

coderabbitai bot commented Mar 27, 2025

Walkthrough

This 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

File(s) Change Summary
Wable-iOS.xcodeproj/project.pbxproj
Wable-iOS/Resource/Info.plist
Added new build entries for LoginViewModel.swift and OAuthEventManager.swift; removed OAuthErrorMonitor.swift; updated groups; added CFBundleURLTypes with Kakao URL scheme.
Wable-iOS/App/SceneDelegate.swift
Wable-iOS/Core/Logger/WableLogger.swift
Introduced new properties (tokenStorage, tokenProvider, loginRepository) and methods (setupBinding(), handleTokenExpired()) in SceneDelegate; updated error handling and log format.
Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift
Wable-iOS/Domain/RepositoryInterface/UserSessionRepository.swift
Removed auto-login methods (updateAutoLogin, checkAutoLogin), streamlining user session management.
Wable-iOS/Domain/UseCase/Login/FetchUserAuthUseCase.swift
Wable-iOS/Infra/Auth/AppleAuthProvider.swift
Wable-iOS/Infra/Auth/KakaoAuthProvider.swift
Wable-iOS/Infra/Auth/OAuthenticator.swift
Wable-iOS/Infra/Token/TokenStorage.swift
Added a tokenStorage property and updated token extraction/saving logic (renamed token from kakaoAccessToken to loginAccessToken); removed error monitor and refresh logic; enhanced logging and error handling.
Wable-iOS/Infra/Local/KeychainStorage.swift Improved encoding checks and error handling; updated query dictionaries to use the app’s bundle identifier; added logging for storage operations.
Wable-iOS/Infra/Network/APIProvider.swift
Wable-iOS/Infra/Network/MoyaLoggingPlugin.swift
Wable-iOS/Infra/Network/OAuth/OAuthEventManager.swift
Wable-iOS/Infra/Network/TargetType/BaseTargetType.swift
Wable-iOS/Infra/Network/TargetType/ContentTargetType.swift
Updated initialization of authentication interceptors and response mapping; introduced a logoutHandler and robust auth error checking; set validation type to .none and changed an endpoint URL.
Wable-iOS/Presentation/Home/HomeViewController.swift
Wable-iOS/Presentation/Login/LoginViewController.swift
Wable-iOS/Presentation/Login/LoginViewModel.swift
Wable-iOS/Presentation/TabBar/TabBarController.swift
Refactored presentation layer to use Combine for reactive flows; added dependency injection (e.g., contentRepostiory and LoginViewModel); redefined button actions and navigation; updated modal transition style.

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
Loading
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
Loading

Assessment against linked issues

Objective Addressed Explanation
Implement auto-login conditions and logic (#115) Auto-login methods were removed from repositories without introducing new logic to satisfy #115.

Poem

Oh, I’m a hopping rabbit in the code field bright,
Skipping through tokens and logs, everything feels light.
New paths added, old ones gone—such changes make me grin,
Combine flows and sleek functions—where do I begin?
With each line of code, my little heart beats “hip, hop, win!”
🥕 Happy coding from my burrow deep within!


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df533cd and c8f5610.

📒 Files selected for processing (4)
  • Wable-iOS/App/SceneDelegate.swift (4 hunks)
  • Wable-iOS/Core/Bundle/Bundle+.swift (1 hunks)
  • Wable-iOS/Infra/Local/KeychainStorage.swift (3 hunks)
  • Wable-iOS/Presentation/Login/LoginViewModel.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • Wable-iOS/Presentation/Login/LoginViewModel.swift
  • Wable-iOS/App/SceneDelegate.swift
🧰 Additional context used
🧬 Code Definitions (1)
Wable-iOS/Infra/Local/KeychainStorage.swift (1)
Wable-iOS/Infra/Local/UserDefaultsStorage.swift (1)
  • setValue (24-30)
🔇 Additional comments (6)
Wable-iOS/Core/Bundle/Bundle+.swift (1)

11-17: LGTM! The bundle identifier is a good addition to support social login.

The new static property follows the established pattern in this file and provides a clean way to access the bundle identifier, which is essential for OAuth configuration. Using fatalError() is appropriate since the bundle identifier is a critical requirement for the app to function correctly.

Wable-iOS/Infra/Local/KeychainStorage.swift (5)

12-13: Good addition of a reusable JSONDecoder property.

Adding a private JSONDecoder property improves performance by avoiding repeated decoder creation with each method call.


27-29: Correct use of bundle identifier for keychain service.

Good update using the proper key-value assignments in the query dictionary, with Bundle.identifier for the service and the provided key for the account.


45-46: Consistent key usage in getValue method.

The query dictionary now correctly uses the key for the account attribute and Bundle.identifier for the service attribute, ensuring consistency with the setValue method.


56-56: Appropriate error logging.

Good addition of error logging when keychain data is not found, providing both the key and status code for debugging.


70-71: Good use of the class property for JSON decoding.

Excellent use of the class-level jsonDecoder property for the fallback decoding path.

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a 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 mechanism

This 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 UIKit import 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 presentation

Presenting a UITabBarController directly 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 Home

This straightforward approach to present the TabBarController covers 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 presence

The guard let urlString check is good practice. Consider logging or handling cases where urlRequest.url is nil.


78-82: Empty refresh implementation

Leaving 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 logging

Appending the request body is valuable for debugging. Watch for potentially sensitive data in logs.


68-70: Symmetrical handling on failure

Re-checking for auth errors covers additional scenarios. Consider combining with success logic for code reuse.


103-142: Consider renaming condtion and clarifying logic

The variable name condtion is misspelled and can be confusing. Also, check for potential branching issues. If v1/auth/token appears 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 the receiveValue closure and [weak self] for the completion closure 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

📥 Commits

Reviewing files that changed from the base of the PR and between 41f9c6e and 1ee2873.

📒 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 consistency

The 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 .successCodes to .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 swift

Length of output: 3074


Critical Change in Response Validation – Custom Error Handling in Place

The change in BaseTargetType.swift to return .none disables 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 improved

Changed from storing as .kakaoAccessToken to .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 logging

Added 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 logic
  • OAuthEventManager.swift: Manages OAuth events, particularly token expiration events

These 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 login

The addition of CFBundleURLTypes with 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 correctly

Adding the TokenStorage instance is appropriate for the new token handling functionality.

Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift (1)

21-22: Improved code organization with MARK

Good use of a section marker to improve code readability.

Wable-iOS/Presentation/TabBar/TabBarController.swift (2)

15-15: HomeViewController dependency updated for content repository

The 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 added

The 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 kakaoAccessToken to loginAccessToken makes 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 Data first and then convert to String using 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 of map(\.data)) and an explicit self reference in the tryMap are 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 fatalError is a good practice for view controllers that are instantiated programmatically.

Wable-iOS/Presentation/Login/LoginViewController.swift (8)

10-13: Good adoption of Combine & CombineCocoa

Importing these frameworks is a solid step toward a reactive programming model.


18-20: Great approach with reactive properties

Defining viewModel and cancelBag as class properties clearly indicates a reactive architecture. This helps isolate responsibilities and manage Combine subscriptions effectively.


39-39: Kakao button layout is consistent

The 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 UIButton

While this offers flexibility in styling, ensure that additional logic (e.g., sign-in with Apple flows) is handled elsewhere, since native ASAuthorizationAppleIDButton offers 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 injection

This constructor-based dependency injection is a good design choice to ensure that LoginViewModel is always injected when initializing LoginViewController.


79-79: setupBinding invocation is well placed

Calling setupBinding in viewDidLoad centralizes reactive subscription setup.


90-98: Convenient batch addition of UI subviews

Using view.addSubviews(...) keeps view setup succinct and easy to read.


183-199: Navigation to Onboarding is straightforward

The code prompts a notice view and transitions to LCKYearViewController if 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 simplification

Removing errorMonitor clarifies the authenticator’s responsibility. Dependency injection of TokenStorage is straightforward and testable.


23-24: Descriptive doc comment

Well-documented purpose for the apply method. Keep scoping consistent if you add more calls (e.g., for other token types).


30-37: Flexible token type determination

Selecting loginAccessToken or wableAccessToken based on the URL path is well-structured. Ensure server endpoints remain stable.


39-52: Separate handling for token refresh requests

Injecting 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 logging

Logging the token load failure ensures visibility. Consider localized messaging or user notifications if critical.


67-67: didRequest short-circuit

Returning false is a clear approach if you intentionally skip automatic authentication error handling.

Wable-iOS/Infra/Network/MoyaLoggingPlugin.swift (3)

14-20: Logout handler addition

Introducing a LogoutHandler closure is a neat way to decouple logout logic. This fosters reusability and testability.


23-29: Constructor injection for logout and token storage

Allowing logoutHandler and tokenStorage as optional parameters increases flexibility. Good approach for custom error handling or logging flows.


65-65: Check for auth error after successful response

Good approach to unify error checks. This ensures no missed cases.

Wable-iOS/Infra/Local/KeychainStorage.swift (1)

43-44: Good move aligning the key with kSecAttrAccount.
Storing the custom key in kSecAttrAccount is 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.
Introducing tokenStorage, tokenProvider, and loginRepository clarifies the separation of concerns for managing tokens and user sessions.


46-47: Invoking setupBinding() is a good choice.
Calling setupBinding() 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 to AuthController.handleOpenUrl is essential for successful sign-in flow. Implementation here is concise and sound.


81-90: Login screen configuration is appropriate.
Setting up LoginViewController with the new LoginViewModel that references the FetchUserAuthUseCase fosters an organized architecture for authentication.


103-110: Binding tokenExpiredSubject for session management is clear.
Listening for token expiration events and taking immediate action keeps session state consistent and improves the user experience.

Comment on lines +27 to +37
.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)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

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

Comment on lines +102 to +115
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)
}
}
Copy link

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.

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

Comment on lines +35 to +53
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))
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Comment on lines +16 to +19
guard let stringValue = value as? String,
let stringData = stringValue.data(using: .utf8) else {
throw LocalError.saveFailed
}
Copy link

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Comment on lines +58 to +61
if T.self == Data.self {
WableLogger.log("\(data)", for: .debug)
return data as? T
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Comment on lines +63 to +67
if T.self == String.self,
let stringValue = String(data: data, encoding: .utf8) {
WableLogger.log("\(stringValue)", for: .debug)
return stringValue as? T
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Comment on lines +113 to +119
func handleTokenExpired() {
userSessionRepository.updateActiveUserID(nil)
configureLoginScreen()

let toast = ToastView(status: .caution, message: "세션이 만료되었습니다. 다시 로그인해주세요.")
toast.show()
}
Copy link

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.

Copy link

@coderabbitai coderabbitai bot left a 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 condition

There'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 condition

The 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 operators

Using 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 in

Or 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 method

The 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 information

The 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1ee2873 and df533cd.

📒 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 good

The new properties and type alias are well-structured to support the social login functionality. The LogoutHandler type alias makes the code more readable and the dependency injection pattern with tokenStorage provides good flexibility.


23-29: Good initializer implementation with default parameters

The 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 improvement

This minor change to use bodyString directly instead of bodyString.description is a small but good optimization.


65-65: Good implementation of auth error checking in both success and failure paths

Adding the auth error check in both response paths ensures comprehensive error handling regardless of the request outcome.

Also applies to: 68-70

coderabbitai bot added a commit that referenced this pull request Mar 28, 2025
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`
@Team-Wable Team-Wable deleted a comment from coderabbitai bot Mar 28, 2025
Copy link
Collaborator

@JinUng41 JinUng41 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다!

시퀀스 다이어그램을 통해 과정을 설명해주셔서 바로 이해할 수 있었습니다.
그리고 추가된 AI의 코드리뷰 또한 앞으로 개발함에 있어 많은 도움이 될 것 같아요.
저도 빨리 PR을 작성해 보고 싶네요.

끝으로, 중간중간 비동기 클로저 내에서 self 키워드를 직접 사용하는 부분이 보이는데요.
순환 참조와 메모리 누수의 위험성은 없는지 조금 더 신경 써주시면 좋겠습니다.

Comment on lines 70 to 74
if let url = URLContexts.first?.url {
if (AuthApi.isKakaoTalkLoginUrl(url)) {
_ = AuthController.handleOpenUrl(url: url)
}
}
Copy link
Collaborator

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)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

적용했습니다 !! 알려주셔서 감사합니닷 ㅎㅎ

Comment on lines +82 to +89
self.window?.rootViewController = LoginViewController(
viewModel: LoginViewModel(
useCase: FetchUserAuthUseCase(
loginRepository: loginRepository,
userSessionRepository: userSessionRepository
)
)
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이러한 코드는 제가 빠른 시일 내에 DI컨테이너를 공부하고 다시 알려드리겠습니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅠㅠ 감사합니다

Comment on lines 39 to 77
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()
)
}
Copy link
Collaborator

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()
    )
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

적용했습니다 !! 감사합니다 🥹

Comment on lines 45 to +46
modalPresentationStyle = .fullScreen
modalTransitionStyle = .crossDissolve
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment on lines 43 to 44
kSecAttrAccount as String: key, // key 값을 kSecAttrAccount에 사용해야 함
kSecAttrService as String: Bundle.main.bundleIdentifier ?? "com.wable.Wable-iOS",
Copy link
Collaborator

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로 묶어서 선언해도 좋을 것 같습니다.

Copy link
Member Author

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
        ]

Comment on lines 69 to 70
return try JSONDecoder().decode(T.self, from: data)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KeychainStorage가 JSONDecoder를 소유한다면, 매번 객체 생성이 필요 없을 것 같아요.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했습니다 ~! 감사합니닷

@youz2me youz2me merged commit d2f593d into develop Mar 30, 2025
1 check passed
@youz2me youz2me deleted the feat/#115-auto-login branch March 30, 2025 08:30
youz2me added a commit that referenced this pull request Oct 26, 2025
[Feat] 소셜 로그인 및 자동 로그인 기능 구현
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat 기능 또는 객체 구현 🦉 유진 🛌🛌🛌🛌🛌🛌🛌🛌🛌🛌

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 자동 로그인 조건 및 로직 추가 구현하기

3 participants