Skip to content

Conversation

@youz2me
Copy link
Member

@youz2me youz2me commented May 14, 2025

👻 PULL REQUEST

📄 작업 내용

  • FCM을 이용한 푸시 알림 기능을 구현했어요.
  • UserDefaultsRepository에서 세션이 제대로 저장되지 않던 오류를 개선했어요.
  • 로딩 화면이 앱 진입 시 자동 로그인이 활성화 되어있을 경우, 마이 탭에서 홈 탭으로 이동할 경우에만 뜨도록 수정했어요.
구현 내용 알림 수신 및 해당 알림 클릭 시 로딩 화면 등장 조건
IPhone 13 mini

👀 기타 더 이야기해볼 점

  • 수정된 파일이 많아 충돌날 것 같은데 그 부분은 제가 고쳐두겠습니다 ,,,

김진웅 화이팅!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

🔗 연결된 이슈

Summary by CodeRabbit

  • New Features

    • Integrated Firebase Cloud Messaging (FCM) for push notifications, including FCM token management and notification handling.
    • Added support for updating and displaying user notification badge counts.
    • Enhanced notification UI with badge status and rounded profile images.
    • Added image tap functionality to view images in detail from content and home screens.
  • Improvements

    • Streamlined user session and profile updates, including more flexible profile and badge management.
    • Improved notification handling, including badge reset on notification view refresh.
    • Enhanced navigation bar with dynamic title and notification status indicators.
  • Bug Fixes

    • Removed unused launch screen resource to prevent conflicts.
  • Style

    • Updated icons and visual elements for a more polished appearance.
  • Chores

    • Removed temporary UI elements and refactored code for better maintainability.

@youz2me youz2me requested a review from JinUng41 May 14, 2025 04:00
@youz2me youz2me self-assigned this May 14, 2025
@youz2me youz2me added ✨ feat 기능 또는 객체 구현 🦉 유진 🛌🛌🛌🛌🛌🛌🛌🛌🛌🛌 labels May 14, 2025
@coderabbitai
Copy link

coderabbitai bot commented May 14, 2025

Walkthrough

This change implements and integrates push notification (알림) functionality into the iOS app. It adds FCM (Firebase Cloud Messaging) token management, notification handling in the AppDelegate, badge updates, and necessary repository and use case logic. UI components are updated to reflect notification status, and supporting code is refactored to accommodate these features.

Changes

File(s) / Group Change Summary
Wable-iOS.xcodeproj/project.pbxproj Project file updated: added UpdateFCMTokenUseCase.swift, AppDelegate+Firebase.swift, UpdateUserBadgeUseCase.swift as sources; removed LaunchScreen.storyboard resource.
Wable-iOS/App/AppDelegate+Firebase.swift New file: Extends AppDelegate for FCM/notification integration, handles token registration, notification reception, badge decrement, and navigation to detail views.
Wable-iOS/App/AppDelegate.swift Adds repository/use case properties, FCM/badge logic, notification permission, and user session updates. Removes unused scene session method.
Wable-iOS/App/SceneDelegate.swift Adds profile repository. On login, fetches profile, updates user session, and updates profile with FCM token. Passes new use case to login view model.
Wable-iOS/Data/Mapper/ContentMapper.swift Refactors mapping to use title from response, not as a parameter.
Wable-iOS/Data/RepositoryImpl/AccountRepositoryImpl.swift, Wable-iOS/Infra/Network/TargetType/AccountTargetType.swift Changes badge update to use DTO request object instead of raw integer.
Wable-iOS/Data/RepositoryImpl/ContentRepositoryImpl.swift, Wable-iOS/Domain/RepositoryInterface/ContentRepository.swift, Wable-iOS/Domain/UseCase/Profile/FetchContentInfoUseCase.swift, Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift Removes title parameter from content info fetching logic and related view model.
Wable-iOS/Data/RepositoryImpl/ProfileRepositoryImpl.swift, Wable-iOS/Domain/RepositoryInterface/ProfileRepository.swift Adds FCM token fetch/update methods, updates profile update to support FCM token, and makes profile fields optional.
Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift, Wable-iOS/Domain/RepositoryInterface/UserSessionRepository.swift, Wable-iOS/Domain/UseCase/Home/FetchUserInformationUseCase.swift Refactors user session update to accept individual parameters, removes separate badge update method.
Wable-iOS/Domain/UseCase/Onboarding/CreateUserProfileUseCase.swift Passes FCM token to profile update.
Wable-iOS/Domain/UseCase/Onboarding/UpdateFCMTokenUseCase.swift New use case for updating FCM token in user profile.
Wable-iOS/Domain/UseCase/Profile/UpdateUserBadgeUseCase.swift New use case for updating user badge count.
Wable-iOS/Infra/Network/DTO/Response/Content/FetchContent.swift Adds contentTitle and isDeleted fields to content response DTO.
Wable-iOS/Infra/Token/TokenStorage.swift Adds FCM token type for secure storage.
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift, Wable-iOS/Presentation/Home/View/HomeViewController.swift, Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift Adds image tap handler to content cells, presents photo detail view, tracks loading screen display, and updates notification badge status in navigation.
Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift Adds badge count publisher to output, updates logic to fetch badge count from user session.
Wable-iOS/Presentation/Login/LoginViewController.swift Removes temporary login button and related code.
Wable-iOS/Presentation/Login/LoginViewModel.swift Adds FCM token update use case and logic to login flow. Refactors session update logic.
Wable-iOS/Presentation/Notification/Activity/View/ActivityNotificationViewController.swift, Wable-iOS/Presentation/Notification/Information/View/InformationNotificationViewController.swift Removes obsolete contentTitle parameter from detail view model initialization.
Wable-iOS/Presentation/Notification/Activity/ViewModel/ActivityNotificationViewModel.swift Adds badge reset logic and user session update on notification view load/refresh. Updates initializer for new use cases.
Wable-iOS/Presentation/Notification/Page/View/NotificationPageViewController.swift Injects real use cases for notification and badge logic, adjusts paging view layout.
Wable-iOS/Presentation/Onboarding/ViewController/AgreementViewController.swift Adds FCM token update logic to onboarding, updates session update call, and preserves badge count.
Wable-iOS/Presentation/Viewit/Create/View/Subview/ViewitInputView.swift Changes icon image for URL input.
Wable-iOS/Presentation/WableComponent/Cell/NotificationCell.swift Adds corner radius to profile image view.
Wable-iOS/Presentation/WableComponent/Navigation/NavigationView.swift Adds helper methods for setting navigation title and updating notification status icon.
Wable-iOS/Resource/Base.lproj/LaunchScreen 2.storyboard Removes unused launch screen storyboard resource.

Sequence Diagram(s)

sequenceDiagram
    participant AppDelegate
    participant Firebase
    participant UserSessionRepository
    participant ProfileRepository
    participant UpdateFCMTokenUseCase
    participant UpdateUserBadgeUseCase
    participant UNUserNotificationCenter
    participant ViewController

    AppDelegate->>Firebase: Configure FCM, register for notifications
    Firebase->>AppDelegate: Provides device token/FCM token
    AppDelegate->>ProfileRepository: saveFCMToken(token)
    AppDelegate->>UpdateFCMTokenUseCase: execute(nickname)
    UpdateFCMTokenUseCase->>ProfileRepository: fetchFCMToken()
    UpdateFCMTokenUseCase->>ProfileRepository: updateUserProfile(nickname, fcmToken)
    UNUserNotificationCenter->>AppDelegate: didReceive notification
    AppDelegate->>UpdateUserBadgeUseCase: execute(number)
    UpdateUserBadgeUseCase->>AccountRepository: updateUserBadge(badge)
    AppDelegate->>UserSessionRepository: updateUserSession(notificationBadgeCount)
    AppDelegate->>ViewController: (optional) Navigate to detail view
Loading

Assessment against linked issues

Objective Addressed Explanation
알림 기능 구현 (#177)

Suggested reviewers

  • JinUng41

Poem

A rabbit hops with ears held high,
"Notifications now can fly! 🐇📲
FCM tokens safe and sound,
Badges update, alerts abound.
Tap an image, see it grow—
In Wable’s world, you’re in the know!
Push complete, let’s watch it go!"

Tip

⚡️ Faster reviews with caching
  • CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.

Enjoy the performance boost—your workflow just got faster.

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 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.
    • Explain this complex logic.
    • 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 explain this code block.
    • @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 explain its main purpose.
    • @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.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

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 generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @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: 10

🔭 Outside diff range comments (2)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (1)

170-184: ⚠️ Potential issue

Strong capture of self and cell risks retain cycle

contentImageViewTapHandler stores a closure inside the cell; the closure captures self strongly, while the view controller retains the collection view → cell. This cycle (VC ➜ collectionView ➜ cell ➜ closure ➜ VC) prevents both from deallocating.

-                contentImageViewTapHandler: {
-                    guard let image = cell.contentImageView.image else {
-                        return
-                    }
-                    
-                    self.present(PhotoDetailViewController(image: image), animated: true)
-                },
+                contentImageViewTapHandler: { [weak self, weak cell] in
+                    guard
+                        let self,
+                        let image = cell?.contentImageView.image
+                    else { return }
+                    self.present(PhotoDetailViewController(image: image), animated: true)
+                },

The same pattern appears in other handlers; consider applying [weak self] consistently.

🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 170-170: Unused parameter in a closure should be replaced with _

(unused_closure_parameter)

Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (1)

35-40: 🛠️ Refactor suggestion

Handlers retained across reuse may leak or trigger stale actions

contentImageViewHandler, likeButtonTapHandler, etc. are not reset in prepareForReuse().
If a handler closes over a view controller, the cell will keep that controller alive until the handler is replaced, and taps on a reused cell could call logic from the previous screen.

     override func prepareForReuse() {
         super.prepareForReuse()
 
+        // Release handlers to break retain cycles & avoid stale callbacks
+        contentImageViewHandler = nil
+        likeButtonTapHandler = nil
+        profileImageViewTapHandler = nil
+        settingButtonTapHandler = nil
+        ghostButtonTapHandler = nil
+
         contentImageView.kf.cancelDownloadTask()
         contentImageView.image = nil
         contentTextView.text = nil
         titleTextView.text = nil
     }
🧹 Nitpick comments (13)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (2)

29-30: Static mutable flag is not thread-safe and survives app life-cycle

hasShownLoadingScreen is mutated on the main queue but is technically accessible from any thread.
Using static also means it will never reset until the app is killed, so a subsequent log-out/login inside the same run won’t show the loading screen again, even if desired.

If the intent is “once per app launch, on main thread” document it explicitly or move the flag into AppDelegate / SceneDelegate where life-cycle ownership is clearer.


98-103: Guard clause reads better & prevents accidental double-presentation

-if shouldShowLoadingScreen && !HomeViewController.hasShownLoadingScreen {
-    showLoadingScreen()
-    HomeViewController.hasShownLoadingScreen = true
-}
+guard shouldShowLoadingScreen,
+      !HomeViewController.hasShownLoadingScreen else { return }
+HomeViewController.hasShownLoadingScreen = true
+showLoadingScreen()

Minor, but keeps early-exit semantics consistent with the rest of the class.

Wable-iOS/Presentation/WableComponent/Navigation/NavigationView.swift (2)

274-275: setNavigationTitle only updates hub titles

setNavigationTitle(text:) writes to hubTitleLabel, so calling it on a .page navigation view has no effect.
Either:

  1. Rename to setHubTitle to clarify intent, or
  2. Add a switch to update the correct label per NavigationType.

277-281: DRY violation: duplicated notification-badge logic

updateNotificationStatus(hasNewNotification:) repeats code already present in configureView() (lines 221-228). Consider extracting the image-selection into a private helper to avoid divergent behaviour when the asset names change.

private func notificationIcon(for state: Bool) -> UIImage? {
    return state ? .icNotiBadge : .icNotiDefault
}

Then both places can use notificationButton.setImage(notificationIcon(for: …), for: .normal).

Wable-iOS/Presentation/Onboarding/ViewController/AgreementViewController.swift (2)

169-177: Consider adding retry logic for FCM token updates.

The FCM token update implementation looks good, but it only logs errors without retry logic or user notification. Consider implementing a retry mechanism for critical operations like this.

self.updateFCMTokenUseCase.execute(nickname: owner.nickname)
    .sink { completion in
        if case .failure(let error) = completion {
            WableLogger.log("FCM 토큰 저장 중 에러 발생: \(error)", for: .error)
+           // Consider implementing retry logic here
+           // Or notify the user if this is a critical error that affects functionality
        }
    } receiveValue: { () in
    }
    .store(in: cancelBag)

169-177: Inconsistent references to nickname property.

There's inconsistency in how the nickname is referenced - using owner.nickname in the FCM token update but self.nickname in the welcome message. Consider standardizing to use self throughout for consistency.

-self.updateFCMTokenUseCase.execute(nickname: owner.nickname)
+self.updateFCMTokenUseCase.execute(nickname: self.nickname)

Also applies to: 203-203

Wable-iOS/Domain/UseCase/Onboarding/UpdateFCMTokenUseCase.swift (1)

22-28: Consider implementing a retry mechanism for FCM token retrieval.

The implementation correctly handles the case when the FCM token is not available by returning an error. However, since FCM token retrieval might be asynchronous or delayed in some cases, consider implementing a retry mechanism or a callback approach for situations where the token might become available shortly after this method is called.

func execute(nickname: String) -> AnyPublisher<Void, WableError> {
    guard let token = repository.fetchFCMToken() else {
+       // Consider implementing a retry mechanism here
+       // Example: return retryWhen condition occurs
        return .fail(WableError.noToken)
    }
    
    return repository.updateUserProfile(nickname: nickname, fcmToken: token)
}
Wable-iOS/Domain/RepositoryInterface/ProfileRepository.swift (1)

18-26: Method overloads are drifting – prefer a single variadic method with defaulted parameters

Maintaining two updateUserProfile overloads (lines 18-26) invites ambiguity and duplicated implementation in concrete repositories.
Because every parameter is already optional, you can collapse them into one method with sensible defaults:

func updateUserProfile(
    nickname: String? = nil,
    profile: UserProfile? = nil,
    isPushAlarmAllowed: Bool? = nil,
    isAlarmAllowed: Bool? = nil,
    image: UIImage? = nil,
    fcmToken: String? = nil,
    defaultProfileType: String? = nil
) -> AnyPublisher<Void, WableError>

This keeps the public surface small and far less error-prone for future-callers.

Wable-iOS/Presentation/Login/LoginViewModel.swift (1)

72-90: Chain FCM-token persistence and session update to guarantee ordering and reduce duplicate code

updateFCMTokenUseCase and updateUserSessionUseCase.updateUserSession are launched in parallel.
If the profile update fails but the session is already stored, the app may display stale data.
Chaining the publishers ensures “token saved → session updated” semantics and removes the inner sink:

self.updateFCMTokenUseCase.execute(nickname: account.user.nickname)
-    .sink { completion in
-        if case .failure(let error) = completion {
-            WableLogger.log("FCM 토큰 저장 중 에러 발생: \(error)", for: .error)
-        }
-    } receiveValue: { () in
-    }
-    .store(in: cancelBag)
-                    
-self.updateUserSessionUseCase.updateUserSession(
-    userID: account.user.id,
-    nickname: account.user.nickname,
-    profileURL: account.user.profileURL,
-    isPushAlarmAllowed: account.isPushAlarmAllowed,
-    isAdmin: account.isAdmin,
-    isAutoLoginEnabled: true
-)
-...
+    .flatMap { [unowned self] _ in
+        self.updateUserSessionUseCase.updateUserSession(
+            userID: account.user.id,
+            nickname: account.user.nickname,
+            profileURL: account.user.profileURL,
+            isPushAlarmAllowed: account.isPushAlarmAllowed,
+            isAdmin: account.isAdmin,
+            isAutoLoginEnabled: true
+        )
+    }
+    .sink { completion in
+        if case .failure(let error) = completion {
+            WableLogger.log("로그인 후 세션 저장 중 에러: \(error)", for: .error)
+        }
+    } receiveValue: { _ in
+        WableLogger.log("세션 저장 완료", for: .debug)
+    }
+    .store(in: cancelBag)

This guarantees data consistency and simplifies the reactive flow.

Wable-iOS/App/AppDelegate.swift (1)

70-75: Resolve SwiftLint warning – discard unused error parameter

The closure never touches error, so SwiftLint raises unused_closure_parameter.

-        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { [weak self] granted, error in
+        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { [weak self] granted, _ in

This keeps the signature clean and silences the warning.

🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 70-70: Unused parameter in a closure should be replaced with _

(unused_closure_parameter)

Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (1)

243-247: Allow a default value for the new tap handler to ease migration

Every call-site now has to pass contentImageViewTapHandler, even when not needed.
Adding a default nil keeps the API source-compatible and reduces churn:

-        cellType: CellType = .list,
-        contentImageViewTapHandler: (() -> Void)?,
+        cellType: CellType = .list,
+        contentImageViewTapHandler: (() -> Void)? = nil,
Wable-iOS/App/AppDelegate+Firebase.swift (2)

14-22: Firebase token registration implemented correctly.

The implementation properly registers for remote notifications, sets the APNs token for Firebase Messaging, and retrieves the FCM token. However, there's an unused parameter in the closure.

-        Messaging.messaging().token() { [weak self] token, error in
+        Messaging.messaging().token() { [weak self] token, _ in
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 17-17: Unused parameter in a closure should be replaced with _

(unused_closure_parameter)


89-107: HomeDetailViewController initialization may be too coupled.

The initialization of HomeDetailViewController with its dependencies is very verbose and may indicate tight coupling.

Consider using dependency injection or a factory pattern to create view controllers with their dependencies. This would make the code more maintainable and testable.

// Example factory approach
func createHomeDetailViewController(contentID: Int) -> HomeDetailViewController {
    return HomeDetailViewController(
        viewModel: HomeDetailViewModel(
            contentID: contentID,
            fetchContentInfoUseCase: FetchContentInfoUseCase(repository: contentRepository),
            // other dependencies...
        ),
        cancelBag: CancelBag()
    )
}

Then you could simplify this code to:

let detailViewController = createHomeDetailViewController(contentID: contentID)
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between ba3bbc2 and c8d8a2d.

📒 Files selected for processing (37)
  • Wable-iOS.xcodeproj/project.pbxproj (8 hunks)
  • Wable-iOS/App/AppDelegate+Firebase.swift (1 hunks)
  • Wable-iOS/App/AppDelegate.swift (2 hunks)
  • Wable-iOS/App/SceneDelegate.swift (3 hunks)
  • Wable-iOS/Data/Mapper/ContentMapper.swift (2 hunks)
  • Wable-iOS/Data/RepositoryImpl/AccountRepositoryImpl.swift (1 hunks)
  • Wable-iOS/Data/RepositoryImpl/ContentRepositoryImpl.swift (1 hunks)
  • Wable-iOS/Data/RepositoryImpl/ProfileRepositoryImpl.swift (2 hunks)
  • Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift (2 hunks)
  • Wable-iOS/Domain/RepositoryInterface/ContentRepository.swift (1 hunks)
  • Wable-iOS/Domain/RepositoryInterface/ProfileRepository.swift (1 hunks)
  • Wable-iOS/Domain/RepositoryInterface/UserSessionRepository.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Home/FetchUserInformationUseCase.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Login/FetchUserAuthUseCase.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Onboarding/CreateUserProfileUseCase.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Onboarding/UpdateFCMTokenUseCase.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Profile/FetchContentInfoUseCase.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Profile/UpdateUserBadgeUseCase.swift (1 hunks)
  • Wable-iOS/Infra/Network/DTO/Response/Content/FetchContent.swift (1 hunks)
  • Wable-iOS/Infra/Network/TargetType/AccountTargetType.swift (2 hunks)
  • Wable-iOS/Infra/Token/TokenStorage.swift (1 hunks)
  • Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (2 hunks)
  • Wable-iOS/Presentation/Home/View/HomeViewController.swift (4 hunks)
  • Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (3 hunks)
  • Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (4 hunks)
  • Wable-iOS/Presentation/Login/LoginViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Login/LoginViewModel.swift (2 hunks)
  • Wable-iOS/Presentation/Notification/Activity/View/ActivityNotificationViewController.swift (0 hunks)
  • Wable-iOS/Presentation/Notification/Activity/ViewModel/ActivityNotificationViewModel.swift (3 hunks)
  • Wable-iOS/Presentation/Notification/Information/View/InformationNotificationViewController.swift (0 hunks)
  • Wable-iOS/Presentation/Notification/Page/View/NotificationPageViewController.swift (2 hunks)
  • Wable-iOS/Presentation/Onboarding/ViewController/AgreementViewController.swift (3 hunks)
  • Wable-iOS/Presentation/Viewit/Create/View/Subview/ViewitInputView.swift (1 hunks)
  • Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (4 hunks)
  • Wable-iOS/Presentation/WableComponent/Cell/NotificationCell.swift (1 hunks)
  • Wable-iOS/Presentation/WableComponent/Navigation/NavigationView.swift (1 hunks)
  • Wable-iOS/Resource/Base.lproj/LaunchScreen 2.storyboard (0 hunks)
💤 Files with no reviewable changes (3)
  • Wable-iOS/Presentation/Notification/Activity/View/ActivityNotificationViewController.swift
  • Wable-iOS/Presentation/Notification/Information/View/InformationNotificationViewController.swift
  • Wable-iOS/Resource/Base.lproj/LaunchScreen 2.storyboard
🧰 Additional context used
🧬 Code Graph Analysis (11)
Wable-iOS/Domain/UseCase/Onboarding/CreateUserProfileUseCase.swift (1)
Wable-iOS/Data/RepositoryImpl/ProfileRepositoryImpl.swift (1)
  • fetchFCMToken (44-50)
Wable-iOS/Data/RepositoryImpl/ContentRepositoryImpl.swift (1)
Wable-iOS/Data/Mapper/ContentMapper.swift (3)
  • toDomain (13-51)
  • toDomain (53-99)
  • toDomain (101-144)
Wable-iOS/Domain/RepositoryInterface/ContentRepository.swift (1)
Wable-iOS/Data/RepositoryImpl/ContentRepositoryImpl.swift (1)
  • fetchContentInfo (46-53)
Wable-iOS/Infra/Network/TargetType/AccountTargetType.swift (1)
Wable-iOS/Data/RepositoryImpl/AccountRepositoryImpl.swift (1)
  • updateUserBadge (42-49)
Wable-iOS/Domain/UseCase/Profile/UpdateUserBadgeUseCase.swift (1)
Wable-iOS/Data/RepositoryImpl/AccountRepositoryImpl.swift (1)
  • updateUserBadge (42-49)
Wable-iOS/Domain/UseCase/Home/FetchUserInformationUseCase.swift (1)
Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift (1)
  • updateUserSession (50-78)
Wable-iOS/Domain/RepositoryInterface/UserSessionRepository.swift (2)
Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift (1)
  • updateUserSession (50-78)
Wable-iOS/Domain/UseCase/Home/FetchUserInformationUseCase.swift (1)
  • updateUserSession (33-54)
Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (1)
Wable-iOS/Domain/UseCase/Home/FetchUserInformationUseCase.swift (1)
  • fetchActiveUserInfo (28-31)
Wable-iOS/Presentation/Notification/Activity/ViewModel/ActivityNotificationViewModel.swift (2)
Wable-iOS/Presentation/Notification/Activity/View/ActivityNotificationViewController.swift (1)
  • viewDidLoad (77-88)
Wable-iOS/Core/Logger/WableLogger.swift (1)
  • log (14-25)
Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (1)
Wable-iOS/Domain/UseCase/Profile/FetchContentInfoUseCase.swift (1)
  • execute (22-24)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (1)
Wable-iOS/Presentation/WableComponent/Navigation/NavigationView.swift (1)
  • updateNotificationStatus (277-281)
🪛 SwiftLint (0.57.0)
Wable-iOS/App/AppDelegate.swift

[Warning] 16-16: TODOs should be resolved (Repository를 UseCase로 변경)

(todo)


[Warning] 70-70: Unused parameter in a closure should be replaced with _

(unused_closure_parameter)

Wable-iOS/App/AppDelegate+Firebase.swift

[Warning] 17-17: Unused parameter in a closure should be replaced with _

(unused_closure_parameter)

🔇 Additional comments (53)
Wable-iOS/Presentation/Viewit/Create/View/Subview/ViewitInputView.swift (1)

24-24: Verify the urlIconImageView asset reference.

The icon was changed to .add. Please confirm that this asset exists in the catalog (or is provided via an extension) and that it matches the intended design for the URL input action.

Wable-iOS/Presentation/WableComponent/Cell/NotificationCell.swift (1)

22-22: Good UI enhancement for profile image

Adding a corner radius of 18 points (half of the 36-point width/height) creates a perfectly circular profile image view, which is consistent with modern UI design patterns and improves the visual presentation of user avatars in notifications.

Wable-iOS/Infra/Token/TokenStorage.swift (1)

16-16: Good addition of FCM token storage

Adding the fcmToken case to the TokenType enum is essential for securely storing the Firebase Cloud Messaging token in the keychain. This properly enables push notification functionality while maintaining security best practices for token storage.

Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (2)

105-105: Good UI presentation style improvement

Setting modalPresentationStyle = .fullScreen ensures a better user experience by having the view controller fill the entire screen when presented modally.


185-189: Good implementation of image viewing functionality

Adding the content image view tap handler improves user experience by allowing users to view images in a dedicated photo detail screen. The implementation correctly checks if the image exists before presenting the detail view.

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

44-44: Good API integration for FCM badge updates

The modification to wrap the badge count in a DTO.Request.UpdateUserBadge object properly aligns with the API endpoint expectations. This change supports the push notification feature by ensuring badge counts are properly synchronized with the server.

Wable-iOS/Domain/UseCase/Onboarding/CreateUserProfileUseCase.swift (1)

27-27: Integrates FCM token with user profile creation, good addition

This change successfully incorporates FCM token management into the user profile creation flow, which is essential for the push notification functionality mentioned in the PR objectives. The token is fetched from the repository and passed during profile update, allowing the server to associate push notifications with specific users.

Wable-iOS/Domain/RepositoryInterface/ContentRepository.swift (1)

15-15: Simplified interface by removing unnecessary parameter

Removing the title parameter from the fetchContentInfo method is a good design improvement as it simplifies the interface. According to the context, the title will now be retrieved directly from the content response rather than passed as a parameter.

Could you clarify how this change relates to the FCM notification implementation mentioned in the PR objectives? Is it possible that notifications might include content titles that need to be fetched?

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

46-53: Implementation correctly aligns with protocol changes

The implementation has been updated to match the protocol changes, removing the title parameter and simplifying the mapping. The code now relies on the title embedded in the content response, which is a more robust approach.

Wable-iOS/Domain/UseCase/Profile/FetchContentInfoUseCase.swift (1)

22-23: Updated use case correctly matches repository interface

The use case has been properly updated to reflect the changes in the repository interface. The simplified method signature now only requires the essential contentID parameter to fetch content information.

Wable-iOS/Data/Mapper/ContentMapper.swift (2)

13-13: Method signature updated to use DTO's contentTitle directly

The method signature has been simplified by removing the external title parameter, now directly relying on response.contentTitle from the DTO. This is a good refactoring that reduces redundancy and aligns with the DTO structure updates.


40-40: Updated to use response.contentTitle directly

The title field now uses response.contentTitle directly from the response DTO, matching the method signature change. This simplifies the data flow and removes dependency on external parameters.

Wable-iOS/Infra/Network/DTO/Response/Content/FetchContent.swift (3)

23-23: Added contentTitle property to FetchContent DTO

Good addition of the contentTitle field to the DTO. This enables direct access to the content title from the response, supporting the push notification feature and simplifying the mapper implementation.


25-25: Added isDeleted flag to track content status

The addition of the isDeleted flag is important for the notification feature, allowing the app to check if referenced content has been deleted before showing notifications.


33-33: Updated CodingKeys to include new properties

CodingKeys have been properly updated to include the new properties, ensuring they'll be correctly decoded from the JSON response.

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

87-88: Removed temporary login button

The removal of the temporary login button is appropriate as the PR implements a proper login flow with FCM token management. This cleanup helps maintain code quality by removing development shortcuts.

Wable-iOS/Domain/UseCase/Login/FetchUserAuthUseCase.swift (3)

27-38: Enhanced token handling with improved logging

The token saving logic has been improved with:

  1. Better formatting with a multi-line closure for readability
  2. Explicit debug logging after saving both tokens
  3. Proper error logging for failures

These changes improve the debugging experience and align with the FCM push notification implementation.


40-48: Updated user session management for notification support

The user session update now passes nil for notificationBadgeCount instead of a hardcoded 0, which is more appropriate as it allows the repository to determine the default value. This change supports the push notification feature by not overriding potentially existing notification counts.


50-51: Improved code formatting

The call to update the active user ID is now formatted more clearly, improving readability.

Wable-iOS/Infra/Network/TargetType/AccountTargetType.swift (2)

16-16: Well-structured API design change for badge management.

The evolution from a simple Int parameter to a structured DTO.Request.UpdateUserBadge object is a good design decision. This provides better type safety, documentation of the API contract, and allows for future extensibility if additional badge-related parameters are needed.


48-49: Consistent request body handling.

This implementation now aligns with the pattern used for other cases like deleteAccount, creating consistency across the codebase. The change properly maps the structured request object to the request body, completing the refactoring started with the parameter type change.

Wable-iOS.xcodeproj/project.pbxproj (4)

154-155: New UseCase added for FCM token management.

This file UpdateFCMTokenUseCase.swift is correctly added to handle token updates for Firebase Cloud Messaging, which is essential for the push notification implementation. It's appropriately placed in the Onboarding module, likely to register/update tokens during user onboarding and login flows.


159-159: Firebase integration added to AppDelegate.

The file AppDelegate+Firebase.swift extends the AppDelegate to handle Firebase-specific functionality. This is a good architectural decision to separate concerns by placing Firebase configuration and notification handling in a dedicated extension rather than cluttering the main AppDelegate.


160-161: Badge management UseCase added for notifications.

The UpdateUserBadgeUseCase.swift file is added to the Profile module to handle notification badge updates. This will likely be used to update the UI badge counts when notifications are received, read, or cleared.


1-2884: Project structure is properly maintained.

The newly added files for FCM push notifications are correctly integrated into their appropriate modules within the project structure. The project already includes the necessary Firebase dependencies (FirebaseCore and FirebaseMessaging), which are required for push notification functionality.

Wable-iOS/Domain/UseCase/Home/FetchUserInformationUseCase.swift (1)

33-54: LGTM! Well-structured method signature for updating user session attributes.

The implementation correctly accepts individual optional parameters for user session attributes and properly delegates to the repository layer using the Just publisher. This approach provides more flexibility by allowing selective updates to specific session attributes.

Wable-iOS/Domain/RepositoryInterface/UserSessionRepository.swift (1)

19-25: Effective protocol refactoring for user session updates.

The protocol now uses a single consolidated method with optional parameters rather than multiple specific methods. This simplifies the interface while maintaining flexibility for partial updates.

Wable-iOS/Presentation/Notification/Page/View/NotificationPageViewController.swift (2)

112-129: Good implementation of real use cases replacing mock dependencies.

The code properly initializes the activity notification view model with three distinct use cases:

  1. Notification use case for fetching notifications
  2. User badge use case for updating badge counts
  3. User information use case for managing user session data

This change supports the new push notification functionality by providing the necessary dependencies for badge management.


141-142: UI refinement with proper top offset.

The 10-point top offset for the paging view improves the vertical alignment and provides better visual spacing.

Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (2)

141-141: Content title parameter removal aligns with data layer changes.

Removed the unnecessary contentTitle parameter from the fetchContentInfoUseCase.execute call, which now retrieves the title from the response data directly.


303-313: Simplified content data fetching with proper null checks.

The code now uses a guard statement for early return and no longer passes the content title parameter when fetching content information, aligning with the updated use case interface.

Wable-iOS/Presentation/Onboarding/ViewController/AgreementViewController.swift (3)

23-23: Good addition of FCM token update capability.

The updateFCMTokenUseCase properly encapsulates the token update functionality, adhering to clean architecture principles by using dependency injection with the repository.


159-159: Improved push notification handling.

Nice update to use the actual marketing checkbox state for the isPushAlarmAllowed parameter instead of the hardcoded false value. This ensures user preferences are correctly captured.


178-186: Good preservation of notification badge count.

The user session update now properly preserves the existing notificationBadgeCount when updating other session parameters, which is important for maintaining notification state.

Wable-iOS/Domain/UseCase/Profile/UpdateUserBadgeUseCase.swift (2)

11-17: Well-structured use case with proper dependency injection.

The UpdateUserBadgeUseCase class follows clean architecture principles by depending on the AccountRepository abstraction rather than a concrete implementation, which enhances testability and modularity.


21-25: Clean and focused implementation.

The execute method has a clear purpose and follows good functional reactive programming practices with Combine. It's appropriately returning an error when needed and delegating to the repository layer.

Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (4)

58-58: Good addition of badge count publisher to Output.

This addition properly exposes the notification badge count to subscribers, following the established pattern for the view model's outputs.


75-75: Well-structured subject for badge count tracking.

The badgeCountSubject is correctly implemented as a CurrentValueSubject with an appropriate initial value of nil, consistent with other subjects in this view model.


94-102: Good implementation of user session and badge count retrieval.

The code now properly retrieves the full UserSession? object and extracts both the admin status and notification badge count. This single fetch operation for multiple values is efficient and maintains data consistency.


315-315: Output structure correctly updated to include badge count.

The badge count publisher is properly included in the Output struct initialization, making it available to subscribers.

Wable-iOS/Domain/UseCase/Onboarding/UpdateFCMTokenUseCase.swift (1)

11-17: Well-structured use case with proper dependency injection.

The UpdateFCMTokenUseCase follows clean architecture principles with proper separation of concerns and dependency injection. This makes the code testable and maintainable.

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

50-70: Confirm default values, especially auto-login, before shipping

isAutoLoginEnabled falls back to true when a brand-new session is created (existingSession == nil & parameter is nil).
That means the very first time a user logs in they will automatically be opted-in to auto-login, which is easy to overlook and may be contrary to privacy requirements.

isAutoLoginEnabled: isAutoLoginEnabled ?? existingSession?.isAutoLoginEnabled ?? true,

Consider whether false is a safer default:

-            isAutoLoginEnabled: isAutoLoginEnabled ?? existingSession?.isAutoLoginEnabled ?? true,
+            isAutoLoginEnabled: isAutoLoginEnabled ?? existingSession?.isAutoLoginEnabled ?? false,

(or force the caller to pass an explicit value).

Please double-check the product spec; if auto-login should remain off by default, apply the change above or pass the flag explicitly from the login flow.

Wable-iOS/Data/RepositoryImpl/ProfileRepositoryImpl.swift (6)

17-17: Good addition of token storage for FCM token management.

The tokenStorage property using KeychainStorage is a secure way to store sensitive tokens like FCM tokens, which is important for maintaining user privacy and security.


21-42: New method to update user profile with nickname and FCM token.

This implementation correctly adds the ability to update a user's profile with just a nickname and optional FCM token, keeping all other profile fields as nil. This supports the FCM token registration flow without requiring all profile fields.


44-50: FCM token retrieval with proper error handling.

The implementation properly handles errors when attempting to load the FCM token from storage, returning nil instead of propagating the error, which is appropriate for this use case.


52-59: FCM token update with appropriate logging.

The method correctly handles token storage with error handling and logging. This is important for debugging any issues with FCM token updates.


79-85: Enhanced profile update method with FCM token support.

The updated signature properly makes all parameters optional with default values, allowing for partial profile updates. The addition of the FCM token parameter is consistent with the PR's objectives.


91-97: Updated profile fields mapping with FCM token integration.

The modified implementation correctly handles mapping profile fields including the FCM token to the DTO request structure. The use of optional chaining with profile properties is appropriate.

Wable-iOS/App/AppDelegate+Firebase.swift (5)

28-40: Firebase configuration is well-implemented.

The method properly configures Firebase with appropriate notification permissions (alert, sound, badge). Setting logger level to .min is a good practice for production code. The implementation correctly sets up the messaging delegate and notification center delegate.


42-46: FCM token saving method is properly implemented.

The method correctly guards against nil tokens and uses the profileRepository to update the token.


51-55: MessagingDelegate implementation handles token refresh.

The implementation correctly updates the FCM token when a new registration token is received from Firebase.


81-87: Content navigation from notification properly guarded.

The implementation correctly guards against missing content ID, scene, or root view controller before attempting navigation.


120-128: Foreground notification presentation configured correctly.

The method properly configures how notifications should be presented when the app is in the foreground, enabling banner, sound, and badge options.

private let tokenProvider = OAuthTokenProvider()

private let loginRepository = LoginRepositoryImpl()
private let profileRepository = ProfileRepositoryImpl()
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Duplicate repository instance may lead to inconsistent state

profileRepository is declared as a stored property (line 22), yet new instances of ProfileRepositoryImpl are created later (e.g. in configureLoginScreen). Re-using the shared instance avoids redundant network caches / Keychain access and guarantees a single source of truth.

-    private let profileRepository = ProfileRepositoryImpl()
+    private let profileRepository = ProfileRepositoryImpl()   // keep one instance

Consider injecting this same instance wherever it is needed.

📝 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
private let profileRepository = ProfileRepositoryImpl()
private let profileRepository = ProfileRepositoryImpl() // keep one instance

Comment on lines +87 to 90
updateFCMTokenUseCase: UpdateFCMTokenUseCase(
repository: ProfileRepositoryImpl()
),
fetchUserAuthUseCase: FetchUserAuthUseCase(
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Reuse the existing profileRepository instead of instantiating a new one

Here a fresh ProfileRepositoryImpl() is passed into UpdateFCMTokenUseCase, defeating any benefit of the singleton created on line 22.

-                updateFCMTokenUseCase: UpdateFCMTokenUseCase(
-                    repository: ProfileRepositoryImpl()
-                ),
+                updateFCMTokenUseCase: UpdateFCMTokenUseCase(
+                    repository: profileRepository     // reuse
+                ),

Besides memory overhead, two repositories can race while reading / writing FCM tokens to storage.

📝 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
updateFCMTokenUseCase: UpdateFCMTokenUseCase(
repository: ProfileRepositoryImpl()
),
fetchUserAuthUseCase: FetchUserAuthUseCase(
updateFCMTokenUseCase: UpdateFCMTokenUseCase(
repository: profileRepository // reuse
),
fetchUserAuthUseCase: FetchUserAuthUseCase(

Comment on lines +100 to +131
if let id = userSessionRepository.fetchActiveUserID() {
profileRepository.fetchUserProfile(memberID: id)
.receive(on: DispatchQueue.main)
.sink { _ in
} receiveValue: { [weak self] info in
guard let self = self else { return }
let token = self.profileRepository.fetchFCMToken()

self.userSessionRepository.updateUserSession(
userID: id,
nickname: info.user.nickname,
profileURL: info.user.profileURL,
isAutoLoginEnabled: true
)

self.profileRepository.updateUserProfile(nickname: info.user.nickname, fcmToken: token)
.sink { completion in
switch completion {
case .failure(let error):
WableLogger.log("토큰 업데이트 실패: \(error)", for: .error)
default:
break
}
} receiveValue: { _ in
WableLogger.log("토큰 업데이트 성공", for: .debug)
}
.store(in: cancelBag)

}
.store(in: cancelBag)

}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Missing error handling and optional-token guard in the login flow

  1. The upstream publisher fetchUserProfile ignores its failure case – a network error will silently skip the rest of the chain.
  2. fetchFCMToken() may return nil; passing it straight into updateUserProfile risks sending “null” to your backend.
profileRepository.fetchUserProfile(memberID: id)
+    .catch { [weak self] error -> Empty<ProfileInfo, Never> in
+        WableLogger.log("프로필 조회 실패: \(error)", for: .error)
+        return .init()           // bail-out; still completes
+    }
...
-    let token = self.profileRepository.fetchFCMToken()
+    guard let token = self.profileRepository.fetchFCMToken() else {
+        WableLogger.log("FCM 토큰이 없어 업데이트를 생략합니다.", for: .debug)
+        return
+    }

Fail-fast logging helps diagnose login issues early.

📝 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
if let id = userSessionRepository.fetchActiveUserID() {
profileRepository.fetchUserProfile(memberID: id)
.receive(on: DispatchQueue.main)
.sink { _ in
} receiveValue: { [weak self] info in
guard let self = self else { return }
let token = self.profileRepository.fetchFCMToken()
self.userSessionRepository.updateUserSession(
userID: id,
nickname: info.user.nickname,
profileURL: info.user.profileURL,
isAutoLoginEnabled: true
)
self.profileRepository.updateUserProfile(nickname: info.user.nickname, fcmToken: token)
.sink { completion in
switch completion {
case .failure(let error):
WableLogger.log("토큰 업데이트 실패: \(error)", for: .error)
default:
break
}
} receiveValue: { _ in
WableLogger.log("토큰 업데이트 성공", for: .debug)
}
.store(in: cancelBag)
}
.store(in: cancelBag)
}
if let id = userSessionRepository.fetchActiveUserID() {
profileRepository.fetchUserProfile(memberID: id)
.catch { [weak self] error -> Empty<ProfileInfo, Never> in
WableLogger.log("프로필 조회 실패: \(error)", for: .error)
return .init() // bail-out; still completes
}
.receive(on: DispatchQueue.main)
.sink { _ in
} receiveValue: { [weak self] info in
guard let self = self else { return }
guard let token = self.profileRepository.fetchFCMToken() else {
WableLogger.log("FCM 토큰이 없어 업데이트를 생략합니다.", for: .debug)
return
}
self.userSessionRepository.updateUserSession(
userID: id,
nickname: info.user.nickname,
profileURL: info.user.profileURL,
isAutoLoginEnabled: true
)
self.profileRepository.updateUserProfile(nickname: info.user.nickname, fcmToken: token)
.sink { completion in
switch completion {
case .failure(let error):
WableLogger.log("토큰 업데이트 실패: \(error)", for: .error)
default:
break
}
} receiveValue: { _ in
WableLogger.log("토큰 업데이트 성공", for: .debug)
}
.store(in: cancelBag)
}
.store(in: cancelBag)
}

Comment on lines +344 to +355
output.badgeCount
.receive(on: DispatchQueue.main)
.sink { [weak self] count in
guard let self = self,
let count = count
else {
return
}

self.navigationView.updateNotificationStatus(hasNewNotification: count > 0)
}
.store(in: cancelBag)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

badgeCount nil case leaves stale UI

When output.badgeCount publishes nil, the navigation bar retains whatever badge state it had previously.
If the server clears notifications and your view model publishes nil (or finishes), the icon will still show the red badge.

A safer approach:

-guard let self = self,
-      let count = count
-else {
-    return
-}
-
-self.navigationView.updateNotificationStatus(hasNewNotification: count > 0)
+guard let self else { return }
+self.navigationView.updateNotificationStatus(hasNewNotification: (count ?? 0) > 0)

This treats “no value” as “no new notifications”.

📝 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
output.badgeCount
.receive(on: DispatchQueue.main)
.sink { [weak self] count in
guard let self = self,
let count = count
else {
return
}
self.navigationView.updateNotificationStatus(hasNewNotification: count > 0)
}
.store(in: cancelBag)
output.badgeCount
.receive(on: DispatchQueue.main)
.sink { [weak self] count in
guard let self else { return }
self.navigationView.updateNotificationStatus(hasNewNotification: (count ?? 0) > 0)
}
.store(in: cancelBag)

Comment on lines +16 to +18
func fetchFCMToken() -> String?
func updateFCMToken(token: String)
func updateUserProfile(nickname: String, fcmToken: String?) -> AnyPublisher<Void, WableError>
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 returning publishers for token I/O to keep the interface consistent

All existing read / write operations in this protocol return AnyPublisher<…, WableError>.
fetchFCMToken() and updateFCMToken(token:) break that pattern by returning a plain optional String and Void.
If key-chain (or any secure storage) access fails, there is currently no way to surface the error to the caller.

-func fetchFCMToken() -> String?
-func updateFCMToken(token: String)
+func fetchFCMToken() -> AnyPublisher<String?, WableError>
+func updateFCMToken(token: String) -> AnyPublisher<Void, WableError>

Aligning the signatures keeps error propagation uniform across the repository layer and avoids hidden failures.

📝 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
func fetchFCMToken() -> String?
func updateFCMToken(token: String)
func updateUserProfile(nickname: String, fcmToken: String?) -> AnyPublisher<Void, WableError>
func fetchFCMToken() -> AnyPublisher<String?, WableError>
func updateFCMToken(token: String) -> AnyPublisher<Void, WableError>
func updateUserProfile(nickname: String, fcmToken: String?) -> AnyPublisher<Void, WableError>

Comment on lines +69 to +96
loadTrigger
.withUnretained(self)
.sink { owner, _ in
owner.userBadgeUseCase.execute(number: 0)
.sink { completion in
if case .failure(let error) = completion {
WableLogger.log("뱃지 수정 중 오류 발생: \(error)", for: .error)
}
} receiveValue: { [weak self] _ in
guard let self = self else { return }

self.userInformationUseCase.fetchActiveUserID()
.sink { [weak self] id in
guard let self = self,
let id = id
else {
return
}

self.userInformationUseCase.updateUserSession(userID: id, notificationBadgeCount: 0)
.sink { _ in }
.store(in: cancelBag)
}
.store(in: cancelBag)
}
.store(in: cancelBag)
}
.store(in: cancelBag)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Nested sinks create multiple subscriptions and possible retain cycles – flatten the Combine chain

loadTrigger opens a sink that in turn opens two more sinks (userBadgeUseCase.executefetchActiveUserID).
Every trigger therefore allocates three independent subscriptions and stores them all in the same cancelBag, steadily growing the bag and retaining self.

A flatter pipeline avoids both issues and is easier to reason about:

-loadTrigger
-    .withUnretained(self)
-    .sink { owner, _ in
-        owner.userBadgeUseCase.execute(number: 0)
-            .sink { completion in
-                if case .failure(let error) = completion {
-                    WableLogger.log("뱃지 수정 중 오류 발생: \(error)", for: .error)
-                }
-            } receiveValue: { [weak self] _ in
-                guard let self = self else { return }
-                self.userInformationUseCase.fetchActiveUserID()
-                    .sink { [weak self] id in
-                        guard let self = self, let id = id else { return }
-                        self.userInformationUseCase.updateUserSession(userID: id, notificationBadgeCount: 0)
-                            .sink { _ in }
-                            .store(in: cancelBag)
-                    }
-                    .store(in: cancelBag)
-            }
-            .store(in: cancelBag)
-    }
-    .store(in: cancelBag)
+loadTrigger
+    .flatMap { [weak self] _ -> AnyPublisher<Int?, Never> in
+        guard let self = self else { return Empty().eraseToAnyPublisher() }
+        return self.userBadgeUseCase.execute(number: 0)
+            .catch { error -> AnyPublisher<Void, Never> in
+                WableLogger.log("뱃지 수정 중 오류 발생: \(error)", for: .error)
+                return Empty().eraseToAnyPublisher()
+            }
+            .flatMap { [weak self] _ -> AnyPublisher<Int?, Never> in
+                self?.userInformationUseCase.fetchActiveUserID() ?? Empty().eraseToAnyPublisher()
+            }
+            .eraseToAnyPublisher()
+    }
+    .compactMap { $0 }
+    .flatMap { [weak self] id -> AnyPublisher<Void, Never> in
+        self?.userInformationUseCase.updateUserSession(userID: id, notificationBadgeCount: 0)
+            ?? Empty().eraseToAnyPublisher()
+    }
+    .sink { _ in }
+    .store(in: cancelBag)

Benefits
• Only one subscription per trigger
• No manual nested store(in:) calls
self is captured weakly throughout, eliminating retain-cycle risk

📝 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
loadTrigger
.withUnretained(self)
.sink { owner, _ in
owner.userBadgeUseCase.execute(number: 0)
.sink { completion in
if case .failure(let error) = completion {
WableLogger.log("뱃지 수정 중 오류 발생: \(error)", for: .error)
}
} receiveValue: { [weak self] _ in
guard let self = self else { return }
self.userInformationUseCase.fetchActiveUserID()
.sink { [weak self] id in
guard let self = self,
let id = id
else {
return
}
self.userInformationUseCase.updateUserSession(userID: id, notificationBadgeCount: 0)
.sink { _ in }
.store(in: cancelBag)
}
.store(in: cancelBag)
}
.store(in: cancelBag)
}
.store(in: cancelBag)
loadTrigger
.flatMap { [weak self] _ -> AnyPublisher<Int?, Never> in
guard let self = self else { return Empty().eraseToAnyPublisher() }
return self.userBadgeUseCase.execute(number: 0)
.catch { error -> AnyPublisher<Void, Never> in
WableLogger.log("뱃지 수정 중 오류 발생: \(error)", for: .error)
return Empty().eraseToAnyPublisher()
}
.flatMap { [weak self] _ -> AnyPublisher<Int?, Never> in
self?.userInformationUseCase.fetchActiveUserID() ?? Empty().eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
.compactMap { $0 }
.flatMap { [weak self] id -> AnyPublisher<Void, Never> in
self?.userInformationUseCase.updateUserSession(userID: id, notificationBadgeCount: 0)
?? Empty().eraseToAnyPublisher()
}
.sink { _ in }
.store(in: cancelBag)

Comment on lines +72 to 78
sessions[userID] = updatedSession
try? userDefaults.setValue(sessions, for: Keys.userSessions)

if fetchActiveUserID() == nil {
updateActiveUserID(session.id)
}
}

func updateNotificationBadge(count: Int, forUserID userID: Int) {
var sessions = fetchAllUserSessions()

if let session = sessions[userID] {
let updatedSession = UserSession(
id: session.id,
nickname: session.nickname,
profileURL: session.profileURL,
isPushAlarmAllowed: session.isPushAlarmAllowed,
isAdmin: session.isAdmin,
isAutoLoginEnabled: session.isAutoLoginEnabled,
notificationBadgeCount: count
)
sessions[userID] = updatedSession
try? userDefaults.setValue(sessions, for: Keys.userSessions)
updateActiveUserID(userID)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Log or surface persistence errors instead of silencing them

try? userDefaults.setValue(…) swallows any exception, making it impossible to know when persisting the session failed (e.g. JSON encoding issue, disk full, etc.).

-        try? userDefaults.setValue(sessions, for: Keys.userSessions)
+        do {
+            try userDefaults.setValue(sessions, for: Keys.userSessions)
+        } catch {
+            WableLogger.log("⚠️ Failed to persist user session: \(error)", for: .error)
+        }

Propagating or at least logging the error will save debugging time later.

📝 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
sessions[userID] = updatedSession
try? userDefaults.setValue(sessions, for: Keys.userSessions)
if fetchActiveUserID() == nil {
updateActiveUserID(session.id)
}
}
func updateNotificationBadge(count: Int, forUserID userID: Int) {
var sessions = fetchAllUserSessions()
if let session = sessions[userID] {
let updatedSession = UserSession(
id: session.id,
nickname: session.nickname,
profileURL: session.profileURL,
isPushAlarmAllowed: session.isPushAlarmAllowed,
isAdmin: session.isAdmin,
isAutoLoginEnabled: session.isAutoLoginEnabled,
notificationBadgeCount: count
)
sessions[userID] = updatedSession
try? userDefaults.setValue(sessions, for: Keys.userSessions)
updateActiveUserID(userID)
}
}
sessions[userID] = updatedSession
do {
try userDefaults.setValue(sessions, for: Keys.userSessions)
} catch {
WableLogger.log("⚠️ Failed to persist user session: \(error)", for: .error)
}
if fetchActiveUserID() == nil {
updateActiveUserID(userID)
}
}

Comment on lines +15 to +26
// MARK: Property
// TODO: Repository를 UseCase로 변경

let userBadgeUseCase = UpdateUserBadgeUseCase(repository: AccountRepositoryImpl())
let userSessionRepository = UserSessionRepositoryImpl(userDefaults: UserDefaultsStorage(jsonEncoder: JSONEncoder(), jsonDecoder: JSONDecoder()))
let profileRepository = ProfileRepositoryImpl()
let contentRepository = ContentRepositoryImpl()
let commentRepository = CommentRepositoryImpl()
let contentLikedRepository = ContentLikedRepositoryImpl()
let commentLikedRepository = CommentLikedRepositoryImpl()
let reportRepository = ReportRepositoryImpl()
let cancelBag = CancelBag()
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace TODO with a tracked task or implement DI now

Line 16 still contains a TODO about replacing repositories with use-cases.
These comments easily get lost; either

  1. Create an issue in the tracker and reference it here, or
  2. Finish the refactor before merging.

Leaving it as-is increases technical debt.

🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 16-16: TODOs should be resolved (Repository를 UseCase로 변경)

(todo)

Comment on lines +109 to +117
if let tabBarController = rootViewController as? TabBarController {
guard let viewController = tabBarController.selectedViewController as? UINavigationController else { return }

tabBarController.selectedIndex = 0
viewController.pushViewController(detailViewController, animated: true)
} else
if let viewController = rootViewController as? UINavigationController {
viewController.pushViewController(detailViewController, animated: true)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

View controller navigation logic handles different root view structures.

The implementation correctly handles both TabBarController and UINavigationController root view controllers for navigation, but has some potential issues.

The nested if-else structure could be simplified, and there's a potential issue if the root view controller is neither a TabBarController nor a UINavigationController.

- if let tabBarController = rootViewController as? TabBarController {
-     guard let viewController = tabBarController.selectedViewController as? UINavigationController else { return }
-     
-     tabBarController.selectedIndex = 0
-     viewController.pushViewController(detailViewController, animated: true)
- } else
- if let viewController = rootViewController as? UINavigationController {
-     viewController.pushViewController(detailViewController, animated: true)
- }
+ if let tabBarController = rootViewController as? TabBarController {
+     tabBarController.selectedIndex = 0
+     if let navigationController = tabBarController.selectedViewController as? UINavigationController {
+         navigationController.pushViewController(detailViewController, animated: true)
+     } else {
+         WableLogger.log("Unable to navigate: selected view controller is not a navigation controller", for: .error)
+     }
+ } else if let navigationController = rootViewController as? UINavigationController {
+     navigationController.pushViewController(detailViewController, animated: true)
+ } else {
+     WableLogger.log("Unable to navigate: root view controller is neither a tab bar nor a navigation controller", for: .error)
+ }

Comment on lines +60 to +80
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
guard let aps = response.notification.request.content.userInfo["aps"] as? [String: Any],
let badge = aps["badge"] as? Int
else {
return
}

userBadgeUseCase.execute(number: badge - 1)
.receive(on: DispatchQueue.main)
.sink { completion in
if case .failure(let error) = completion {
WableLogger.log("뱃지 수정 중 오류 발생: \(error)", for: .error)
}
} receiveValue: { [weak self] _ in
guard let self = self else { return }

self.userSessionRepository.updateUserSession(userID: activeID, notificationBadgeCount: badge - 1)
WableLogger.log("뱃지 수정 완료: \(badge - 1)개", for: .debug)
}
.store(in: cancelBag)

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Notification handling implemented with badge count update.

The implementation correctly extracts the badge count from the notification and updates it. It uses Combine for asynchronous operations and properly logs success and error cases.

Consider extracting the badge update logic to a separate method for better readability and reusability. Also, you should consider handling potential race conditions if multiple notifications are received in quick succession.

+ private func updateBadgeCount(_ badge: Int) {
+     userBadgeUseCase.execute(number: badge - 1)
+         .receive(on: DispatchQueue.main)
+         .sink { completion in
+             if case .failure(let error) = completion {
+                 WableLogger.log("뱃지 수정 중 오류 발생: \(error)", for: .error)
+             }
+         } receiveValue: { [weak self] _ in
+             guard let self = self else { return }
+             
+             self.userSessionRepository.updateUserSession(userID: activeID, notificationBadgeCount: badge - 1)
+             WableLogger.log("뱃지 수정 완료: \(badge - 1)개", for: .debug)
+         }
+         .store(in: cancelBag)
+ }

Then call this method instead:

- userBadgeUseCase.execute(number: badge - 1)
-     .receive(on: DispatchQueue.main)
-     .sink { completion in
-         if case .failure(let error) = completion {
-             WableLogger.log("뱃지 수정 중 오류 발생: \(error)", for: .error)
-         }
-     } receiveValue: { [weak self] _ in
-         guard let self = self else { return }
-         
-         self.userSessionRepository.updateUserSession(userID: activeID, notificationBadgeCount: badge - 1)
-         WableLogger.log("뱃지 수정 완료: \(badge - 1)개", for: .debug)
-     }
-     .store(in: cancelBag)
+ updateBadgeCount(badge)
📝 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
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
guard let aps = response.notification.request.content.userInfo["aps"] as? [String: Any],
let badge = aps["badge"] as? Int
else {
return
}
userBadgeUseCase.execute(number: badge - 1)
.receive(on: DispatchQueue.main)
.sink { completion in
if case .failure(let error) = completion {
WableLogger.log("뱃지 수정 중 오류 발생: \(error)", for: .error)
}
} receiveValue: { [weak self] _ in
guard let self = self else { return }
self.userSessionRepository.updateUserSession(userID: activeID, notificationBadgeCount: badge - 1)
WableLogger.log("뱃지 수정 완료: \(badge - 1)", for: .debug)
}
.store(in: cancelBag)
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
guard let aps = response.notification.request.content.userInfo["aps"] as? [String: Any],
let badge = aps["badge"] as? Int
else {
return
}
updateBadgeCount(badge)
}
private func updateBadgeCount(_ badge: Int) {
userBadgeUseCase.execute(number: badge - 1)
.receive(on: DispatchQueue.main)
.sink { completion in
if case .failure(let error) = completion {
WableLogger.log("뱃지 수정 중 오류 발생: \(error)", for: .error)
}
} receiveValue: { [weak self] _ in
guard let self = self else { return }
self.userSessionRepository.updateUserSession(userID: activeID, notificationBadgeCount: badge - 1)
WableLogger.log("뱃지 수정 완료: \(badge - 1)", for: .debug)
}
.store(in: cancelBag)
}

@youz2me youz2me merged commit c3b7df8 into develop May 14, 2025
1 check passed
@youz2me youz2me deleted the feat/#177-push-notification branch May 14, 2025 04:07
youz2me added a commit that referenced this pull request Oct 26, 2025
[Feat] FCM을 이용한 푸시 알림 기능 구현
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Feat] 알림 기능 구현

2 participants