Skip to content

Conversation

@youz2me
Copy link
Member

@youz2me youz2me commented May 21, 2025

👻 PULL REQUEST

📄 작업 내용

"문제의 원인은 예상치 못한 곳에 있을 수 있다"

  • FCM 기능을 재구현했어요.
  • 로그인, 회원가입 단계에서 서버로 유저 프로필을 전송하는 로직을 수정했어요.
구현 내용 IPhone 16
PNG

💻 주요 코드 설명

푸시 알림 설정 확인 코드 및 서버 전송 코드 추가

  • 알림이 안오는 이슈가 FCM 구현 문제 때문인 줄 알았는데 로컬에만 isPushAlarmAllowed 상태를 저장하고 서버에 전송하지 않아서(;;) 일어난 이슈였습니다.
  • LoginViewModel에 아래와 같이 코드를 추가했습니다. (개선 예정...)
Task {
                        let isAuthorized = await UNUserNotificationCenter.current().notificationSettings().authorizationStatus == .authorized
                        
                        WableLogger.log("\(isAuthorized)", for: .debug)
                        
                        self.updateUserSessionUseCase.updateUserSession(
                            userID: account.user.id,
                            nickname: account.user.nickname,
                            profileURL: account.user.profileURL,
                            isPushAlarmAllowed: isAuthorized,
                            isAdmin: account.isAdmin,
                            isAutoLoginEnabled: true
                        )
                        .sink(receiveCompletion: { _ in
                        }, receiveValue: { _ in
                            WableLogger.log("로컬에 세션 저장 완료", for: .debug)
                        })
                        .store(in: cancelBag)
                        
                        self.userProfileUseCase.execute(userID: account.user.id)
                            .sink { _ in
                            } receiveValue: { profile in
                                self.userProfileUseCase.execute(profile: profile, isPushAlarmAllowed: isAuthorized)
                                    .sink { completion in
                                        switch completion {
                                        case .failure(let error):
                                            WableLogger.log("서버로 프로필 업데이트 중 오류 발생: \(error)", for: .error)
                                        default:
                                            WableLogger.log("로그인 및 서버로 프로필 업데이트 완료", for: .debug)
                                        }
                                    } receiveValue: { _ in
                                        
                                    }
                                    .store(in: cancelBag)
                            }
                            .store(in: cancelBag)
                    }

📚 참고자료

[iOS/macOS] FCM을 사용한 Remote Push Notification 구현 방법과 문제 해결 기록
Remote Push Notification(with FCM) 예제 프로젝트

👀 기타 더 이야기해볼 점

  • 머리가 울려서 이것만 올리고 진짜 자러갑니다
  • 도와주셔서 감사합니다 아빠짱아빠최고 평생 김진웅의 넘버완팬이 되겟습니다.
  • AppDelegate 코드에 주석이 좀 있는데 나중에 주석 참고해서 분리 예정이라 그때까지만 양해 부탁드립니다 AppDelegate는 공사중!...

🔗 연결된 이슈

Summary by CodeRabbit

  • New Features

    • Enhanced push notification support with comprehensive Firebase Cloud Messaging integration, including improved token management, user permission handling, and deep linking from notifications.
    • Badge count on the app icon is now automatically cleared when notifications are checked.
    • Interactive pop gesture is enabled across various onboarding and home screens for improved navigation.
    • Tapping your own profile image in comments or content now navigates directly to your profile tab.
  • Improvements

    • More accurate update of push notification permission status in user session and profile after login.
    • Hides the bottom tab bar when entering the alarm settings screen for a cleaner view.
    • Improved handling of banned users: all their content and comments are visually updated and hidden.
    • Initial UI states and label visibility improved in onboarding profile registration.
  • Bug Fixes

    • URL validation in content creation is now more flexible, accepting a wider range of valid links.
    • Fixed data type for Firebase configuration in app settings for better compatibility.
  • Chores

    • Internal refactoring and dependency injection improvements for better maintainability.

@youz2me youz2me requested a review from JinUng41 May 21, 2025 12:51
@youz2me youz2me self-assigned this May 21, 2025
@youz2me youz2me added ✨ feat 기능 또는 객체 구현 🛠️ fix 기능적 버그나 오류 해결 시 사용 ♻️ refactor 기존 코드를 리팩토링하거나 수정하는 등 사용 (생산적인 경우) 🦉 유진 🛌🛌🛌🛌🛌🛌🛌🛌🛌🛌 labels May 21, 2025
@coderabbitai
Copy link

coderabbitai bot commented May 21, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

The changes remove the previous Firebase Cloud Messaging (FCM) integration and reimplement it directly within AppDelegate.swift. The update consolidates FCM setup, token management, notification handling, and deep linking logic into AppDelegate, eliminating the separate extension file. Additional dependency injections and notification handling improvements are made throughout the codebase.

Changes

File(s) Change Summary
Wable-iOS.xcodeproj/project.pbxproj Removed all references to AppDelegate+Firebase.swift from the Xcode project.
Wable-iOS/App/AppDelegate+Firebase.swift Deleted the file; previously contained FCM and notification handling as an extension to AppDelegate.
Wable-iOS/App/AppDelegate.swift Added direct FCM setup, notification handling, token management, and deep linking logic; implements relevant delegate methods.
Wable-iOS/App/SceneDelegate.swift
Wable-iOS/Presentation/Profile/My/View/MyProfileViewController.swift
Wable-iOS/Presentation/Profile/Withdrawal/Guide/View/WithdrawalGuideViewController.swift
Added userProfileUseCase dependency injection to LoginViewModel or LoginViewController initializations.
Wable-iOS/Presentation/Login/LoginViewModel.swift Added UserProfileUseCase dependency; updates push alarm status based on notification authorization; synchronizes with user profile.
Wable-iOS/Presentation/Notification/Activity/ViewModel/ActivityNotificationViewModel.swift Imports UIKit; resets app icon badge number to zero after clearing internal badge count.
Wable-iOS/Presentation/Profile/AlarmSetting/View/AlarmSettingViewController.swift Sets hidesBottomBarWhenPushed = true in the view controller initializer.
Wable-iOS/Presentation/Profile/AlarmSetting/ViewModel/AlarmSettingViewModel.swift Added a comment placeholder for future server update logic regarding alarm authorization.
Wable-iOS/Presentation/Viewit/Create/ViewModel/CreateViewitViewModel.swift Removed restrictive regular expression URL validation; now relies only on NSDataDetector and absence of Korean characters.
Wable-iOS/Resource/Info.plist Changed FirebaseAppDelegateProxyEnabled value from string "NO" to boolean <false/>.
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift Enabled interactive pop gesture; updated profile image tap handler to conditionally switch tab or push profile view controller.
Wable-iOS/Presentation/Home/View/HomeViewController.swift Modified profile image tap handler to switch tab if tapped user is active user; otherwise push profile view controller.
Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift Added method to recursively update banned comments and update content status when banning user.
Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift Added reactive pipeline to update all contents authored by banned user upon ban event.
Wable-iOS/Presentation/Onboarding/View/ProfileRegisterView.swift Changed initial attributed text and visibility of title and description labels to improve UI state management.
Wable-iOS/Presentation/Onboarding/ViewController/AgreementViewController.swift Enabled interactive pop gesture recognizer in navigation controller.
Wable-iOS/Presentation/Onboarding/ViewController/LCKTeamViewController.swift Enabled interactive pop gesture recognizer in navigation controller.
Wable-iOS/Presentation/Onboarding/ViewController/LCKYearViewController.swift Enabled interactive pop gesture recognizer in navigation controller.
Wable-iOS/Presentation/Onboarding/ViewController/ProfileRegisterViewController.swift Enabled interactive pop gesture recognizer in navigation controller.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UIApplication
    participant AppDelegate
    participant Firebase
    participant UNUserNotificationCenter
    participant Messaging
    participant ProfileRepository
    participant ContentDetailViewController

    User->>UIApplication: Launch app
    UIApplication->>AppDelegate: didFinishLaunchingWithOptions
    AppDelegate->>Firebase: Configure
    AppDelegate->>Messaging: Set delegate
    AppDelegate->>UNUserNotificationCenter: Set delegate, request authorization
    UNUserNotificationCenter-->>AppDelegate: Return authorization status
    AppDelegate->>UIApplication: Register for remote notifications

    UIApplication->>AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken
    AppDelegate->>Messaging: Set APNs token

    Messaging-->>AppDelegate: didReceiveRegistrationToken
    AppDelegate->>ProfileRepository: Save FCM token

    UNUserNotificationCenter-->>AppDelegate: didReceive notification response
    AppDelegate->>AppDelegate: Parse badge count, content ID
    AppDelegate->>ProfileRepository: Update badge count
    AppDelegate->>ContentDetailViewController: Navigate to content detail (if ID present)
Loading

Assessment against linked issues

Objective Addressed Explanation
Remove previously implemented FCM code (#194)
Reimplement FCM integration, including setup and notification handling (#194)

Possibly related PRs

Poem

A hop, a skip, a Firebase leap—
Old code gone, new FCM we keep!
Notifications ring, tokens align,
Deep links jump in perfect time.
With badges reset and profiles in sync,
This bunny’s code is fresher than you think!
🐇✨

Note

⚡️ AI Code Reviews for VS Code, Cursor, Windsurf

CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
Learn more here.


📜 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 71db1c6 and 7e98b19.

📒 Files selected for processing (1)
  • Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (2 hunks)
✨ 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: 3

🧹 Nitpick comments (3)
Wable-iOS/Presentation/Profile/AlarmSetting/ViewModel/AlarmSettingViewModel.swift (1)

19-20: Placeholder added for server sync - implementation needed

A TODO comment has been added to mark where the alarm permission status should be sent to the server. This aligns with the PR's objective to synchronize notification preferences with the server.

Would you like help implementing this server sync functionality to complete the FCM push notification feature?

🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 20-20: TODOs should be resolved (서버로 알람 수신 여부 보내기)

(todo)

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

17-21: Ambiguous property/type pairing

private let updateUserSessionUseCase: FetchUserInformationUseCase couples an update-named variable to a fetch use-case type.
This mirrors the confusion flagged in SceneDelegate. If the injected use-case truly updates a session, its type name should reflect that; otherwise, rename the stored property.

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

48-58: Thread-safety when touching userSessionRepository inside the permission callback

requestAuthorization’s completion handler may be invoked on an arbitrary background queue.
userSessionRepository.updateUserSession(...) appears to mutate persistent storage; if the repository isn’t internally thread-safe, race conditions can occur.

Wrap the call on the main queue (or the repository’s dedicated queue):

UNUserNotificationCenter.current().requestAuthorization(
    options: [.alert, .badge, .sound]
) { [weak self] granted, _ in
    guard let self = self,
          let session = self.userSessionRepository.fetchActiveUserSession() else { return }

    DispatchQueue.main.async {
        self.userSessionRepository.updateUserSession(
            userID: session.id,
            isPushAlarmAllowed: granted
        )
    }
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 907bedb and 0360e1b.

📒 Files selected for processing (12)
  • Wable-iOS.xcodeproj/project.pbxproj (0 hunks)
  • Wable-iOS/App/AppDelegate+Firebase.swift (0 hunks)
  • Wable-iOS/App/AppDelegate.swift (3 hunks)
  • Wable-iOS/App/SceneDelegate.swift (1 hunks)
  • Wable-iOS/Presentation/Login/LoginViewModel.swift (4 hunks)
  • Wable-iOS/Presentation/Notification/Activity/ViewModel/ActivityNotificationViewModel.swift (2 hunks)
  • Wable-iOS/Presentation/Profile/AlarmSetting/View/AlarmSettingViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/AlarmSetting/ViewModel/AlarmSettingViewModel.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/My/View/MyProfileViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/Withdrawal/Guide/View/WithdrawalGuideViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Viewit/Create/ViewModel/CreateViewitViewModel.swift (0 hunks)
  • Wable-iOS/Resource/Info.plist (1 hunks)
💤 Files with no reviewable changes (3)
  • Wable-iOS/Presentation/Viewit/Create/ViewModel/CreateViewitViewModel.swift
  • Wable-iOS.xcodeproj/project.pbxproj
  • Wable-iOS/App/AppDelegate+Firebase.swift
🧰 Additional context used
🪛 SwiftLint (0.57.0)
Wable-iOS/Presentation/Profile/AlarmSetting/ViewModel/AlarmSettingViewModel.swift

[Warning] 20-20: TODOs should be resolved (서버로 알람 수신 여부 보내기)

(todo)

🔇 Additional comments (6)
Wable-iOS/Resource/Info.plist (1)

20-21: Use a Boolean value to disable the Firebase App Delegate proxy
Switching from a string to <false/> ensures that the FirebaseAppDelegateProxyEnabled key is correctly interpreted as a boolean in Info.plist, which is required for manual FCM delegate handling in AppDelegate.swift.

Wable-iOS/Presentation/Profile/My/View/MyProfileViewController.swift (1)

459-460: Dependency injection added for user profile management

Added userProfileUseCase to the LoginViewModel initialization, which enables synchronizing the user's push notification preferences with the server during login.

Wable-iOS/Presentation/Profile/Withdrawal/Guide/View/WithdrawalGuideViewController.swift (1)

117-118: Dependency injection added for user profile management

Added userProfileUseCase to the LoginViewModel initialization, which is consistent with similar changes in other view controllers. This enables the synchronization of user profile data, including push notification preferences, with the server during login.

Wable-iOS/Presentation/Notification/Activity/ViewModel/ActivityNotificationViewModel.swift (2)

9-9: Import changed from Foundation to UIKit

Changed the import to UIKit which is necessary to access UIApplication for the badge number update.


91-92: App icon badge reset added

Added code to reset the application icon badge number to zero when notification badges are cleared. This ensures that the visual indicator on the app icon is synchronized with the internal badge count.

Wable-iOS/Presentation/Profile/AlarmSetting/View/AlarmSettingViewController.swift (1)

44-46: 👍 Good UX-centric tweak

Setting hidesBottomBarWhenPushed = true directly in the initializer guarantees consistent behaviour whenever the screen is shown. Looks perfect.

Comment on lines +94 to 96
updateUserSessionUseCase: FetchUserInformationUseCase(repository: userSessionRepository),
userProfileUseCase: UserProfileUseCase(repository: ProfileRepositoryImpl())
)
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

⚠️ Inconsistent naming between parameter label and injected type

updateUserSessionUseCase is being initialised with FetchUserInformationUseCase(...).
This will compile if the constructor actually expects FetchUserInformationUseCase, but the label implies a different responsibility (update vs fetch). The mismatch is confusing for future readers and makes the dependency graph harder to reason about.

Consider either:

- updateUserSessionUseCase: FetchUserInformationUseCase(repository: userSessionRepository),
+ fetchUserInformationUseCase: FetchUserInformationUseCase(repository: userSessionRepository),

or injecting the real UpdateUserSessionUseCase if that is what the view-model needs.


🏁 Script executed:

#!/bin/bash
set -e
# Search for any definition or usage of UpdateUserSessionUseCase
rg "UpdateUserSessionUseCase" -n .

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -e
# Locate SceneDelegate.swift
scene_file=$(fd SceneDelegate.swift -t f)
echo "SceneDelegate file: $scene_file"
# Show lines 1-200 to inspect initializer context
sed -n '1,200p' "$scene_file"

Length of output: 5767


🏁 Script executed:

#!/bin/bash
set -e
# Locate LoginViewModel.swift
login_vm_file=$(fd LoginViewModel.swift -t f)
echo "LoginViewModel file: $login_vm_file"
# Show the initializer signature and surrounding context
sed -n '1,200p' "$login_vm_file"

Length of output: 5948


🏁 Script executed:

#!/bin/bash
set -e
# Find definition of FetchUserInformationUseCase
rg "struct FetchUserInformationUseCase" -n .
rg "class FetchUserInformationUseCase" -n .

# Show its file contents for context
file=$(rg "FetchUserInformationUseCase" -l . | head -n 1)
echo "File: $file"
sed -n '1,200p' "$file"

# Search for updateUserSession method within this file
rg "updateUserSession" -n "$file"

Length of output: 55


🏁 Script executed:

#!/bin/bash
set -e
# Case-insensitive search for FetchUserInformationUseCase
rg -i "FetchUserInformationUseCase" -n .

Length of output: 5419


Rename updateUserSessionUseCase to match the injected FetchUserInformationUseCase

The parameter label (updateUserSessionUseCase) currently does not match the type being injected (FetchUserInformationUseCase), which is confusing. Since there is no UpdateUserSessionUseCase in the codebase, you should rename the label (and corresponding property/parameter in LoginViewModel) to fetchUserInformationUseCase.

• SceneDelegate.swift (around lines 94–96):

- updateUserSessionUseCase: FetchUserInformationUseCase(repository: userSessionRepository),
+ fetchUserInformationUseCase: FetchUserInformationUseCase(repository: userSessionRepository),

• Presentation/Login/LoginViewModel.swift:

- private let updateUserSessionUseCase: FetchUserInformationUseCase
+ private let fetchUserInformationUseCase: FetchUserInformationUseCase

 init(
     updateFCMTokenUseCase: UpdateFCMTokenUseCase,
     fetchUserAuthUseCase: FetchUserAuthUseCase,
-    updateUserSessionUseCase: FetchUserInformationUseCase,
+    fetchUserInformationUseCase: FetchUserInformationUseCase,
     userProfileUseCase: UserProfileUseCase
 ) { … }

 // Inside transform(...)
-    self.updateUserSessionUseCase.updateUserSession(…)
+    self.fetchUserInformationUseCase.updateUserSession(…)

Make sure to update all related call sites (AppDelegate, other ViewControllers/ViewModels) for consistency.

📝 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
updateUserSessionUseCase: FetchUserInformationUseCase(repository: userSessionRepository),
userProfileUseCase: UserProfileUseCase(repository: ProfileRepositoryImpl())
)
fetchUserInformationUseCase: FetchUserInformationUseCase(repository: userSessionRepository),
userProfileUseCase: UserProfileUseCase(repository: ProfileRepositoryImpl())
🤖 Prompt for AI Agents
In Wable-iOS/App/SceneDelegate.swift around lines 94 to 96, rename the parameter
label and any related property named updateUserSessionUseCase to
fetchUserInformationUseCase to match the injected type
FetchUserInformationUseCase. Also update the corresponding parameter and
property names in Presentation/Login/LoginViewModel.swift and all related call
sites such as AppDelegate and other ViewControllers/ViewModels to maintain
consistency and avoid confusion.

Comment on lines +89 to 126
Task {
let isAuthorized = await UNUserNotificationCenter.current().notificationSettings().authorizationStatus == .authorized

WableLogger.log("\(isAuthorized)", for: .debug)

self.updateUserSessionUseCase.updateUserSession(
userID: account.user.id,
nickname: account.user.nickname,
profileURL: account.user.profileURL,
isPushAlarmAllowed: isAuthorized,
isAdmin: account.isAdmin,
isAutoLoginEnabled: true
)
.sink(receiveCompletion: { _ in
}, receiveValue: { _ in
WableLogger.log("로컬에 세션 저장 완료", for: .debug)
})
.store(in: cancelBag)

self.userProfileUseCase.execute(userID: account.user.id)
.sink { _ in
} receiveValue: { profile in
self.userProfileUseCase.execute(profile: profile, isPushAlarmAllowed: isAuthorized)
.sink { completion in
switch completion {
case .failure(let error):
WableLogger.log("서버로 프로필 업데이트 중 오류 발생: \(error)", for: .error)
default:
WableLogger.log("로그인 및 서버로 프로필 업데이트 완료", for: .debug)
}
} receiveValue: { _ in

}
.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

Async Task + Combine may leak work onto an arbitrary actor

Inside the Combine sink you spin up a detached Task { … } that:

  1. Calls the async UNUserNotificationCenter API.
  2. Performs several Combine pipelines (updateUserSessionUseCase…, userProfileUseCase…) without awaiting their completion, then
  3. Immediately lets the sink of the outer publisher finish.

Potential issues:

• The Task captures self strongly; if the view-model is deallocated the Task keeps it alive.
• Any publisher callbacks will run on the thread chosen by the publisher (often a background queue) while you are also inside an actor-hop created by Task {}. Side-effects to shared state (e.g. userSessionRepository) risk race conditions.

Suggestion:

- Task {
-     let settings = await UNUserNotificationCenter.current().notificationSettings()
-     let isAuthorized = settings.authorizationStatus == .authorized
-
-     updateLocalSessionAndProfile(isAuthorized: isAuthorized, account: account)
- }
+ UNUserNotificationCenter.current()
+     .notificationSettings()                     // async
+     .publisher                                   // convert to Combine
+     .map { $0.authorizationStatus == .authorized}
+     .flatMap { [unowned self] isAuth in
+         self.updateLocalSessionAndProfilePublisher(
+             isAuthorized: isAuth, account: account)
+     }
+     .sink(receiveCompletion: { … })
+     .store(in: cancelBag)

This keeps everything in the Combine world, avoids the additional Task, and guarantees lifecycle-managed cancellation.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In Wable-iOS/Presentation/Login/LoginViewModel.swift between lines 89 and 126,
the current code uses a detached Task to call async APIs and start Combine
pipelines without awaiting them, which can cause strong self-captures and race
conditions due to actor hops. To fix this, remove the detached Task and instead
integrate the async call to UNUserNotificationCenter into the Combine chain
using a Future or Deferred publisher, then chain the subsequent Combine
publishers sequentially, ensuring all asynchronous work is managed within
Combine's lifecycle and cancellation system without spawning detached Tasks.

Comment on lines 8 to 13
import UIKit

import FirebaseCore
import FirebaseMessaging
import KakaoSDKCommon

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing mandatory imports cause compilation failure

UNUserNotificationCenter (and its delegate) and Combine are both referenced later in the file, but the headers are not imported:

import UserNotifications   // for UNUserNotificationCenter & delegate
import Combine             // for CancelBag, AnyPublisher, sink, …

Without these, the file will not compile.

🤖 Prompt for AI Agents
In Wable-iOS/App/AppDelegate.swift around lines 8 to 13, the imports for
UserNotifications and Combine are missing, causing compilation errors due to
undefined references to UNUserNotificationCenter and Combine types. Add the
lines 'import UserNotifications' and 'import Combine' alongside the existing
imports to resolve these missing dependencies and enable successful compilation.

@ah-o-ng12
Copy link

20250511_171537

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: 1

🧹 Nitpick comments (1)
Wable-iOS/Presentation/Onboarding/View/ProfileRegisterView.swift (1)

26-26: Consider a cleaner alternative to the single space placeholder.

Using a single space as placeholder text for titleLabel is a workaround that could cause unexpected layout calculations or behavior. Consider these cleaner alternatives:

-        $0.attributedText = " ".pretendardString(with: .head0)
+        $0.text = nil

Or if you need to maintain the styling:

-        $0.attributedText = " ".pretendardString(with: .head0)
+        $0.attributedText = "".pretendardString(with: .head0)
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0360e1b and bef5743.

📒 Files selected for processing (12)
  • Wable-iOS.xcodeproj/project.pbxproj (0 hunks)
  • Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (2 hunks)
  • Wable-iOS/Presentation/Home/View/HomeViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (7 hunks)
  • Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (1 hunks)
  • Wable-iOS/Presentation/Onboarding/View/ProfileRegisterView.swift (2 hunks)
  • Wable-iOS/Presentation/Onboarding/ViewController/AgreementViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Onboarding/ViewController/LCKTeamViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Onboarding/ViewController/LCKYearViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Onboarding/ViewController/ProfileRegisterViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/AlarmSetting/View/AlarmSettingViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/My/View/MyProfileViewController.swift (1 hunks)
💤 Files with no reviewable changes (1)
  • Wable-iOS.xcodeproj/project.pbxproj
✅ Files skipped from review due to trivial changes (1)
  • Wable-iOS/Presentation/Onboarding/ViewController/AgreementViewController.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Wable-iOS/Presentation/Profile/AlarmSetting/View/AlarmSettingViewController.swift
  • Wable-iOS/Presentation/Profile/My/View/MyProfileViewController.swift
🧰 Additional context used
🧬 Code Graph Analysis (3)
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (1)
Wable-iOS/Presentation/TabBar/TabBarController.swift (1)
  • tabBarController (189-205)
Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (1)
Wable-iOS/Domain/Entity/Opacity.swift (1)
  • reduced (72-78)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (1)
Wable-iOS/Presentation/TabBar/TabBarController.swift (1)
  • tabBarController (189-205)
🪛 SwiftLint (0.57.0)
Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift

[Warning] 321-321: where clauses are preferred over a single if inside a for

(for_where)

🔇 Additional comments (9)
Wable-iOS/Presentation/Onboarding/ViewController/ProfileRegisterViewController.swift (1)

58-59: LGTM! Good UX improvement.

Enabling the interactive pop gesture recognizer allows users to use the standard iOS swipe-to-go-back gesture, which improves the navigation experience. The placement in setupView() is appropriate and consistent with similar changes in other onboarding view controllers.

Wable-iOS/Presentation/Onboarding/ViewController/LCKYearViewController.swift (1)

46-47: LGTM! Good UX improvement for onboarding flow.

Enabling the interactive pop gesture recognizer allows users to swipe back to previous screens, which improves the navigation experience in the onboarding flow. The placement in setupView() is appropriate and the optional chaining ensures safety.

Wable-iOS/Presentation/Onboarding/View/ProfileRegisterView.swift (1)

34-34: Good approach for controlling UI state during setup.

The pattern of initially hiding the descriptionLabel and then making it visible during configuration is a solid approach for controlling the UI presentation flow. This prevents flash of content and allows for better control over when elements appear.

Also applies to: 183-184

Wable-iOS/Presentation/Onboarding/ViewController/LCKTeamViewController.swift (1)

50-51:

Details

❓ Verification inconclusive

LGTM! Consider gesture interaction and onboarding UX.

The implementation correctly enables the interactive pop gesture recognizer with proper optional chaining for safety. This change aligns with the pattern applied across other onboarding view controllers.

However, consider these aspects:

  • Gesture conflicts: Since this view contains a collection view with horizontal scrolling potential, ensure the swipe-to-go-back gesture doesn't interfere with collection view interactions
  • Onboarding UX: In an onboarding flow, users might accidentally swipe back and lose their team selection progress. Consider if this aligns with the intended user experience

You may want to test the gesture interactions to ensure smooth UX:


🏁 Script executed:

#!/bin/bash
# Search for other interactive pop gesture configurations to understand the pattern
rg -A 3 -B 3 "interactivePopGestureRecognizer" --type swift

Length of output: 8619


Enable Interactive Pop Gesture—Consistent but Verify UX

This change aligns with your existing pattern—11 other view controllers (e.g., AgreementViewController, ProfileRegisterViewController, LCKYearViewController, HomeDetailViewController, etc.) already enable interactivePopGestureRecognizer in their setup methods.

Before merging, please verify:

  • Gesture conflict
    • Ensure the collection view’s horizontal scrolling isn’t disrupted by the edge-swipe back gesture.
  • Onboarding UX
    • Confirm users won’t accidentally navigate back and lose progress (consider a confirmation step or temporarily disabling the gesture during critical selections).

Test on-device to validate smooth navigation and onboarding flow.

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

142-142: LGTM! Good UX improvement.

Enabling the interactive pop gesture recognizer enhances the user experience by allowing swipe-back navigation, which is a standard iOS pattern.


430-449: Well-implemented profile navigation logic.

The conditional navigation logic is well-structured:

  • Correctly switches to the profile tab (index 4) when the user taps their own profile
  • Properly initializes OtherProfileViewController with dependency injection for other users
  • Matches the similar implementation in HomeViewController.swift for consistency

The dependency injection pattern using FetchUserProfileUseCaseImpl() and CheckUserRoleUseCaseImpl is properly implemented.

Wable-iOS/Presentation/Home/View/HomeViewController.swift (1)

269-292: Excellent consistency with the detail view implementation.

The profile navigation logic is well-implemented and maintains consistency with the similar implementation in HomeDetailViewController.swift. The conditional navigation pattern properly:

  • Switches to the profile tab (index 4) for the active user
  • Pushes OtherProfileViewController for other users
  • Uses proper dependency injection with UserDefaultsStorage initialization

This consistency improves maintainability and user experience across the app.

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

355-381: Well-structured banned user handling integration.

The updated banned user handling properly integrates the new updateBannedComments helper method and maintains consistency with the existing ghost user handling pattern. The logic correctly:

  • Updates all comments by the banned user
  • Updates the content if the banned user is the author
  • Sets the appropriate .blind status and reduces opacity

The implementation aligns well with the broader banning functionality across the app.


538-574: Excellent recursive comment update implementation.

The updateBannedComments method is well-implemented with proper recursive handling:

  • Follows the same pattern as updateGhostComments for consistency
  • Correctly handles nested child comments through recursion
  • Properly sets .blind status and reduces opacity using the reduced() method
  • Maintains immutability by creating new instances

The recursive approach ensures all comments by the banned user are updated regardless of nesting level.

Comment on lines 308 to 351
input.didBannedTappedItem
.withUnretained(self)
.flatMap { owner, content -> AnyPublisher<Int, Never> in
return owner.createBannedUseCase.execute(memberID: content.0, triggerType: .content, triggerID: content.1)
.map { _ in content.0 }
.asDriver(onErrorJustReturn: -1)
}
.sink(receiveValue: { userID in
guard userID != -1 else { return }

var updatedContents = contentsSubject.value

for i in 0..<updatedContents.count {
if updatedContents[i].content.contentInfo.author.id == userID {
let content = updatedContents[i]
let contentInfo = content.content.contentInfo
let userContent = content.content
let opacity = contentInfo.opacity.reduced()

let updatedContent = Content(
content: UserContent(
id: userContent.id,
contentInfo: ContentInfo(
author: contentInfo.author,
createdDate: contentInfo.createdDate,
title: contentInfo.title,
imageURL: contentInfo.imageURL,
text: contentInfo.text,
status: .blind,
like: contentInfo.like,
opacity: opacity,
commentCount: contentInfo.commentCount
)
),
isDeleted: content.isDeleted
)

updatedContents[i] = updatedContent
}
}

contentsSubject.send(updatedContents)
})
.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

Address code duplication and apply static analysis suggestion.

This new reactive pipeline duplicates the logic from the existing didBannedTappedItem handler (lines 267-306). The duplication creates maintenance issues and inconsistency risks.

Additionally, the static analysis tool correctly identifies that the for loop with an if condition (line 321) should use a where clause for better Swift idiom.

Consider refactoring to eliminate duplication and apply the static analysis suggestion:

-        input.didBannedTappedItem
-            .withUnretained(self)
-            .flatMap { owner, content -> AnyPublisher<Int, Never> in
-                return owner.createBannedUseCase.execute(memberID: content.0, triggerType: .content, triggerID: content.1)
-                    .map { _ in content.0 }
-                    .asDriver(onErrorJustReturn: -1)
-            }
-            .sink(receiveValue: { userID in
-                guard userID != -1 else { return }
-                
-                var updatedContents = contentsSubject.value
-                
-                for i in 0..<updatedContents.count {
-                    if updatedContents[i].content.contentInfo.author.id == userID {
-                        let content = updatedContents[i]
-                        let contentInfo = content.content.contentInfo
-                        let userContent = content.content
-                        let opacity = contentInfo.opacity.reduced()
-                        
-                        let updatedContent = Content(
-                            content: UserContent(
-                                id: userContent.id,
-                                contentInfo: ContentInfo(
-                                    author: contentInfo.author,
-                                    createdDate: contentInfo.createdDate,
-                                    title: contentInfo.title,
-                                    imageURL: contentInfo.imageURL,
-                                    text: contentInfo.text,
-                                    status: .blind,
-                                    like: contentInfo.like,
-                                    opacity: opacity,
-                                    commentCount: contentInfo.commentCount
-                                )
-                            ),
-                            isDeleted: content.isDeleted
-                        )
-                        
-                        updatedContents[i] = updatedContent
-                    }
-                }
-                
-                contentsSubject.send(updatedContents)
-            })
-            .store(in: cancelBag)

And update the existing handler to use a where clause:

-                for i in 0..<updatedContents.count {
-                    if updatedContents[i].content.contentInfo.author.id == userID {
+                for i in 0..<updatedContents.count where updatedContents[i].content.contentInfo.author.id == userID {

Instead, consider creating a helper method to update all contents by a banned user and remove the duplicate pipeline.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 321-321: where clauses are preferred over a single if inside a for

(for_where)

🤖 Prompt for AI Agents
In Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift between lines 308
and 351, the reactive pipeline duplicates the logic already implemented in the
didBannedTappedItem handler at lines 267-306, causing maintenance and
consistency issues. Refactor by extracting the content update logic into a
helper method that updates all contents by a banned user, then call this helper
from the existing handler to avoid duplication. Also, replace the for-loop with
an if condition at line 321 by using a for-in loop with a where clause to
conform to Swift idiomatic style. Remove the duplicate pipeline entirely after
refactoring.

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: 2

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between bef5743 and 71db1c6.

📒 Files selected for processing (1)
  • Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (1)
Wable-iOS/Domain/Entity/Opacity.swift (1)
  • reduced (72-78)
🪛 SwiftLint (0.57.0)
Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift

[Warning] 208-208: where clauses are preferred over a single if inside a for

(for_where)


[Warning] 282-282: where clauses are preferred over a single if inside a for

(for_where)

Comment on lines +204 to 238
.sink(receiveValue: { userID in
var updatedContents = contentsSubject.value

let content = updatedContents[index]
let contentInfo = content.content.contentInfo
let userContent = content.content
let opacity = contentInfo.opacity.reduced()

let updatedContent = Content(
content: UserContent(
id: userContent.id,
contentInfo: ContentInfo(
author: contentInfo.author,
createdDate: contentInfo.createdDate,
title: contentInfo.title,
imageURL: contentInfo.imageURL,
text: contentInfo.text,
status: .ghost,
like: contentInfo.like,
opacity: opacity,
commentCount: contentInfo.commentCount
for i in 0..<updatedContents.count {
if updatedContents[i].content.contentInfo.author.id == userID {
let content = updatedContents[i]
let contentInfo = content.content.contentInfo
let userContent = content.content
let opacity = contentInfo.opacity.reduced()

let updatedContent = Content(
content: UserContent(
id: userContent.id,
contentInfo: ContentInfo(
author: contentInfo.author,
createdDate: contentInfo.createdDate,
title: contentInfo.title,
imageURL: contentInfo.imageURL,
text: contentInfo.text,
status: .ghost,
like: contentInfo.like,
opacity: opacity,
commentCount: contentInfo.commentCount
)
),
isDeleted: content.isDeleted
)
),
isDeleted: content.isDeleted
)

updatedContents[i] = updatedContent
}
}

updatedContents[index] = updatedContent
contentsSubject.send(updatedContents)
})
.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

Extract common content update logic to eliminate duplication.

The didGhostTappedItem handler duplicates significant logic with the didBannedTappedItem handler. Both iterate through all contents, check author IDs, create updated content with reduced opacity, and update the contents array. The only differences are the status value (.ghost vs .blind) and the presence of a guard clause.

Additionally, the SwiftLint warning correctly identifies that the for loop with if condition should use a where clause for better Swift idiom.

Consider extracting the common update logic into a helper method:

+private func updateContentsByUser(
+    userID: Int, 
+    newStatus: ContentStatus, 
+    contents: inout [Content]
+) {
+    for i in 0..<contents.count where contents[i].content.contentInfo.author.id == userID {
+        let content = contents[i]
+        let contentInfo = content.content.contentInfo
+        let userContent = content.content
+        let opacity = contentInfo.opacity.reduced()
+        
+        let updatedContent = Content(
+            content: UserContent(
+                id: userContent.id,
+                contentInfo: ContentInfo(
+                    author: contentInfo.author,
+                    createdDate: contentInfo.createdDate,
+                    title: contentInfo.title,
+                    imageURL: contentInfo.imageURL,
+                    text: contentInfo.text,
+                    status: newStatus,
+                    like: contentInfo.like,
+                    opacity: opacity,
+                    commentCount: contentInfo.commentCount
+                )
+            ),
+            isDeleted: content.isDeleted
+        )
+        
+        contents[i] = updatedContent
+    }
+}

Then simplify the ghost handler:

-            .sink(receiveValue: { userID in
-                var updatedContents = contentsSubject.value
-                
-                for i in 0..<updatedContents.count {
-                    if updatedContents[i].content.contentInfo.author.id == userID {
-                        let content = updatedContents[i]
-                        let contentInfo = content.content.contentInfo
-                        let userContent = content.content
-                        let opacity = contentInfo.opacity.reduced()
-                        
-                        let updatedContent = Content(
-                            content: UserContent(
-                                id: userContent.id,
-                                contentInfo: ContentInfo(
-                                    author: contentInfo.author,
-                                    createdDate: contentInfo.createdDate,
-                                    title: contentInfo.title,
-                                    imageURL: contentInfo.imageURL,
-                                    text: contentInfo.text,
-                                    status: .ghost,
-                                    like: contentInfo.like,
-                                    opacity: opacity,
-                                    commentCount: contentInfo.commentCount
-                                )
-                            ),
-                            isDeleted: content.isDeleted
-                        )
-                        
-                        updatedContents[i] = updatedContent
-                    }
-                }
-                
-                contentsSubject.send(updatedContents)
-            })
+            .sink(receiveValue: { [weak self] userID in
+                var updatedContents = contentsSubject.value
+                self?.updateContentsByUser(userID: userID, newStatus: .ghost, contents: &updatedContents)
+                contentsSubject.send(updatedContents)
+            })
📝 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
.sink(receiveValue: { userID in
var updatedContents = contentsSubject.value
let content = updatedContents[index]
let contentInfo = content.content.contentInfo
let userContent = content.content
let opacity = contentInfo.opacity.reduced()
let updatedContent = Content(
content: UserContent(
id: userContent.id,
contentInfo: ContentInfo(
author: contentInfo.author,
createdDate: contentInfo.createdDate,
title: contentInfo.title,
imageURL: contentInfo.imageURL,
text: contentInfo.text,
status: .ghost,
like: contentInfo.like,
opacity: opacity,
commentCount: contentInfo.commentCount
for i in 0..<updatedContents.count {
if updatedContents[i].content.contentInfo.author.id == userID {
let content = updatedContents[i]
let contentInfo = content.content.contentInfo
let userContent = content.content
let opacity = contentInfo.opacity.reduced()
let updatedContent = Content(
content: UserContent(
id: userContent.id,
contentInfo: ContentInfo(
author: contentInfo.author,
createdDate: contentInfo.createdDate,
title: contentInfo.title,
imageURL: contentInfo.imageURL,
text: contentInfo.text,
status: .ghost,
like: contentInfo.like,
opacity: opacity,
commentCount: contentInfo.commentCount
)
),
isDeleted: content.isDeleted
)
),
isDeleted: content.isDeleted
)
updatedContents[i] = updatedContent
}
}
updatedContents[index] = updatedContent
contentsSubject.send(updatedContents)
})
.store(in: cancelBag)
.sink(receiveValue: { [weak self] userID in
var updatedContents = contentsSubject.value
self?.updateContentsByUser(userID: userID, newStatus: .ghost, contents: &updatedContents)
contentsSubject.send(updatedContents)
})
.store(in: cancelBag)
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 208-208: where clauses are preferred over a single if inside a for

(for_where)

🤖 Prompt for AI Agents
In Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift between lines 204
and 238, the code in the didGhostTappedItem handler duplicates logic found in
didBannedTappedItem, both iterating contents and updating items with reduced
opacity and different status values. To fix this, extract the common update
logic into a helper method that takes the author ID and status as parameters,
applies the opacity reduction, and updates the contents array accordingly. Also,
refactor the for loop to use a where clause for filtering by author ID to follow
Swift idioms. Then, simplify the didGhostTappedItem handler by calling this
helper method with the appropriate status.

Comment on lines 276 to 312
.sink(receiveValue: { userID in
guard userID != -1 else { return }

let content = updatedContents[index]
let contentInfo = content.content.contentInfo
let userContent = content.content
let opacity = contentInfo.opacity.reduced()
var updatedContents = contentsSubject.value

let updatedContent = Content(
content: UserContent(
id: userContent.id,
contentInfo: ContentInfo(
author: contentInfo.author,
createdDate: contentInfo.createdDate,
title: contentInfo.title,
imageURL: contentInfo.imageURL,
text: contentInfo.text,
status: .blind,
like: contentInfo.like,
opacity: opacity,
commentCount: contentInfo.commentCount
for i in 0..<updatedContents.count {
if updatedContents[i].content.contentInfo.author.id == userID {
let content = updatedContents[i]
let contentInfo = content.content.contentInfo
let userContent = content.content
let opacity = contentInfo.opacity.reduced()

let updatedContent = Content(
content: UserContent(
id: userContent.id,
contentInfo: ContentInfo(
author: contentInfo.author,
createdDate: contentInfo.createdDate,
title: contentInfo.title,
imageURL: contentInfo.imageURL,
text: contentInfo.text,
status: .blind,
like: contentInfo.like,
opacity: opacity,
commentCount: contentInfo.commentCount
)
),
isDeleted: content.isDeleted
)
),
isDeleted: content.isDeleted
)

updatedContents[i] = updatedContent
}
}

updatedContents[index] = updatedContent
contentsSubject.send(updatedContents)
})
.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

Apply the same refactoring to eliminate code duplication.

This handler has nearly identical logic to the didGhostTappedItem handler, with only minor differences (the guard clause and .blind status). After implementing the helper method suggested above, this handler should also be simplified.

Apply this diff to use the extracted helper method:

-            .sink(receiveValue: { userID in
-                guard userID != -1 else { return }
-                
-                var updatedContents = contentsSubject.value
-                
-                for i in 0..<updatedContents.count {
-                    if updatedContents[i].content.contentInfo.author.id == userID {
-                        let content = updatedContents[i]
-                        let contentInfo = content.content.contentInfo
-                        let userContent = content.content
-                        let opacity = contentInfo.opacity.reduced()
-                        
-                        let updatedContent = Content(
-                            content: UserContent(
-                                id: userContent.id,
-                                contentInfo: ContentInfo(
-                                    author: contentInfo.author,
-                                    createdDate: contentInfo.createdDate,
-                                    title: contentInfo.title,
-                                    imageURL: contentInfo.imageURL,
-                                    text: contentInfo.text,
-                                    status: .blind,
-                                    like: contentInfo.like,
-                                    opacity: opacity,
-                                    commentCount: contentInfo.commentCount
-                                )
-                            ),
-                            isDeleted: content.isDeleted
-                        )
-                        
-                        updatedContents[i] = updatedContent
-                    }
-                }
-                
-                contentsSubject.send(updatedContents)
-            })
+            .sink(receiveValue: { [weak self] userID in
+                guard userID != -1 else { return }
+                
+                var updatedContents = contentsSubject.value
+                self?.updateContentsByUser(userID: userID, newStatus: .blind, contents: &updatedContents)
+                contentsSubject.send(updatedContents)
+            })

This refactoring eliminates ~60 lines of duplicated code and addresses the SwiftLint warnings about for-where clause usage.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 282-282: where clauses are preferred over a single if inside a for

(for_where)

🤖 Prompt for AI Agents
In Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift between lines 276
and 312, the sink handler duplicates logic found in the didGhostTappedItem
handler, differing mainly in the guard condition and the status set to .blind.
Refactor by extracting the common update logic into a helper method that accepts
parameters for the guard condition and status, then call this helper from both
handlers. This will remove about 60 lines of duplicated code and resolve
SwiftLint warnings related to for-where usage.

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.

고생하셨습니다~

이제 편히 자러 갑시다~

jsonDecoder: JSONDecoder()
guard let self = self else { return }

if self.activeUserID == item.content.contentInfo.author.id,
Copy link
Collaborator

Choose a reason for hiding this comment

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

추후에 뷰모델에서 판단하도록 합시다!

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 +209 to +229
let content = updatedContents[i]
let contentInfo = content.content.contentInfo
let userContent = content.content
let opacity = contentInfo.opacity.reduced()

let updatedContent = Content(
content: UserContent(
id: userContent.id,
contentInfo: ContentInfo(
author: contentInfo.author,
createdDate: contentInfo.createdDate,
title: contentInfo.title,
imageURL: contentInfo.imageURL,
text: contentInfo.text,
status: .ghost,
like: contentInfo.like,
opacity: opacity,
commentCount: contentInfo.commentCount
)
),
isDeleted: content.isDeleted
Copy link
Collaborator

Choose a reason for hiding this comment

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

빨리 Content와 Comment를 하나로 통합해야 겠어요.

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 277 to 278
guard userID != -1 else { return }

Copy link
Collaborator

Choose a reason for hiding this comment

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

sink 전에 해당 조건을 가지고 filter를 써도 좋을 것 같네요.

Comment on lines +19 to 21

// TODO: 서버로 알람 수신 여부 보내기
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

감사합니다~ 👍

@github-project-automation github-project-automation bot moved this to In Review in Wable-iOS May 24, 2025
@youz2me youz2me merged commit 395caad into develop May 24, 2025
1 check was pending
@github-project-automation github-project-automation bot moved this from In Review to Done in Wable-iOS May 24, 2025
@youz2me youz2me deleted the feat/#194-fcm branch May 24, 2025 17:29
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 기능 또는 객체 구현 🛠️ fix 기능적 버그나 오류 해결 시 사용 ♻️ refactor 기존 코드를 리팩토링하거나 수정하는 등 사용 (생산적인 경우) 🦉 유진 🛌🛌🛌🛌🛌🛌🛌🛌🛌🛌

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Feat] FCM 재구현

4 participants