Skip to content

Conversation

@JinUng41
Copy link
Collaborator

@JinUng41 JinUng41 commented May 19, 2025

👻 PULL REQUEST

📄 작업 내용

  • 프로필 화면을 구현하다가 잠시 멈추었습니다.

💻 주요 코드 설명

📍 async/await을 도입하여 Combine 코드 지옥에서 탈피해보자.

  • APIProvider의 requestasync/await으로도 동작하게끔 하여, 그 이후의 사용되는 로직에서도 async/await으로 작성할 수 있도록 하였습니다.
  • 이에 따라 추가로 ErrorMapper도 구현하였습니다.

🔗 연결된 이슈

Summary by CodeRabbit

  • New Features

    • Introduced a redesigned user profile screen with enhanced profile editing, segmented views for posts and comments, and improved layout.
    • Added dedicated screens for account information, alarm (notification) settings, and account withdrawal, each with new interactive UI components.
    • Implemented multi-step account withdrawal flow, including reason selection and confirmation guides.
    • Provided support for profile image customization with new logic for default and custom images.
  • Improvements

    • Enhanced error handling and user feedback throughout profile and account management features.
    • Updated navigation and menu options for easier access to account info, alarm settings, feedback, and support.
  • Bug Fixes

    • Improved profile image handling to display appropriate defaults and placeholders.
  • Refactor

    • Migrated to new async/await and Combine-based data flows for improved performance and maintainability.
    • Renamed and reorganized several files and components for clarity and scalability.
  • Chores

    • Updated asset catalog with new icons and images.
    • Removed obsolete or unused controllers and methods.

JinUng41 and others added 30 commits May 12, 2025 21:51
# Conflicts:
#	Wable-iOS.xcodeproj/project.pbxproj
- 기존의 ViewModelType을 따르도록 함.
@JinUng41 JinUng41 requested a review from youz2me May 19, 2025 06:17
@JinUng41 JinUng41 self-assigned this May 19, 2025
@JinUng41 JinUng41 added ✨ feat 기능 또는 객체 구현 🍻 진웅 술 한잔 가온나~ labels May 19, 2025
@coderabbitai
Copy link

coderabbitai bot commented May 19, 2025

Walkthrough

This update restructures and expands the profile, withdrawal, account info, and alarm setting features in the Wable-iOS project. It introduces new view controllers, view models, and UI components for profile management, withdrawal flows, and account settings. Several files are renamed, removed, or refactored, with new use cases and repository interfaces adopting Swift concurrency and Combine for asynchronous operations.

Changes

File(s) / Path(s) Change Summary
Wable-iOS.xcodeproj/project.pbxproj Project structure updated: new/renamed/removed files for profile, withdrawal, account info, and alarm setting features. Groups and references updated.
Wable-iOS/Domain/Entity/UserProfile.swift UserProfile now conforms to Hashable.
Wable-iOS/Domain/Enum/WithdrawalReason.swift Enum renamed from AccountDeleteReason to WithdrawalReason and now conforms to CaseIterable.
Wable-iOS/Domain/RepositoryInterface/ProfileRepository.swift Method fetchUserInfo() replaced by async/await fetchAccountInfo().
Wable-iOS/Domain/UseCase/Onboarding/UserProfileUseCase.swift Class renamed to UserProfileUseCase; method signatures updated for optional parameters and new fetch method by userID.
Wable-iOS/Domain/UseCase/Home/FetchUserContentListUseCase.swift File deleted; old use case class removed.
Wable-iOS/Domain/UseCase/Profile/FetchUserProfileUseCase.swift
.../FetchUserCommentListUseCase.swift
...
New use cases for fetching user profile, comments, content, account info, withdrawal, and session removal added.
Wable-iOS/Data/Mapper/ErrorMapper.swift New ErrorMapper utility for mapping generic errors to WableError.
Wable-iOS/Data/Mapper/ProfileMapper.swift Improved social platform string parsing for enum mapping.
Wable-iOS/Data/RepositoryImpl/ProfileRepositoryImpl.swift fetchUserInfo() replaced by async/await fetchAccountInfo() with new error handling.
Wable-iOS/Infra/Network/APIProvider.swift Added async/await version of request method.
Wable-iOS/Infra/Network/TargetType/ContentTargetType.swift fetchUserContentList enum case updated to accept both memberID and cursor.
Wable-iOS/Infra/Network/TargetType/CommentTargetType.swift Endpoint path for user comment list updated.
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift
.../HomeViewController.swift
Navigation to old ProfileViewController removed; replaced with TODOs for future profile feature.
Wable-iOS/Presentation/Onboarding/View/ProfileRegisterView.swift Added configureProfileView(profileImageURL:) and adjusted view setup logic.
Wable-iOS/Presentation/Onboarding/ViewController/AgreementViewController.swift profileUseCase now uses UserProfileUseCase instead of CreateUserProfileUseCase.
Wable-iOS/Presentation/Onboarding/ViewController/ProfileRegisterViewController.swift Added call to configureView() in setup.
Wable-iOS/Presentation/Profile/AccountInfo/Model/AccountInfoCellItem.swift
.../View/AccountInfoViewController.swift
.../View/Cell/AccountInfoCell.swift
.../ViewModel/AccountInfoViewModel.swift
New model, view controller, cell, and view model for account info screen.
Wable-iOS/Presentation/Profile/AlarmSetting/View/AlarmSettingViewController.swift
.../ViewModel/AlarmSettingViewModel.swift
New view controller and view model for alarm (notification) settings.
Wable-iOS/Presentation/Profile/My/View/MyProfileViewController.swift
.../View/Subview/MyProfileView.swift
.../ViewModel/MyProfileViewModel.swift
New user profile screen, custom view, and view model for displaying and managing user's profile, posts, and comments.
Wable-iOS/Presentation/Profile/ProfileEditViewController.swift New controller for editing user profile, including nickname and image.
Wable-iOS/Presentation/Profile/ProfileInfoCell.swift New cell for displaying user profile info.
Wable-iOS/Presentation/Profile/ProfileSegmentKind.swift
.../ProfileSegmentedHeaderView.swift
New enum and reusable header for segmenting profile content (posts/comments).
Wable-iOS/Presentation/Profile/ProfileViewItem.swift New struct to group profile, content, and comment data for UI.
Wable-iOS/Presentation/Profile/ProfileViewController.swift Old, empty ProfileViewController deleted.
Wable-iOS/Presentation/Profile/Withdrawal/Guide/View/Subview/WithdrawalGuideDescriptionView.swift
.../WithdrawalGuideView.swift
.../WithdrawalGuideViewController.swift
.../ViewModel/WithdrawalGuideViewModel.swift
New UI and logic for withdrawal guide and confirmation flow.
Wable-iOS/Presentation/Profile/Withdrawal/Reason/Model/WithdrawalReasonCellItem.swift
.../View/Cell/WithdrawalReasonCell.swift
.../View/WithdrawalReasonViewController.swift
.../ViewModel/WithdrawalReasonViewModel.swift
New model, cell, view controller, and view model for withdrawal reason selection.
Wable-iOS/Presentation/TabBar/TabBarController.swift Tab bar now uses new MyProfileViewController with fully constructed view model and dependencies.
Wable-iOS/Presentation/Viewit/List/View/Cell/ViewitListCell.swift Improved logic for assigning random/default profile images or loading from URL.
Wable-iOS/Presentation/WableComponent/Navigation/NavigationView.swift setNavigationTitle(text:) now updates both title labels.
Wable-iOS/Resource/Assets.xcassets/Icon/ic_dot.imageset/Contents.json New image asset for dot icon added.
Wable-iOS/App/SceneDelegate.swift Removed trailing whitespace; no functional change.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant TabBarController
    participant MyProfileViewController
    participant MyProfileViewModel
    participant ProfileRepository
    participant CommentRepository
    participant ContentRepository

    User->>TabBarController: Tap Profile Tab
    TabBarController->>MyProfileViewController: Show Profile Screen
    MyProfileViewController->>MyProfileViewModel: Load Profile Data
    MyProfileViewModel->>ProfileRepository: Fetch UserProfile
    MyProfileViewModel->>ContentRepository: Fetch UserContentList
    MyProfileViewModel->>CommentRepository: Fetch UserCommentList
    ProfileRepository-->>MyProfileViewModel: UserProfile
    ContentRepository-->>MyProfileViewModel: UserContent[]
    CommentRepository-->>MyProfileViewModel: UserComment[]
    MyProfileViewModel-->>MyProfileViewController: ProfileViewItem (profile, content, comment)
    MyProfileViewController-->>User: Display Profile, Posts, Comments
Loading
sequenceDiagram
    participant User
    participant MyProfileViewController
    participant AccountInfoViewController
    participant AccountInfoViewModel
    participant FetchAccountInfoUseCase
    participant ProfileRepository

    User->>MyProfileViewController: Tap Account Info
    MyProfileViewController->>AccountInfoViewController: Show Account Info Screen
    AccountInfoViewController->>AccountInfoViewModel: viewDidLoad()
    AccountInfoViewModel->>FetchAccountInfoUseCase: execute()
    FetchAccountInfoUseCase->>ProfileRepository: fetchAccountInfo()
    ProfileRepository-->>FetchAccountInfoUseCase: AccountInfo
    FetchAccountInfoUseCase-->>AccountInfoViewModel: AccountInfo
    AccountInfoViewModel-->>AccountInfoViewController: items
    AccountInfoViewController-->>User: Display Account Info
Loading
sequenceDiagram
    participant User
    participant AccountInfoViewController
    participant WithdrawalReasonViewController
    participant WithdrawalReasonViewModel
    participant WithdrawalGuideViewController
    participant WithdrawalGuideViewModel
    participant WithdrawUseCase
    participant RemoveUserSessionUseCase
    participant AccountRepository
    participant UserSessionRepository

    User->>AccountInfoViewController: Tap "Delete Account"
    AccountInfoViewController->>WithdrawalReasonViewController: Show Withdrawal Reasons
    User->>WithdrawalReasonViewController: Select Reasons, Tap Next
    WithdrawalReasonViewController->>WithdrawalGuideViewController: Show Withdrawal Guide
    User->>WithdrawalGuideViewController: Agree, Tap Next, Confirm
    WithdrawalGuideViewController->>WithdrawalGuideViewModel: Withdraw
    WithdrawalGuideViewModel->>WithdrawUseCase: execute(reasons)
    WithdrawUseCase->>AccountRepository: deleteAccount(reasons)
    AccountRepository-->>WithdrawUseCase: Success
    WithdrawUseCase-->>WithdrawalGuideViewModel: Success
    WithdrawalGuideViewModel->>RemoveUserSessionUseCase: removeUserSession()
    RemoveUserSessionUseCase->>UserSessionRepository: Remove Session
    UserSessionRepository-->>RemoveUserSessionUseCase: Success
    WithdrawalGuideViewModel-->>WithdrawalGuideViewController: isWithdrawSuccess
    WithdrawalGuideViewController-->>User: Show Login Screen
Loading

Assessment against linked issues

Objective Addressed Explanation
프로필 ui를 구현합니다. (#182)
프로필 관련 기능을 구현합니다. (#182)

Poem

🐰
A profile reborn, with segments and flair,
Withdrawal and settings—new features to share!
Buttons and cells, all shiny and bright,
MyProfile hops in, everything’s right.
With code so refreshed, and views so divine,
This bunny’s delighted—your profile will shine!

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.


Note

⚡️ 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: 22

🔭 Outside diff range comments (1)
Wable-iOS/Presentation/Onboarding/ViewController/AgreementViewController.swift (1)

140-217: 🛠️ Refactor suggestion

Consider migrating this Combine implementation to async/await

Since the PR objectives mention introducing async/await syntax to improve code readability and maintainability, consider refactoring this nested Combine chain to use the new async/await pattern. This would make the code more linear and easier to follow.

Here's how you might refactor the nextButtonDidTap method:

@objc func nextButtonDidTap() {
    Task {
        do {
            guard let userSession = try await userInformationUseCase.fetchActiveUserInfoAsync() else { return }
            
            try await profileUseCase.executeAsync(
                profile: UserProfile(
                    user: User(
                        id: userSession.id,
                        nickname: nickname,
                        profileURL: userSession.profileURL,
                        fanTeam: LCKTeam(rawValue: lckTeam)
                    ),
                    introduction: "",
                    ghostCount: 0,
                    lckYears: lckYear,
                    userLevel: 1
                ),
                isPushAlarmAllowed: rootView.marketingAgreementItemView.checkButton.isSelected,
                isAlarmAllowed: rootView.marketingAgreementItemView.checkButton.isSelected,
                image: profileImage,
                defaultProfileType: defaultImage
            )
            
            try await updateFCMTokenUseCase.executeAsync(nickname: nickname)
            
            try await userInformationUseCase.updateUserSessionAsync(
                userID: userSession.id,
                nickname: userSession.nickname,
                profileURL: userSession.profileURL,
                isPushAlarmAllowed: userSession.isPushAlarmAllowed,
                isAdmin: userSession.isAdmin,
                isAutoLoginEnabled: true,
                notificationBadgeCount: userSession.notificationBadgeCount
            )
            
            WableLogger.log("세션 저장 완료", for: .debug)
            
            await MainActor.run {
                guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
                      let loginViewController = windowScene.windows.first?.rootViewController
                else {
                    return
                }
                
                let tabBarController = TabBarController()
                
                dismiss(animated: false) {
                    loginViewController.present(tabBarController, animated: true) {
                        let noticeViewController = WableSheetViewController(
                            title: "와블과 함께해 주셔서 감사합니다!",
                            message: "\(self.nickname)\n와블의 일원이 되신 것을 환영해요.\nLCK 함께 보며 같이 즐겨요 :)"
                        )
                        
                        noticeViewController.addAction(.init(title: "와블 즐기러 가기", style: .primary))
                        
                        tabBarController.present(noticeViewController, animated: true)
                    }
                }
            }
        } catch {
            // Handle errors appropriately
            WableLogger.log("Profile creation error: \(error)", for: .error)
        }
    }
}

Note: You'll need to ensure that the use cases have async variants of their methods.

🧹 Nitpick comments (48)
Wable-iOS/Presentation/WableComponent/Navigation/NavigationView.swift (1)

275-275: Navigation title now consistently updates both title labels.

The setNavigationTitle(text:) method now updates both hubTitleLabel and pageTitleLabel, ensuring consistent navigation title styling across different view types.

Consider adding conditional updates based on the current navigation type to avoid setting titles for hidden labels:

func setNavigationTitle(text: String) {
    hubTitleLabel.attributedText = text.pretendardString(with: .head2)
    pageTitleLabel.attributedText = text.pretendardString(with: .head2)
+   
+   // Alternatively, consider conditionally updating labels based on navigation type
+   // if case .hub = type {
+   //     hubTitleLabel.attributedText = text.pretendardString(with: .head2)
+   // } else {
+   //     pageTitleLabel.attributedText = text.pretendardString(with: .head2)
+   // }
}
Wable-iOS/Domain/RepositoryInterface/ProfileRepository.swift (1)

13-13: Good adoption of async/await for improved readability.

The change from Combine's AnyPublisher<AccountInfo, WableError> to Swift's native async throws -> AccountInfo pattern is an excellent improvement that aligns with modern Swift concurrency practices. The method name change from fetchUserInfo() to fetchAccountInfo() also provides better clarity about what's being fetched.

Consider creating a migration plan to eventually convert other repository methods (lines 14-25) from Combine to async/await for API consistency. This would further simplify your codebase by standardizing on a single concurrency model.

Wable-iOS/Data/Mapper/ProfileMapper.swift (1)

19-20: Improved social platform extraction logic.

The updated code correctly handles situations where the social platform string contains additional information by extracting just the first word. This is a good fix that adds robustness to the mapping logic.

Consider a slightly more concise implementation:

-let splitKeyword = response.socialPlatform.split(separator: " ").map { "\($0)" }.first
-let socialPlatform = SocialPlatform(rawValue: splitKeyword ?? response.socialPlatform)
+let platformName = response.socialPlatform.components(separatedBy: " ").first ?? response.socialPlatform
+let socialPlatform = SocialPlatform(rawValue: platformName)
Wable-iOS/Presentation/Profile/ProfileViewItem.swift (1)

10-14: Consider renaming the comment property to plural form

The property comment contains an array of UserComment objects, so using the plural form comments would be more consistent with Swift naming conventions and make the code more readable.

struct ProfileViewItem {
    var profileInfo: UserProfile?
    var content: [UserContent]
-    var comment: [UserComment]
+    var comments: [UserComment]
}
Wable-iOS/Presentation/Profile/AccountInfo/Model/AccountInfoCellItem.swift (1)

10-20: Clean model design with appropriate Hashable conformance.

The AccountInfoCellItem struct has a clean design with clear properties and a good initializer pattern that provides a default value for isUserInteractive. The Hashable conformance is appropriate for use with diffable data sources.

Consider using localization for user-facing strings when you populate this model to support multiple languages in the future.

Wable-iOS/Data/Mapper/ErrorMapper.swift (1)

28-44: Good addition to support async/await error handling.

The new ErrorMapper enum effectively mirrors the existing Publisher extension's error mapping logic but makes it available for use in async/await contexts. This supports the PR's goal of transitioning from Combine to async/await for improved code readability.

Consider extending the mapper to handle different types of non-NetworkError errors more specifically, rather than mapping all of them to .unknownError. This would provide more granular error information to the user.

 enum ErrorMapper {
     static func map(_ error: Error) -> WableError {
         WableLogger.log(error.localizedDescription, for: .network)
         WableLogger.log("\(error)", for: .network)
         
         if let networkError = error as? NetworkError {
             switch networkError {
             case .statusError(_, let message):
                 return WableError(rawValue: message) ?? .networkError
             default:
                 return .unknownError
             }
         }
+        
+        // Handle other specific error types if needed
+        if let decodingError = error as? DecodingError {
+            return .decodingError
+        }
         
         return .unknownError
     }
 }
Wable-iOS/Presentation/Profile/Withdrawal/Guide/View/Subview/WithdrawalGuideDescriptionView.swift (1)

13-36: Well-structured UI component with clean configuration API.

The WithdrawalGuideDescriptionView is a well-designed UI component with proper initialization and a clean configuration API. The configure(description:) method provides a good way to update the label's content.

Consider extracting the hardcoded string "설명" from line 17 to a constant or use localization to support multiple languages:

 private let descriptionLabel = UILabel().then {
-    $0.attributedText = "설명".pretendardString(with: .body2)
+    $0.attributedText = NSLocalizedString("description_placeholder", comment: "Default text for description label").pretendardString(with: .body2)
     $0.textColor = .gray800
     $0.numberOfLines = 0
 }
Wable-iOS/Presentation/Profile/AlarmSetting/ViewModel/AlarmSettingViewModel.swift (1)

15-20: Well-implemented async notification authorization check.

The checkAlarmAuthorization method appropriately uses Swift concurrency with Task to perform the async operation of checking notification settings. The code is clean and focused on a single responsibility.

One suggestion to consider:

Consider adding error handling if there's any possibility the notification settings check could fail:

func checkAlarmAuthorization() {
    Task {
-       let settings = await UNUserNotificationCenter.current().notificationSettings()
-       isAuthorized = settings.authorizationStatus == .authorized
+       do {
+           let settings = await UNUserNotificationCenter.current().notificationSettings()
+           isAuthorized = settings.authorizationStatus == .authorized
+       } catch {
+           // Handle any potential errors
+           isAuthorized = false
+           // Optionally log the error
+       }
    }
}

However, since the notificationSettings() method doesn't throw, the current implementation is acceptable.

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

8-9: Remove unnecessary Combine import.

The Combine framework is imported but not used anywhere in this file since the implementation uses Swift's native async/await pattern instead of Combine publishers.

-import Combine
 import Foundation
Wable-iOS/Presentation/Onboarding/View/ProfileRegisterView.swift (3)

191-200: Potential layout issue with constraint updates.

The method updates constraints that were already set in setupConstraint(), which might cause layout issues. Consider invalidating the existing constraints before updating them or refactoring the common layout code to avoid duplication.

func configureProfileView(profileImageURL: URL? = .none) {
+    // Remove existing constraints before updating
+    titleLabel.snp.removeConstraints()
+    profileImageView.snp.removeConstraints()
+    
    titleLabel.snp.updateConstraints { make in
        make.top.equalToSuperview().offset(28)
    }
    
    profileImageView.snp.makeConstraints { make in
        make.top.equalTo(titleLabel.snp.bottom).offset(62)
        make.centerX.equalToSuperview()
        make.size.equalTo(166.adjustedWidth)
    }

202-207: Consider using proper localization for text strings.

The hard-coded Korean text strings should be moved to a localization file for easier maintenance and future internationalization support.

Also, the button configuration updates are mixing styles. Consider creating a method for resetting button to completed state.


214-226: Extract hardcoded image references to constants.

The switch statement contains hardcoded string values that could be defined as constants. Additionally, consider creating a dedicated method for profile image selection to avoid duplicating logic between configureView and configureProfileView.

+private enum ProfileImageType {
+    static let purple = "PURPLE"
+    static let green = "GREEN"
+    static let blue = "BLUE"
+}

switch profileImageURL.absoluteString {
-case "PURPLE":
+case ProfileImageType.purple:
    profileImageView.image = .imgProfilePurple
-case "GREEN":
+case ProfileImageType.green:
    profileImageView.image = .imgProfileGreen
-case "BLUE":
+case ProfileImageType.blue:
    profileImageView.image = .imgProfileBlue
default:
    profileImageView.kf.setImage(
        with: profileImageURL,
        placeholder: [UIImage.imgProfilePurple, UIImage.imgProfileBlue, UIImage.imgProfileGreen].randomElement()
    )
}
Wable-iOS/Domain/UseCase/Profile/RemoveUserSessionUseCase.swift (2)

21-28: Consider using async/await for consistency.

This method is implemented synchronously, while other use cases in the PR are moving toward async/await. Consider making it consistent with the project's direction.

-func removeUserSession() {
+func removeUserSession() async {
    guard let userID = repository.fetchActiveUserID() else {
        WableLogger.log("유저 아이디를 찾을 수 없음.", for: .debug)
        return
    }
    
-    repository.removeUserSession(forUserID: userID)
+    await repository.removeUserSession(forUserID: userID)
}

This assumes that the repository methods would also be updated to be asynchronous.


23-23: Localize log messages for consistency.

The log message is in Korean, which might be inconsistent with other log messages. Consider using a consistent language strategy for logs across the application.

-WableLogger.log("유저 아이디를 찾을 수 없음.", for: .debug)
+WableLogger.log("User ID not found.", for: .debug)
Wable-iOS/Domain/UseCase/Profile/FetchUserProfileUseCase.swift (1)

22-30: Redundant map operator in publisher chain.

The .map { $0 } operation doesn't transform the data and can be removed without changing the behavior.

func execute(userID: Int) -> AnyPublisher<UserProfile?, WableError> {
    guard userID > .zero else {
        return .fail(.notFoundMember)
    }
    
    return repository.fetchUserProfile(memberID: userID)
-        .map { $0 }
        .eraseToAnyPublisher()
}
Wable-iOS/Presentation/Profile/ProfileSegmentedHeaderView.swift (1)

51-54: Consider adding proper documentation.

Adding documentation comments for the segmentDidChange method would help clarify its purpose, especially for maintaining closure-based callback patterns.

@objc
private func segmentDidChange() {
+    // Notify listeners about the segment change through the closure
    segmentDidChangeClosure?(segmentedControl.selectedSegmentIndex)
}
Wable-iOS/Presentation/TabBar/TabBarController.swift (2)

80-103: Complex dependency graph initialized inline.

The initialization of the profile view controller includes a deep tree of dependencies created inline, which can make the code harder to maintain. Consider moving this initialization logic to a factory or dependency injection container.

- private let profileViewController = MyProfileViewController(
-     viewModel: .init(
-         userinformationUseCase: FetchUserInformationUseCase(
-             repository: UserSessionRepositoryImpl(
-                 userDefaults: UserDefaultsStorage(jsonEncoder: .init(), jsonDecoder: .init())
-             )
-         ),
-         fetchUserProfileUseCase: FetchUserProfileUseCaseImpl(
-             repository: ProfileRepositoryImpl()
-         ),
-         fetchUserCommentListUseCase: FetchUserCommentListUseCaseImpl(
-             repository: CommentRepositoryImpl()
-         ),
-         fetchUserContentListUseCase: FetchUserContentUseCaseImpl(
-             repository: ContentRepositoryImpl()
-         ),
-         removeUserSessionUseCase: RemoveUserSessionUseCaseImpl(
-             repository: UserSessionRepositoryImpl(
-                 userDefaults: UserDefaultsStorage(
-                     jsonEncoder: .init(), jsonDecoder: .init()
-                 )
-             )
-         )
-     )
- )
+ private let profileViewController = MyProfileViewControllerFactory.create()

You could then define the factory in a separate file:

// MyProfileViewControllerFactory.swift
struct MyProfileViewControllerFactory {
    static func create() -> MyProfileViewController {
        let userDefaults = UserDefaultsStorage(jsonEncoder: .init(), jsonDecoder: .init())
        let userSessionRepository = UserSessionRepositoryImpl(userDefaults: userDefaults)
        
        let viewModel = MyProfileViewModel(
            userinformationUseCase: FetchUserInformationUseCase(repository: userSessionRepository),
            fetchUserProfileUseCase: FetchUserProfileUseCaseImpl(repository: ProfileRepositoryImpl()),
            fetchUserCommentListUseCase: FetchUserCommentListUseCaseImpl(repository: CommentRepositoryImpl()),
            fetchUserContentListUseCase: FetchUserContentUseCaseImpl(repository: ContentRepositoryImpl()),
            removeUserSessionUseCase: RemoveUserSessionUseCaseImpl(repository: userSessionRepository)
        )
        
        return MyProfileViewController(viewModel: viewModel)
    }
}

84-85: Redundant UserDefaultsStorage initialization.

You're creating two separate instances of UserDefaultsStorage with the same configuration. Consider reusing the same instance to avoid duplicating shared resources.

private let profileViewController = MyProfileViewController(
    viewModel: .init(
        userinformationUseCase: FetchUserInformationUseCase(
            repository: UserSessionRepositoryImpl(
-                userDefaults: UserDefaultsStorage(jsonEncoder: .init(), jsonDecoder: .init())
+                userDefaults: sharedUserDefaultsStorage
            )
        ),
        // ... other use cases ...
        removeUserSessionUseCase: RemoveUserSessionUseCaseImpl(
            repository: UserSessionRepositoryImpl(
                userDefaults: UserDefaultsStorage(
-                    jsonEncoder: .init(), jsonDecoder: .init()
+                    userDefaults: sharedUserDefaultsStorage
                )
            )
        )
    )
)

Where sharedUserDefaultsStorage could be a private property of the class:

private let sharedUserDefaultsStorage = UserDefaultsStorage(jsonEncoder: .init(), jsonDecoder: .init())

Also applies to: 98-100

Wable-iOS/Domain/UseCase/Profile/FetchUserContentListUseCase.swift (3)

2-2: Filename in comment header doesn't match actual filename

The file header comment refers to FetchUserContentUseCase.swift while the actual filename is FetchUserContentListUseCase.swift.

-//  FetchUserContentUseCase.swift
+//  FetchUserContentListUseCase.swift

15-15: Class name should match protocol name for consistency

The class name FetchUserContentUseCaseImpl doesn't include "List" which is present in the protocol name FetchUserContentListUseCase. This inconsistency can lead to confusion.

-final class FetchUserContentUseCaseImpl: FetchUserContentListUseCase {
+final class FetchUserContentListUseCaseImpl: FetchUserContentListUseCase {

22-25: Consider using guard pattern for consistency with other UseCases

Your validation logic uses an if statement, while similar use cases in the codebase (like FetchUserCommentListUseCase and FetchUserProfileUseCase) use the guard pattern. Consider adopting the same pattern for consistency.

-func execute(for userID: Int, last contentID: Int) -> AnyPublisher<[UserContent], WableError> {
-    if userID < .zero {
-        return .fail(.notFoundMember)
-    }
+func execute(for userID: Int, last contentID: Int) -> AnyPublisher<[UserContent], WableError> {
+    guard userID > .zero else {
+        return .fail(.notFoundMember)
+    }
Wable-iOS/Presentation/Profile/AccountInfo/View/Cell/AccountInfoCell.swift (3)

16-18: Remove hardcoded placeholder text

The placeholder "제목" (Title) will be replaced by actual data, but it's better to start with an empty text to avoid potential localization or testing issues.

private let titleLabel = UILabel().then {
-    $0.attributedText = "제목".pretendardString(with: .body2)
+    $0.font = UIFont.pretendardFont(for: .body2)
     $0.textColor = .gray600
}

20-23: Remove hardcoded placeholder text

Similar to the titleLabel, it's better to avoid hardcoded placeholder text in the descriptionLabel.

private let descriptionLabel = UILabel().then {
-    $0.attributedText = "본문".pretendardString(with: .body2)
+    $0.font = UIFont.pretendardFont(for: .body2)
     $0.textColor = .wableBlack
}

38-53: Make description label handling more robust

The current implementation sets text for normal descriptions but uses attributedText for interactive ones. This could lead to inconsistency if configure is called multiple times on the same cell with different values.

func configure(title: String, description: String, userInteraction: (() -> Void)? = nil) {
    titleLabel.text = title
    
+    // Reset interactive state
+    descriptionLabel.isUserInteractionEnabled = false
+    if let gestureRecognizers = descriptionLabel.gestureRecognizers {
+        for recognizer in gestureRecognizers {
+            descriptionLabel.removeGestureRecognizer(recognizer)
+        }
+    }
+    
    guard let userInteraction else {
        descriptionLabel.text = description
        return
    }
    
    userInteractionClosure = userInteraction
    descriptionLabel.attributedText = description.pretendardString(with: .body2).addUnderline()
    
    descriptionLabel.isUserInteractionEnabled = true
    
    let tapGesture = UITapGestureRecognizer(target: self, action: #selector(descriptionDidTap))
    descriptionLabel.addGestureRecognizer(tapGesture)
}
Wable-iOS/Presentation/Profile/Withdrawal/Reason/View/Cell/WithdrawalReasonCell.swift (2)

21-23: Remove hardcoded placeholder text and add accessibility support

Similar to previous files, avoid hardcoded placeholder text. Also, add accessibility support for the description label.

private let descriptionLabel = UILabel().then {
-    $0.attributedText = "설명".pretendardString(with: .body2)
+    $0.font = UIFont.pretendardFont(for: .body2)
+    $0.isAccessibilityElement = true
+    $0.accessibilityTraits = .staticText
}

57-60: Reconsider fixed checkbox size for better adaptability

The fixed size of 48x48 for the checkbox button might be too large on smaller devices. Consider a more adaptive approach.

checkboxButton.snp.makeConstraints { make in
    make.leading.centerY.equalToSuperview()
-    make.size.equalTo(48)
+    // Option 1: Use a more appropriate size that scales better
+    make.size.equalTo(24)
+    // Option 2: Scale with dynamic type for accessibility
+    // make.size.equalTo(UIFontMetrics.default.scaledValue(for: 24))
}
Wable-iOS/Presentation/Profile/AccountInfo/ViewModel/AccountInfoViewModel.swift (1)

25-31: Extract hardcoded titles to constants and prepare for localization

The current implementation hardcodes UI strings directly in the view model. Extract these to constants or a localization system for better maintainability and localization support.

+// Add at the top of the file
+private enum AccountInfoStrings {
+    static let socialLogin = "소셜 로그인"
+    static let versionInfo = "버전 정보"
+    static let userID = "아이디"
+    static let joinDate = "가입일"
+    static let termsOfService = "이용약관"
+    static let viewDetails = "자세히 보기"
+}

// Then in the viewDidLoad method:
items = [
-    .init(title: "소셜 로그인", description: accountInfo.socialPlatform?.rawValue ?? ""),
-    .init(title: "버전 정보", description: accountInfo.version),
-    .init(title: "아이디", description: accountInfo.displayMemberID),
-    .init(title: "가입일", description: formatDate(accountInfo.createdDate ?? .now)),
-    .init(title: "이용약관", description: "자세히 보기", isUserInteractive: true)
+    .init(title: AccountInfoStrings.socialLogin, description: accountInfo.socialPlatform?.rawValue ?? ""),
+    .init(title: AccountInfoStrings.versionInfo, description: accountInfo.version),
+    .init(title: AccountInfoStrings.userID, description: accountInfo.displayMemberID),
+    .init(title: AccountInfoStrings.joinDate, description: formatDate(accountInfo.createdDate ?? .now)),
+    .init(title: AccountInfoStrings.termsOfService, description: AccountInfoStrings.viewDetails, isUserInteractive: true)
]
Wable-iOS/Domain/UseCase/Onboarding/userProfileUseCase.swift (3)

2-2: Update file header comment to match the renamed class.

The file header comment still references "CreateUserProfileUseCase.swift" while the actual class name has changed to "userProfileUseCase".

-//  CreateUserProfileUseCase.swift
+//  userProfileUseCase.swift

20-21: Consider splitting responsibilities into separate use cases.

This use case now handles both updating and fetching user profiles, which might violate the Single Responsibility Principle. Consider creating separate use cases for different operations or renaming to reflect the broader scope.


32-34: Add documentation for the new method.

The new execute method lacks documentation explaining its purpose and parameters.

+/// Fetches a user profile by user ID
+/// - Parameter userID: The ID of the user whose profile should be fetched
+/// - Returns: A publisher that emits the user profile or an error
func execute(userID: Int) -> AnyPublisher<UserProfile, WableError> {
    return repository.fetchUserProfile(memberID: userID)
}
Wable-iOS/Presentation/Profile/Withdrawal/Guide/View/Subview/WithdrawalGuideView.swift (1)

119-120: Check grammar in the second withdrawal description.

There seems to be a grammatical issue in the second description text. The sentence "탈퇴와 재가입을 통해 아이디를 교체하며 선량한 이용자들께 피해를 끼치는 행위를 방지하려는 조치 오니 넓은 양해 부탁드립니다." might be missing a word between "조치" and "오니".

-static let secondDescription = "탈퇴와 재가입을 통해 아이디를 교체하며 선량한 이용자들께 피해를 끼치는 행위를 방지하려는 조치 오니 넓은 양해 부탁드립니다."
+static let secondDescription = "탈퇴와 재가입을 통해 아이디를 교체하며 선량한 이용자들께 피해를 끼치는 행위를 방지하려는 조치이오니 넓은 양해 부탁드립니다."
Wable-iOS/Presentation/Profile/Withdrawal/Guide/View/WithdrawalGuideViewController.swift (1)

95-116: Consider using dependency injection for login screen initialization.

The method to present the login view creates multiple new instances and dependencies inline. Consider using dependency injection or a factory pattern to improve testability and reduce coupling.

func presentLoginView() {
    guard let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate else {
        return WableLogger.log("SceneDelegate 찾을 수 없음.", for: .debug)
    }
    
-    sceneDelegate.window?.rootViewController = LoginViewController(
-        viewModel: .init(
-            updateFCMTokenUseCase: UpdateFCMTokenUseCase(repository: ProfileRepositoryImpl()),
-            fetchUserAuthUseCase: FetchUserAuthUseCase(
-                loginRepository: LoginRepositoryImpl(),
-                userSessionRepository: UserSessionRepositoryImpl(
-                    userDefaults: UserDefaultsStorage(jsonEncoder: .init(), jsonDecoder: .init())
-                )
-            ),
-            updateUserSessionUseCase: FetchUserInformationUseCase(
-                repository: UserSessionRepositoryImpl(
-                    userDefaults: UserDefaultsStorage(jsonEncoder: .init(), jsonDecoder: .init())
-                )
-            )
-        )
-    )
+    sceneDelegate.window?.rootViewController = DIContainer.shared.makeLoginViewController()
}

Consider implementing a DIContainer class for dependency injection:

// Example DIContainer implementation
final class DIContainer {
    static let shared = DIContainer()
    
    private init() {}
    
    func makeLoginViewController() -> UIViewController {
        let userDefaults = UserDefaultsStorage(jsonEncoder: .init(), jsonDecoder: .init())
        let userSessionRepository = UserSessionRepositoryImpl(userDefaults: userDefaults)
        
        return LoginViewController(
            viewModel: .init(
                updateFCMTokenUseCase: UpdateFCMTokenUseCase(repository: ProfileRepositoryImpl()),
                fetchUserAuthUseCase: FetchUserAuthUseCase(
                    loginRepository: LoginRepositoryImpl(),
                    userSessionRepository: userSessionRepository
                ),
                updateUserSessionUseCase: FetchUserInformationUseCase(
                    repository: userSessionRepository
                )
            )
        )
    }
}
Wable-iOS/Presentation/Profile/Withdrawal/Guide/ViewModel/WithdrawalGuideViewModel.swift (1)

38-44: isNextEnabled toggles blindly; consider explicit boolean source of truth.

Each tap simply inverts the flag, so double-tapping leaves it unchanged.
Using the checkbox’s actual on/off state (e.g., a Bool sent by the view) avoids desynchronisation after reuse or programmatic changes.

- input.checkbox
-     .sink { _ in isNextEnabledRelay.value.toggle() }
+ input.checkboxState     //  Driver<Bool>
+     .assign(to: \.value, on: isNextEnabledRelay)
Wable-iOS/Presentation/Profile/AlarmSetting/View/AlarmSettingViewController.swift (2)

29-32: Typo & naming consistency: openSettingbuttonopenSettingButton.

The lowercase “b” hurts readability and deviates from the project’s camel-case convention used elsewhere (backButton, navigationView).
Renaming avoids future autocomplete mistakes.


161-163: Spelling – appDidBecomActiveappDidBecomeActive.

Minor typo in selector/method name. While it compiles, fixing it prevents confusion for future maintainers.

Wable-iOS/Presentation/Profile/My/ViewModel/MyProfileViewModel.swift (1)

27-33: Parameter name typo (userinformationUseCase) reduces clarity.

userinformationUseCase (lower-case “i”) is easy to mis-read and differs from the property userSessionUseCase.
Rename to userInformationUseCase (or simply userSessionUseCase) to stay consistent.

Wable-iOS/Presentation/Profile/AccountInfo/View/AccountInfoViewController.swift (1)

116-133: Replace the unused indexPath parameter with _ to silence SwiftLint and improve clarity

indexPath is not referenced inside the registration closure; Swift-Lint is already warning about it.
Using _ makes the intent explicit and removes the need for a swiftlint:disable comment later.

-let cellRegistration = CellRegistration<AccountInfoCell, Item> { cell, indexPath, item in
+let cellRegistration = CellRegistration<AccountInfoCell, Item> { cell, _, item in
🧰 Tools
🪛 SwiftLint (0.57.0)

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

(unused_closure_parameter)

Wable-iOS/Presentation/Profile/Withdrawal/Reason/View/WithdrawalReasonViewController.swift (1)

120-132: Minor: mark the unused indexPath parameter as _

The closure does not use indexPath; replacing it with _ removes the Swift-Lint
warning and communicates intent more clearly.

-let cellRegistration = CellRegistration<WithdrawalReasonCell, Item> { cell, indexPath, item in
+let cellRegistration = CellRegistration<WithdrawalReasonCell, Item> { cell, _, item in
🧰 Tools
🪛 SwiftLint (0.57.0)

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

(unused_closure_parameter)

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

89-102: Silence Swift-Lint by discarding the unused indexPath

indexPath is not required inside this closure.

-let profileInfoCellRegistration = CellRegistration<ProfileInfoCell, UserProfile> { cell, indexPath, item in
+let profileInfoCellRegistration = CellRegistration<ProfileInfoCell, UserProfile> { cell, _, item in
🧰 Tools
🪛 SwiftLint (0.57.0)

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

(unused_closure_parameter)


103-119: Same issue for content cell registration

-let contentCellRegistration = CellRegistration<ContentCollectionViewCell, UserContent> {
-    cell, indexPath, item in
+let contentCellRegistration = CellRegistration<ContentCollectionViewCell, UserContent> {
+    cell, _, item in
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 113-113: TODOs should be resolved (추후 기능 연결)

(todo)


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

(unused_closure_parameter)


121-138: …and for the comment cell registration

-let commentCellRegistration = CellRegistration<CommentCollectionViewCell, UserComment> {
-    cell, indexPath, item in
+let commentCellRegistration = CellRegistration<CommentCollectionViewCell, UserComment> {
+    cell, _, item in
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 131-131: TODOs should be resolved (추후 기능 연결)

(todo)


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

(unused_closure_parameter)


113-132: Address the lingering TODOs to avoid shipping dead code paths

Both // TODO: 추후 기능 연결 markers indicate unfinished like-button logic.
Consider at least raising a tracking issue or implementing a stub handler
(showing a toast, etc.) before merging to keep the feature discoverable.

🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 113-113: TODOs should be resolved (추후 기능 연결)

(todo)


[Warning] 131-131: TODOs should be resolved (추후 기능 연결)

(todo)


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

(unused_closure_parameter)

Wable-iOS/Presentation/Profile/ProfileEditViewController.swift (5)

15-18: Resolve TODO and remove redundant initialisations

defaultImage is initialised with nil, which is the default for optionals, and the TODO comment indicates pending refactor work. Removing the explicit = nil keeps the code concise; consider also injecting the three use-cases via the initializer in the forthcoming refactor so the controller can be unit-tested without creating concrete repositories internally.

-    // MARK: Property
-    // TODO: 유즈케이스 리팩 후에 뷰모델 만들어 넘기기
-
-    private var defaultImage: String? = nil
+    // MARK: Property
+    // TODO: 유즈케이스 리팩터링 후 ViewModel 주입
+
+    private var defaultImage: String?
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 17-17: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 15-15: TODOs should be resolved (유즈케이스 리팩 후에 뷰모델 만들어 넘기기)

(todo)


64-65: Typo in section marker

PriviatePrivate for consistency with the other // MARK: headers.


146-176: Improve error handling & retain cycle safety in profile update

  1. The sink ignores the completion block—network or decoding errors will silently fail.
  2. withUnretained(self) is good, but consider using tryMap & catch to propagate UI errors.
  3. Pulling only the nickname from the text field discards any changes the user may have made to profileImageView; consider moving profile mutation logic to a dedicated view-model.

208-228: Nested subscriptions can be flattened

The two sink calls form a pyramid of doom and are hard to cancel independently. Using flatMap (or an async/await wrapper once the data layer supports it) keeps the Combine chain linear and makes cancellation via cancelBag more predictable.

userSessionUseCase.fetchActiveUserID()
    .compactMap { $0 }
    .flatMap(profileUseCase.execute(userID:))
    .receive(on: DispatchQueue.main)
    .sink {  }
    .store(in: cancelBag)

235-243: Handle errors and silence SwiftLint warning in image picker

The second parameter error is unused and currently produces a SwiftLint warning. Also, failing to load the image should notify the user.

-        results.first?.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in
-            guard let image = image as? UIImage else { return }
+        results.first?.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
+            guard let self = self else { return }
+            if let error = error {
+                // TODO: Present an alert instead of print
+                print("Image load failed: \(error)")
+                return
+            }
+            guard let image = image as? UIImage else { return }
🧰 Tools
🪛 SwiftLint (0.57.0)

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

(unused_closure_parameter)

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

203-203: Fix naming convention inconsistency.

The file userProfileUseCase.swift uses camelCase, which is inconsistent with the PascalCase naming convention used for other use case files like FetchUserProfileUseCase.swift. Swift file names should use PascalCase for consistency.

-DDED59782D78F1E500A0BEF1 /* userProfileUseCase.swift in Sources */,
+DDED59782D78F1E500A0BEF1 /* UserProfileUseCase.swift in Sources */,
📜 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 1c81d82 and 574b658.

⛔ Files ignored due to path filters (3)
  • Wable-iOS/Resource/Assets.xcassets/Icon/ic_dot.imageset/ic_dot.png is excluded by !**/*.png
  • Wable-iOS/Resource/Assets.xcassets/Icon/ic_dot.imageset/ic_dot@2x.png is excluded by !**/*.png
  • Wable-iOS/Resource/Assets.xcassets/Icon/ic_dot.imageset/ic_dot@3x.png is excluded by !**/*.png
📒 Files selected for processing (51)
  • Wable-iOS.xcodeproj/project.pbxproj (31 hunks)
  • Wable-iOS/App/SceneDelegate.swift (0 hunks)
  • Wable-iOS/Data/Mapper/ErrorMapper.swift (1 hunks)
  • Wable-iOS/Data/Mapper/ProfileMapper.swift (1 hunks)
  • Wable-iOS/Data/RepositoryImpl/ProfileRepositoryImpl.swift (1 hunks)
  • Wable-iOS/Domain/Entity/UserProfile.swift (1 hunks)
  • Wable-iOS/Domain/Enum/WithdrawalReason.swift (2 hunks)
  • Wable-iOS/Domain/RepositoryInterface/ProfileRepository.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Home/FetchUserContentListUseCase.swift (0 hunks)
  • Wable-iOS/Domain/UseCase/Onboarding/userProfileUseCase.swift (2 hunks)
  • Wable-iOS/Domain/UseCase/Profile/FetchAccountInfoUseCase.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Profile/FetchUserCommentListUseCase.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Profile/FetchUserContentListUseCase.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Profile/FetchUserProfileUseCase.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Profile/RemoveUserSessionUseCase.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Profile/WithdrawUseCase.swift (1 hunks)
  • Wable-iOS/Infra/Network/APIProvider.swift (1 hunks)
  • Wable-iOS/Infra/Network/TargetType/CommentTargetType.swift (1 hunks)
  • Wable-iOS/Infra/Network/TargetType/ContentTargetType.swift (2 hunks)
  • Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (0 hunks)
  • Wable-iOS/Presentation/Home/View/HomeViewController.swift (0 hunks)
  • Wable-iOS/Presentation/Onboarding/View/ProfileRegisterView.swift (2 hunks)
  • Wable-iOS/Presentation/Onboarding/ViewController/AgreementViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Onboarding/ViewController/ProfileRegisterViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/AccountInfo/Model/AccountInfoCellItem.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/AccountInfo/View/AccountInfoViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/AccountInfo/View/Cell/AccountInfoCell.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/AccountInfo/ViewModel/AccountInfoViewModel.swift (1 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/My/View/Subview/MyProfileView.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/My/ViewModel/MyProfileViewModel.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/ProfileEditViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/ProfileInfoCell.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/ProfileSegmentKind.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/ProfileSegmentedHeaderView.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/ProfileViewController.swift (0 hunks)
  • Wable-iOS/Presentation/Profile/ProfileViewItem.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/Withdrawal/Guide/View/Subview/WithdrawalGuideDescriptionView.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/Withdrawal/Guide/View/Subview/WithdrawalGuideView.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/Withdrawal/Guide/View/WithdrawalGuideViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/Withdrawal/Guide/ViewModel/WithdrawalGuideViewModel.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/Withdrawal/Reason/Model/WithdrawalReasonCellItem.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/Withdrawal/Reason/View/Cell/WithdrawalReasonCell.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/Withdrawal/Reason/View/WithdrawalReasonViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/Withdrawal/Reason/ViewModel/WithdrawalReasonViewModel.swift (1 hunks)
  • Wable-iOS/Presentation/TabBar/TabBarController.swift (1 hunks)
  • Wable-iOS/Presentation/Viewit/List/View/Cell/ViewitListCell.swift (1 hunks)
  • Wable-iOS/Presentation/WableComponent/Navigation/NavigationView.swift (1 hunks)
  • Wable-iOS/Resource/Assets.xcassets/Icon/ic_dot.imageset/Contents.json (1 hunks)
💤 Files with no reviewable changes (5)
  • Wable-iOS/App/SceneDelegate.swift
  • Wable-iOS/Presentation/Home/View/HomeViewController.swift
  • Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift
  • Wable-iOS/Presentation/Profile/ProfileViewController.swift
  • Wable-iOS/Domain/UseCase/Home/FetchUserContentListUseCase.swift
🧰 Additional context used
🧬 Code Graph Analysis (15)
Wable-iOS/Presentation/Profile/Withdrawal/Guide/View/Subview/WithdrawalGuideDescriptionView.swift (2)
Wable-iOS/Presentation/Profile/Withdrawal/Guide/View/Subview/WithdrawalGuideView.swift (1)
  • setupView (43-111)
Wable-iOS/Presentation/Helper/Extension/UIView+.swift (1)
  • addSubviews (32-36)
Wable-iOS/Infra/Network/TargetType/ContentTargetType.swift (1)
Wable-iOS/Presentation/Profile/My/ViewModel/MyProfileViewModel.swift (1)
  • fetchUserContentList (126-139)
Wable-iOS/Data/Mapper/ErrorMapper.swift (1)
Wable-iOS/Core/Logger/WableLogger.swift (1)
  • log (14-25)
Wable-iOS/Domain/UseCase/Profile/WithdrawUseCase.swift (2)
Wable-iOS/Data/RepositoryImpl/AccountRepositoryImpl.swift (1)
  • deleteAccount (20-31)
Wable-iOS/Data/Mapper/ErrorMapper.swift (1)
  • map (29-43)
Wable-iOS/Presentation/Profile/Withdrawal/Reason/View/Cell/WithdrawalReasonCell.swift (1)
Wable-iOS/Presentation/Helper/Extension/UIView+.swift (1)
  • addSubviews (32-36)
Wable-iOS/Domain/UseCase/Profile/FetchAccountInfoUseCase.swift (1)
Wable-iOS/Data/RepositoryImpl/ProfileRepositoryImpl.swift (1)
  • fetchAccountInfo (61-68)
Wable-iOS/Domain/UseCase/Profile/FetchUserContentListUseCase.swift (3)
Wable-iOS/Domain/UseCase/Profile/FetchUserCommentListUseCase.swift (1)
  • execute (22-28)
Wable-iOS/Domain/UseCase/Profile/FetchUserProfileUseCase.swift (1)
  • execute (22-30)
Wable-iOS/Presentation/Profile/My/ViewModel/MyProfileViewModel.swift (1)
  • fetchUserContentList (126-139)
Wable-iOS/Domain/RepositoryInterface/ProfileRepository.swift (1)
Wable-iOS/Data/RepositoryImpl/ProfileRepositoryImpl.swift (1)
  • fetchAccountInfo (61-68)
Wable-iOS/Presentation/Profile/ProfileSegmentedHeaderView.swift (2)
Wable-iOS/Presentation/Helper/Extension/UIView+.swift (1)
  • addSubviews (32-36)
Wable-iOS/Presentation/Helper/Extension/ConstraintMaker+.swift (1)
  • adjustedHeightEqualTo (45-48)
Wable-iOS/Presentation/Profile/AlarmSetting/View/AlarmSettingViewController.swift (4)
Wable-iOS/Presentation/Profile/AlarmSetting/ViewModel/AlarmSettingViewModel.swift (1)
  • checkAlarmAuthorization (15-20)
Wable-iOS/Presentation/Helper/Extension/UIView+.swift (1)
  • addSubviews (32-36)
Wable-iOS/Presentation/Helper/Extension/ConstraintMaker+.swift (1)
  • adjustedHeightEqualTo (45-48)
Wable-iOS/Core/Logger/WableLogger.swift (1)
  • log (14-25)
Wable-iOS/Presentation/Onboarding/View/ProfileRegisterView.swift (1)
Wable-iOS/Presentation/WableComponent/Button/WableButton.swift (1)
  • updateStyle (51-53)
Wable-iOS/Presentation/Profile/ProfileInfoCell.swift (2)
Wable-iOS/Presentation/Helper/Extension/UIView+.swift (1)
  • addSubviews (32-36)
Wable-iOS/Presentation/Helper/Extension/ConstraintMaker+.swift (1)
  • adjustedHeightEqualTo (45-48)
Wable-iOS/Presentation/Profile/AccountInfo/View/Cell/AccountInfoCell.swift (1)
Wable-iOS/Presentation/Helper/Extension/UIView+.swift (1)
  • addSubviews (32-36)
Wable-iOS/Domain/UseCase/Profile/RemoveUserSessionUseCase.swift (1)
Wable-iOS/Core/Logger/WableLogger.swift (1)
  • log (14-25)
Wable-iOS/Presentation/Profile/My/ViewModel/MyProfileViewModel.swift (5)
Wable-iOS/Domain/UseCase/Home/FetchUserInformationUseCase.swift (1)
  • fetchActiveUserInfo (28-31)
Wable-iOS/Domain/UseCase/Profile/RemoveUserSessionUseCase.swift (1)
  • removeUserSession (21-28)
Wable-iOS/Domain/UseCase/Profile/FetchUserCommentListUseCase.swift (1)
  • execute (22-28)
Wable-iOS/Domain/UseCase/Profile/FetchUserContentListUseCase.swift (1)
  • execute (22-28)
Wable-iOS/Domain/UseCase/Profile/FetchUserProfileUseCase.swift (1)
  • execute (22-30)
🪛 SwiftLint (0.57.0)
Wable-iOS/Presentation/Profile/AccountInfo/View/AccountInfoViewController.swift

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

(unused_closure_parameter)

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

[Warning] 73-73: Using ternary to call Void functions should be avoided

(void_function_in_ternary)

Wable-iOS/Domain/UseCase/Onboarding/userProfileUseCase.swift

[Error] 12-12: Type name 'userProfileUseCase' should start with an uppercase character

(type_name)

Wable-iOS/Presentation/Profile/Withdrawal/Reason/View/WithdrawalReasonViewController.swift

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

(unused_closure_parameter)

Wable-iOS/Presentation/Profile/My/View/MyProfileViewController.swift

[Warning] 113-113: TODOs should be resolved (추후 기능 연결)

(todo)


[Warning] 131-131: TODOs should be resolved (추후 기능 연결)

(todo)


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

(unused_closure_parameter)


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

(unused_closure_parameter)


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

(unused_closure_parameter)


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

(unused_closure_parameter)


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

(unused_closure_parameter)


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

(unused_closure_parameter)


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

(unused_closure_parameter)

Wable-iOS/Presentation/Profile/ProfileEditViewController.swift

[Warning] 17-17: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 30-30: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 15-15: TODOs should be resolved (유즈케이스 리팩 후에 뷰모델 만들어 넘기기)

(todo)


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

(unused_closure_parameter)

🔇 Additional comments (34)
Wable-iOS/Domain/Entity/UserProfile.swift (1)

12-12:

Details

✅ Verification successful

Good addition of Hashable conformance.

Adding the Hashable protocol conformance to UserProfile enables it to work in hash-based collections like Set and as dictionary keys, which is beneficial for state management in the new profile UI components.


🏁 Script executed:

#!/bin/bash
# Verify that the User type also conforms to Hashable, which is required for UserProfile to be properly Hashable

# Search for the User struct definition
echo "Checking User struct definition for Hashable conformance:"
rg -A 5 "struct User" --type swift

Length of output: 2587


Hashable Conformance Verified

Verified that the User struct in Wable-iOS/Domain/Entity/User.swift already conforms to Hashable, so UserProfile: Hashable is fully supported. No further changes required—good to merge.

• Wable-iOS/Domain/Entity/User.swift:

struct User: Identifiable, Hashable {  }

• Wable-iOS/Domain/Entity/UserProfile.swift:

struct UserProfile: Hashable {  }
Wable-iOS/Infra/Network/TargetType/CommentTargetType.swift (1)

28-28:

Details

✅ Verification successful

API endpoint path updated.

The endpoint path for fetching user comments has been simplified from "/v3/member/{memberID}/member-comments" to "/v3/member/{memberID}/comments". This change aligns with the newly introduced FetchUserCommentListUseCase.


🏁 Script executed:

#!/bin/bash
# Verify that the updated endpoint is used consistently across the codebase

# Check for any references to the old endpoint path
echo "Checking for any references to the old endpoint path:"
rg "member-comments" --type swift

# Check for references to the new endpoint in the associated use case
echo "Checking for references to the new endpoint in use cases:"
rg -A 5 "FetchUserCommentListUseCase" --type swift

Length of output: 3965


API endpoint path updated and verified
The endpoint for fetching user comments has been simplified from /v3/member/{memberID}/member-comments to /v3/member/{memberID}/comments, in line with the new FetchUserCommentListUseCase. I confirmed:

  • No references to the old /member-comments path remain.
  • The new /comments endpoint is used exclusively across the Swift codebase.

Approved.

Wable-iOS/Domain/Enum/WithdrawalReason.swift (1)

2-2: Good renaming and protocol addition.

The renaming from "AccountDeleteReason" to "WithdrawalReason" provides more user-friendly terminology while maintaining the same functionality. Adding the CaseIterable protocol is an excellent decision, as it enables easy iteration over all withdrawal reasons, which will be useful when displaying these options in UI components.

Also applies to: 12-12

Wable-iOS/Presentation/Profile/ProfileSegmentKind.swift (1)

10-13: Well-designed enum for segmented control.

The ProfileSegmentKind enum is well-structured with appropriate Int raw values (0, 1) that align perfectly with UISegmentedControl indices. The naming clearly indicates the purpose of each segment.

For even more flexibility, consider conforming to CaseIterable if you need to iterate through all segment options programmatically:

-enum ProfileSegmentKind: Int {
+enum ProfileSegmentKind: Int, CaseIterable {
    case content = 0
    case comment = 1
}
Wable-iOS/Resource/Assets.xcassets/Icon/ic_dot.imageset/Contents.json (1)

1-23: Properly configured image asset

The ic_dot image asset is correctly configured with all required scale variations (1x, 2x, 3x) for universal device support, following iOS asset catalog best practices.

Wable-iOS/Presentation/Profile/Withdrawal/Reason/Model/WithdrawalReasonCellItem.swift (1)

10-13: LGTM - Good implementation for cell data representation

This struct is well-designed for its purpose, conforming to Hashable for collection diffing support, and providing a clear model for representing withdrawal reasons with selection state.

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

24-24: Refactored use case naming looks good

The update from CreateUserProfileUseCase to userProfileUseCase aligns with the broader refactoring mentioned in the PR objectives, which aims to improve code readability through modern concurrency approaches.

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

68-69: Correctly updated pattern matching for additional parameter.

The fetchUserContentList case pattern matching now correctly handles the additional cursor parameter, ensuring the endpoint URL is properly constructed.


77-78: Query parameters updated to match new case format.

The pattern matching in the query property now properly extracts the cursor from the second parameter while ignoring the first parameter with an underscore. This correctly maintains the existing functionality while supporting the updated case structure.

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

37-49: Clean UI setup with proper constraints.

The view setup and constraint configuration are well-organized and follow good practices with SnapKit. The use of the addSubviews extension method keeps the code concise.

Wable-iOS/Presentation/Viewit/List/View/Cell/ViewitListCell.swift (3)

264-268: Well-implemented random profile image selection logic.

The array of profile images with random selection provides a good fallback when no profile image URL is available, ensuring the UI always displays something meaningful.


273-274: Good handling of empty/nil profile image URLs.

Setting a random profile image when URL is nil or empty is appropriate, and the early return prevents unnecessary code execution.


277-286: Smart implementation of special keyword handling for profile images.

The switch statement elegantly handles special case strings ("PURPLE", "GREEN", "BLUE") while defaulting to Kingfisher for remote images. This approach maintains consistency with the profile image handling pattern introduced elsewhere in the project.

Wable-iOS/Infra/Network/APIProvider.swift (1)

73-98: Excellent async/await implementation of the request method.

The async implementation correctly wraps the existing callback-based method using withCheckedThrowingContinuation, providing a modern alternative without breaking existing functionality. The error handling is comprehensive, categorizing different error types appropriately.

A few observations:

  1. The method properly decodes the response and validates it
  2. Error handling is thorough with specific handling for decoding errors
  3. The implementation aligns with Swift concurrency best practices

This change supports the PR's objective of moving from Combine to async/await for improved readability and maintainability.

Wable-iOS/Presentation/Profile/AlarmSetting/ViewModel/AlarmSettingViewModel.swift (1)

12-14: Clean ViewModel implementation with good encapsulation.

The view model correctly uses @Published with private(set) to allow external observation of the authorization state while preventing external modification. This is a good practice for maintaining proper data flow.

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

61-68: Good async/await implementation replacing Combine-based approach.

The repository method effectively uses the new async/await pattern to fetch account information, aligning with the PR's objective to improve code readability. The error handling is clean and properly uses the new ErrorMapper to standardize error mapping.

This implementation:

  1. Correctly uses async throws for error propagation
  2. Properly calls the async version of the provider's request method
  3. Maps the DTO response to a domain model
  4. Handles errors through the centralized ErrorMapper

The change demonstrates a good migration pattern from reactive programming to Swift concurrency.

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

11-13: Clean protocol definition.

The protocol follows good design principles with a simple, clear async interface that matches the PR's objective of moving from Combine to async/await.


15-25: Proper implementation of async pattern.

The implementation correctly follows the async/await pattern, delegating to the repository layer. This aligns with the PR objective of improving code readability by moving away from Combine.

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

11-13: Well-defined protocol with clear interface.

The protocol provides a clean contract with a single method that follows the use case pattern, returning a publisher for asynchronous operation.


15-31:

Details

❓ Verification inconclusive

Consider using async/await instead of Combine.

According to the PR objectives, this PR aims to introduce async/await syntax to improve code readability. Consider updating this implementation to use async/await instead of Combine publishers.

protocol FetchUserProfileUseCase {
-    func execute(userID: Int) -> AnyPublisher<UserProfile?, WableError>
+    func execute(userID: Int) async throws -> UserProfile?
}

final class FetchUserProfileUseCaseImpl: FetchUserProfileUseCase {
    private let repository: ProfileRepository
    
    init(repository: ProfileRepository) {
        self.repository = repository
    }
    
-    func execute(userID: Int) -> AnyPublisher<UserProfile?, WableError> {
+    func execute(userID: Int) async throws -> UserProfile? {
        guard userID > .zero else {
-            return .fail(.notFoundMember)
+            throw WableError.notFoundMember
        }
        
-        return repository.fetchUserProfile(memberID: userID)
-            .map { $0 }
-            .eraseToAnyPublisher()
+        return try await repository.fetchUserProfile(memberID: userID)
    }
}

🏁 Script executed:

#!/bin/bash
# Check if the repository layer supports async/await
rg "fetchUserProfile.*async" --type swift

Length of output: 43


Verify ProfileRepository async/await support before refactoring UseCase

It looks like there isn’t an async throws signature for fetchUserProfile(memberID:) in the ProfileRepository protocol, so switching the use case to async/await will break its dependency. Please confirm and update the repository layer before refactoring:

• Update the protocol:

-protocol ProfileRepository {
-    func fetchUserProfile(memberID: Int) -> AnyPublisher<UserProfile?, WableError>
+protocol ProfileRepository {
+    func fetchUserProfile(memberID: Int) async throws -> UserProfile?
}

• Ensure all conforming classes implement the new async API, either by calling a native async endpoint or by bridging the existing Combine publisher (e.g. using withCheckedThrowingContinuation or publisher’s .values).

Once the repository is async-ready, you can merge the diff below into FetchUserProfileUseCase:

 protocol FetchUserProfileUseCase {
-    func execute(userID: Int) -> AnyPublisher<UserProfile?, WableError>
+    func execute(userID: Int) async throws -> UserProfile?
 }

 final class FetchUserProfileUseCaseImpl: FetchUserProfileUseCase {
     private let repository: ProfileRepository

     init(repository: ProfileRepository) {
         self.repository = repository
     }

-    func execute(userID: Int) -> AnyPublisher<UserProfile?, WableError> {
+    func execute(userID: Int) async throws -> UserProfile? {
         guard userID > .zero else {
-            return .fail(.notFoundMember)
+            throw WableError.notFoundMember
         }

-        return repository.fetchUserProfile(memberID: userID)
-            .map { $0 }
-            .eraseToAnyPublisher()
+        return try await repository.fetchUserProfile(memberID: userID)
     }
 }
Wable-iOS/Domain/UseCase/Profile/FetchUserCommentListUseCase.swift (1)

11-13: Clear protocol interface with pagination support.

The protocol is well-defined and supports pagination through the last commentID parameter.

Wable-iOS/Presentation/Profile/ProfileSegmentedHeaderView.swift (3)

15-17: Good use of then pattern for UI initialization.

Using the Then library provides a clean, declarative way to configure the segmented control.


19-19: Well-designed callback pattern.

The use of a closure for segment changes instead of a delegate pattern is a good choice for this simple use case.


33-49: Clean UI setup with proper constraints.

The view setup is well-organized with clear separation of UI element creation, hierarchy setup, and constraints.

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

13-39: LGTM! Well-structured UI component.

The implementation follows project standards with clear separation of UI elements and proper configuration.

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

54-85: LGTM! Clean layout implementation.

The view setup and constraints are well-organized using SnapKit, following project standards.

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

128-136: LGTM! Good implementation of confirmation flow.

The implementation of the withdrawal confirmation sheet is well done, using the appropriate UI components and handling the action properly.

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

33-33: File renaming for better semantics.

The change from AccountDeleteReason.swift to WithdrawalReason.swift improves the clarity of terminology. "Withdrawal" better represents the user action of leaving the service compared to "AccountDelete".


245-250: Well-structured profile use cases added.

These profile-related use cases follow clean architecture principles by encapsulating specific business logic operations. The separation into distinct use cases (Fetch Profile, Comments, Contents, AccountInfo, and Withdraw) promotes single responsibility and maintainability.


155-155: Well-organized UI components for profile feature.

The implementation follows a proper MVVM structure with separate files for:

  1. Views (MyProfileViewController, ProfileEditViewController)
  2. Models (ProfileViewItem, ProfileSegmentKind)
  3. Supporting UI components (ProfileSegmentedHeaderView, ProfileInfoCell)

This organization makes the code more maintainable and testable.

Also applies to: 242-244, 309-315


301-308: Comprehensive implementation for account withdrawal flow.

The withdrawal feature is thoroughly implemented with proper separation of concerns:

  1. Different views for reason selection and confirmation
  2. Models for withdrawal reasons
  3. Supporting UI components for explanation
  4. View models for business logic

This structure ensures a good user experience for a critical flow.


321-324: Well-structured AccountInfo feature implementation.

The AccountInfo feature follows clean architecture with separate files for view, view model, and model. The cell-based approach allows for flexible display of different types of account information.


242-243: Addition of settings-related features.

The implementation of AlarmSettingViewController and AlarmSettingViewModel following MVVM pattern allows users to manage their notification preferences, which is a standard feature for profile sections in modern apps.


1617-1713: Well-organized profile feature structure.

The profile section is well-organized into logical sub-features:

  1. My Profile - for viewing and managing personal profile
  2. AlarmSetting - for notification preferences
  3. AccountInfo - for account details
  4. Withdrawal - for account termination flow

This organization maintains clean separation of concerns and makes the codebase more maintainable.

Comment on lines +61 to +62

rootView.configureView()
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

Added explicit view configuration call.

The addition of rootView.configureView() ensures that the view is properly configured after setting the default image. This change is aligned with the profile view configuration patterns introduced elsewhere in the onboarding flow.


🏁 Script executed:

#!/bin/bash
# Check how configureView is implemented in the ProfileRegisterView

echo "Looking for ProfileRegisterView implementation:"
fd ProfileRegisterView --type file --exec cat {} \; | rg -A 10 "func configureView" --context 10

# Check for potential issues with defaultImageList
echo "Checking for potential issues with defaultImageList access:"
fd ProfileRegisterView --type file --exec cat {} \; | rg "defaultImageList" --context 5

Length of output: 2006


Swap configureView() and defaultImage assignment in setupView

The explicit call to rootView.configureView() shuffles defaultImageList (via configureDefaultImage()), so you should assign the view controller’s defaultImage after calling configureView(), just as you do in switchButtonDidTap(). This guarantees the defaultImage property matches the image shown in the UI.

• In ProfileRegisterViewController.swiftsetupView():

 func setupView() {
     view.addSubview(rootView)
-    defaultImage = rootView.defaultImageList[0].uppercased
-    rootView.configureView()
+    rootView.configureView()
+    defaultImage = rootView.defaultImageList[0].uppercased
 }

Alternatively, you can call rootView.configureDefaultImage() directly before setting defaultImage.

📝 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
rootView.configureView()
func setupView() {
view.addSubview(rootView)
rootView.configureView()
defaultImage = rootView.defaultImageList[0].uppercased
}
🤖 Prompt for AI Agents
In
Wable-iOS/Presentation/Onboarding/ViewController/ProfileRegisterViewController.swift
around lines 61 to 62, the call to rootView.configureView() should be moved
before assigning the view controller's defaultImage property in setupView(),
because configureView() modifies defaultImageList and affects the defaultImage
shown. To fix this, reorder the code so that configureView() is called first,
then assign defaultImage, or alternatively call rootView.configureDefaultImage()
directly before setting defaultImage to ensure consistency between the property
and the UI.

Comment on lines +11 to +13
protocol WithdrawUseCase {
func execute(reasons: [WithdrawalReason]) -> AnyPublisher<Bool, 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 using async/await for consistency.

This use case uses Combine publishers while FetchAccountInfoUseCase uses async/await. According to the PR objectives, you're moving toward async/await for better readability. Consider making this consistent.

protocol WithdrawUseCase {
-    func execute(reasons: [WithdrawalReason]) -> AnyPublisher<Bool, WableError>
+    func execute(reasons: [WithdrawalReason]) async throws -> Bool
}
📝 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
protocol WithdrawUseCase {
func execute(reasons: [WithdrawalReason]) -> AnyPublisher<Bool, WableError>
}
protocol WithdrawUseCase {
func execute(reasons: [WithdrawalReason]) async throws -> Bool
}
🤖 Prompt for AI Agents
In Wable-iOS/Domain/UseCase/Profile/WithdrawUseCase.swift around lines 11 to 13,
the WithdrawUseCase protocol currently uses Combine's AnyPublisher for its
execute method, but to maintain consistency with FetchAccountInfoUseCase and
improve readability, refactor the execute method to use async/await by changing
its signature to an async function that returns a Bool and throws WableError.

Comment on lines +22 to +26
func execute(reasons: [WithdrawalReason]) -> AnyPublisher<Bool, WableError> {
return repository.deleteAccount(reason: reasons.map { $0.rawValue })
.map { _ in true }
.eraseToAnyPublisher()
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Implement async method consistently with PR objectives.

The implementation should follow the async/await pattern for consistency with other use cases and to align with the PR's focus on improving code readability.

-func execute(reasons: [WithdrawalReason]) -> AnyPublisher<Bool, WableError> {
-    return repository.deleteAccount(reason: reasons.map { $0.rawValue })
-        .map { _ in true }
-        .eraseToAnyPublisher()
+async func execute(reasons: [WithdrawalReason]) async throws -> Bool {
+    try await repository.deleteAccount(reason: reasons.map { $0.rawValue })
+    return true
}

Note: This assumes that the AccountRepository.deleteAccount method would also be updated to use async/await. If that method hasn't been updated yet, you might need to create an adapter pattern or update both simultaneously.

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

🤖 Prompt for AI Agents
In Wable-iOS/Domain/UseCase/Profile/WithdrawUseCase.swift around lines 22 to 26,
the execute method currently returns a Combine publisher but should be
refactored to use async/await for consistency with the PR objectives. Change the
method signature to async and have it call an async version of
repository.deleteAccount, awaiting its result and returning a Bool directly.
Ensure that repository.deleteAccount is also updated to support async/await or
create an adapter to bridge between Combine and async/await if needed.

Comment on lines +22 to +28
func execute(for userID: Int, last commentID: Int) -> AnyPublisher<[UserComment], WableError> {
guard userID > .zero else {
return .fail(.notFoundMember)
}

return repository.fetchUserCommentList(memberID: userID, cursor: commentID)
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Consider using async/await instead of Combine.

According to the PR objectives, this PR aims to introduce async/await syntax. Consider updating this implementation to use async/await instead of Combine publishers for better code readability and alignment with the PR goals.

protocol FetchUserCommentListUseCase {
-    func execute(for userID: Int, last commentID: Int) -> AnyPublisher<[UserComment], WableError>
+    func execute(for userID: Int, last commentID: Int) async throws -> [UserComment]
}

final class FetchUserCommentListUseCaseImpl: FetchUserCommentListUseCase {
    private let repository: CommentRepository
    
    init(repository: CommentRepository) {
        self.repository = repository
    }
    
-    func execute(for userID: Int, last commentID: Int) -> AnyPublisher<[UserComment], WableError> {
+    func execute(for userID: Int, last commentID: Int) async throws -> [UserComment] {
        guard userID > .zero else {
-            return .fail(.notFoundMember)
+            throw WableError.notFoundMember
        }

-        return repository.fetchUserCommentList(memberID: userID, cursor: commentID)
+        return try await repository.fetchUserCommentList(memberID: userID, cursor: commentID)
    }
}

🏁 Script executed:

#!/bin/bash
# Check if the repository layer supports async/await
rg "fetchUserCommentList.*async" --type swift

Length of output: 47


Adopt async/await across use-case and repository

To align with this PR’s async/await goal, update both the use-case and its repository dependency:

• In FetchUserCommentListUseCase (protocol + implementation), change

func execute(for userID: Int, last commentID: Int) -> AnyPublisher<[UserComment], WableError>

to

func execute(for userID: Int, last commentID: Int) async throws -> [UserComment]

and throw on userID <= 0, then call

try await repository.fetchUserCommentList(memberID: userID, cursor: commentID)

• In CommentRepository (and its conforming types), change

func fetchUserCommentList(memberID: Int, cursor: Int) -> AnyPublisher<[UserComment], WableError>

to

func fetchUserCommentList(memberID: Int, cursor: Int) async throws -> [UserComment]

and update all implementations accordingly.

// Protocol
-protocol FetchUserCommentListUseCase {
-    func execute(for userID: Int, last commentID: Int) -> AnyPublisher<[UserComment], WableError>
-}
+protocol FetchUserCommentListUseCase {
+    func execute(for userID: Int, last commentID: Int) async throws -> [UserComment]
+}

// Implementation
-final class FetchUserCommentListUseCaseImpl: FetchUserCommentListUseCase {
+final class FetchUserCommentListUseCaseImpl: FetchUserCommentListUseCase {
     private let repository: CommentRepository
     
     init(repository: CommentRepository) {
         self.repository = repository
     }
     
-    func execute(for userID: Int, last commentID: Int) -> AnyPublisher<[UserComment], WableError> {
+    func execute(for userID: Int, last commentID: Int) async throws -> [UserComment] {
         guard userID > .zero else {
-            return .fail(.notFoundMember)
+            throw WableError.notFoundMember
         }
-
-        return repository.fetchUserCommentList(memberID: userID, cursor: commentID)
+        return try await repository.fetchUserCommentList(memberID: userID, cursor: commentID)
     }
 }
// In CommentRepository.swift
-protocol CommentRepository {
-    func fetchUserCommentList(memberID: Int, cursor: Int) -> AnyPublisher<[UserComment], WableError>
-    // …
-}
+protocol CommentRepository {
+    func fetchUserCommentList(memberID: Int, cursor: Int) async throws -> [UserComment]
+    // …
+}
🤖 Prompt for AI Agents
In Wable-iOS/Domain/UseCase/Profile/FetchUserCommentListUseCase.swift around
lines 22 to 28, refactor the execute function to use async/await instead of
Combine. Change the function signature to async throws returning [UserComment],
throw an error if userID is less than or equal to zero, and call the
repository's fetchUserCommentList method using try await. Also update the
CommentRepository protocol and its implementations to use async throws for
fetchUserCommentList to maintain consistency.

Comment on lines +82 to +102
userinformationUseCase: FetchUserInformationUseCase(
repository: UserSessionRepositoryImpl(
userDefaults: UserDefaultsStorage(jsonEncoder: .init(), jsonDecoder: .init())
)
),
fetchUserProfileUseCase: FetchUserProfileUseCaseImpl(
repository: ProfileRepositoryImpl()
),
fetchUserCommentListUseCase: FetchUserCommentListUseCaseImpl(
repository: CommentRepositoryImpl()
),
fetchUserContentListUseCase: FetchUserContentUseCaseImpl(
repository: ContentRepositoryImpl()
),
removeUserSessionUseCase: RemoveUserSessionUseCaseImpl(
repository: UserSessionRepositoryImpl(
userDefaults: UserDefaultsStorage(
jsonEncoder: .init(), jsonDecoder: .init()
)
)
)
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

Verify the integration with async/await.

According to the PR objectives, this PR aims to introduce async/await syntax to improve code readability. Ensure that the view model properly handles the transition from Combine-based use cases to async/await-based ones.


🏁 Script executed:

#!/bin/bash
# Check how MyProfileViewModel handles asynchronous operations
rg -A 10 "class MyProfileViewModel" --type swift

Length of output: 1415


🏁 Script executed:

#!/bin/bash
# Check for async/await usage and Combine subscriptions in MyProfileViewModel
rg "await" -n Wable-iOS/Presentation/Profile/My/ViewModel/MyProfileViewModel.swift
rg "Task" -n Wable-iOS/Presentation/Profile/My/ViewModel/MyProfileViewModel.swift
rg "\.sink" -n Wable-iOS/Presentation/Profile/My/ViewModel/MyProfileViewModel.swift

Length of output: 393


Migrate MyProfileViewModel to async/await
The view model still uses Combine’s .sink on lines 65 and 78 and has no await or Task calls. To complete the PR objective you’ll need to refactor these subscriptions to call the new async use cases directly.

• Remove .sink on:
– Line 65 (updating selectedSegment)
– Line 78 (sending profileViewItemRelay)
• Wrap each call in a Task and use await on the use‐case’s async method (for example, let profile = try await fetchUserProfileUseCase.execute())
• Handle errors and cancellation as needed, and send results to your relays

🤖 Prompt for AI Agents
In Wable-iOS/Presentation/Profile/My/ViewModel/MyProfileViewModel.swift around
lines 60 to 90, refactor the Combine `.sink` subscriptions on lines 65 and 78 to
use async/await instead. Remove the `.sink` calls and wrap the asynchronous use
case calls inside `Task` blocks, using `await` to call the use case methods.
Handle any errors or cancellation appropriately within the `Task`, and update
the relays with the results instead of using Combine subscriptions.

Comment on lines +70 to +79
.flatMap { owner, _ in
return owner.fetchUserProfile()
.combineLatest(
owner.fetchUserContentList(cursor: Constant.initialCursor),
owner.fetchUserCommentList(cursor: Constant.initialCursor)
)
}
.map { ProfileViewItem(profileInfo: $0, content: $1, comment: $2) }
.sink { [weak self] in self?.profileViewItemRelay.send($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.

⚠️ Potential issue

Incorrect tuple destructuring after combineLatest causes build failure.

combineLatest with three publishers emits a single tuple, so $0, $1, $2 reference elements of that tuple, not separate parameters.
Unpack the tuple explicitly:

- .map { ProfileViewItem(profileInfo: $0, content: $1, comment: $2) }
+ .map { (profile, content, comment) in
+     ProfileViewItem(profileInfo: profile,
+                     content: content,
+                     comment: comment)
+ }
📝 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
.flatMap { owner, _ in
return owner.fetchUserProfile()
.combineLatest(
owner.fetchUserContentList(cursor: Constant.initialCursor),
owner.fetchUserCommentList(cursor: Constant.initialCursor)
)
}
.map { ProfileViewItem(profileInfo: $0, content: $1, comment: $2) }
.sink { [weak self] in self?.profileViewItemRelay.send($0) }
.store(in: cancelBag)
.flatMap { owner, _ in
return owner.fetchUserProfile()
.combineLatest(
owner.fetchUserContentList(cursor: Constant.initialCursor),
owner.fetchUserCommentList(cursor: Constant.initialCursor)
)
}
.map { (profile, content, comment) in
ProfileViewItem(
profileInfo: profile,
content: content,
comment: comment
)
}
.sink { [weak self] in self?.profileViewItemRelay.send($0) }
.store(in: cancelBag)
🤖 Prompt for AI Agents
In Wable-iOS/Presentation/Profile/My/ViewModel/MyProfileViewModel.swift around
lines 70 to 79, the code incorrectly treats the output of combineLatest as
separate parameters instead of a single tuple. To fix this, explicitly
destructure the tuple emitted by combineLatest in the map closure by unpacking
it into three variables, then use those variables to create the ProfileViewItem.
This will resolve the build failure caused by incorrect tuple handling.

Comment on lines +36 to +55
input.checkbox
.sink { reason in
guard let index = itemsRelay.value.firstIndex(where: { $0.reason == reason }) else { return }

var item = itemsRelay.value[index]
item.isSelected.toggle()
itemsRelay.value[index] = item

if item.isSelected {
selectedReasonsRelay.value.insert(reason)
} else {
selectedReasonsRelay.value.remove(reason)
}
}
.store(in: cancelBag)

selectedReasonsRelay
.map { !$0.isEmpty }
.sink { isNextEnabledRelay.send($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

Selection UI will not refresh because the relay isn’t notified after mutation

After toggling item.isSelected, the mutated array is assigned back to
itemsRelay.value without a subsequent send (or similar) call.
Unless CurrentValueRelay automatically publishes on didSet, the diffable
data-source in the view controller will never receive an update, so check-boxes
won’t visually toggle and isNextEnabled may stay stale.

itemsRelay.value[index] = item
+// Notify subscribers about the mutated array
+itemsRelay.send(itemsRelay.value)

Do the same after the selectedReasonsRelay mutation if its setter is passive.

📝 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
input.checkbox
.sink { reason in
guard let index = itemsRelay.value.firstIndex(where: { $0.reason == reason }) else { return }
var item = itemsRelay.value[index]
item.isSelected.toggle()
itemsRelay.value[index] = item
if item.isSelected {
selectedReasonsRelay.value.insert(reason)
} else {
selectedReasonsRelay.value.remove(reason)
}
}
.store(in: cancelBag)
selectedReasonsRelay
.map { !$0.isEmpty }
.sink { isNextEnabledRelay.send($0) }
.store(in: cancelBag)
input.checkbox
.sink { reason in
guard let index = itemsRelay.value.firstIndex(where: { $0.reason == reason }) else { return }
var item = itemsRelay.value[index]
item.isSelected.toggle()
- itemsRelay.value[index] = item
+ itemsRelay.value[index] = item
+ // Notify subscribers about the mutated array
+ itemsRelay.send(itemsRelay.value)
if item.isSelected {
selectedReasonsRelay.value.insert(reason)
} else {
selectedReasonsRelay.value.remove(reason)
}
}
.store(in: cancelBag)
selectedReasonsRelay
.map { !$0.isEmpty }
.sink { isNextEnabledRelay.send($0) }
.store(in: cancelBag)
🤖 Prompt for AI Agents
In
Wable-iOS/Presentation/Profile/Withdrawal/Reason/ViewModel/WithdrawalReasonViewModel.swift
between lines 36 and 55, after mutating itemsRelay.value by toggling
item.isSelected and updating the array, explicitly call a method like send() or
assign the updated array back to itemsRelay to notify subscribers of the change
so the UI refreshes properly. Similarly, after modifying
selectedReasonsRelay.value by inserting or removing reasons, ensure you trigger
a send or equivalent notification to update any observers and keep the UI state
consistent.

Comment on lines 18 to 30
private let profileUseCase = userProfileUseCase(repository: ProfileRepositoryImpl())
private let nicknameUseCase = FetchNicknameDuplicationUseCase(repository: AccountRepositoryImpl())
private let userSessionUseCase = FetchUserInformationUseCase(
repository: UserSessionRepositoryImpl(
userDefaults: UserDefaultsStorage(
jsonEncoder: JSONEncoder(),
jsonDecoder: JSONDecoder()
)
)
)
private let cancelBag = CancelBag()

private var sessionProfile: UserProfile? = nil
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Favour dependency injection for testability

Creating concrete RepositoryImpl instances inside the view-controller tightly couples UI to data-layer details and makes unit testing cumbersome. Pass the three use-cases (or a single view-model) through the initializer instead.

init(
    profileUseCase: UserProfileUseCaseProtocol,
    nicknameUseCase: FetchNicknameDuplicationUseCaseProtocol,
    userSessionUseCase: FetchUserInformationUseCaseProtocol
) {  }
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 30-30: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

🤖 Prompt for AI Agents
In Wable-iOS/Presentation/Profile/ProfileEditViewController.swift around lines
18 to 30, the view controller directly instantiates concrete repository
implementations, causing tight coupling and hindering testability. Refactor by
removing these direct instantiations and instead add an initializer that accepts
the three use-case instances (or a single view-model) as parameters conforming
to their protocols. Assign these passed-in use-cases to the private properties,
enabling dependency injection and easier unit testing.

Comment on lines +107 to +111
@objc func switchButtonDidTap() {
rootView.configureDefaultImage()
defaultImage = rootView.defaultImageList[0].uppercased
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Compile-time error – call uppercased()

String.uppercased is a method, not a property; omitting the parentheses results in “reference to member ‘uppercased’ cannot be used on value of type ‘String’”.

-        defaultImage = rootView.defaultImageList[0].uppercased
+        defaultImage = rootView.defaultImageList[0].uppercased()
🤖 Prompt for AI Agents
In Wable-iOS/Presentation/Profile/ProfileEditViewController.swift around lines
107 to 111, the code incorrectly uses `uppercased` without parentheses, causing
a compile-time error. Fix this by adding parentheses to call the method as
`uppercased()`, ensuring the string is properly converted to uppercase.

Comment on lines +130 to +144
guard let text = rootView.nickNameTextField.text else { return }

nicknameUseCase.execute(nickname: text)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] completion in
let condition = completion == .finished

self?.rootView.conditiionLabel.text = condition ? "사용 가능한 닉네임입니다." : "이미 사용 중인 닉네임입니다."
self?.rootView.conditiionLabel.textColor = condition ? .success : .error
self?.rootView.nextButton.isUserInteractionEnabled = condition
self?.rootView.nextButton.updateStyle(condition ? .primary : .gray)
}, receiveValue: { _ in
})
.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

Incorrect success/failure logic when checking nickname duplication

Using completion == .finished treats every non-error path as “nickname usable”, which is fragile—network errors would incorrectly mark a nickname as taken. Treat Subscribers.Completion.failure explicitly and surface an error state to the user.

-            .sink(receiveCompletion: { [weak self] completion in
-                let condition = completion == .finished
+            .sink(receiveCompletion: { [weak self] completion in
+                let condition: Bool
+                switch completion {
+                case .finished:
+                    condition = true
+                case .failure:
+                    self?.rootView.conditiionLabel.text = "네트워크 오류가 발생했어요."
+                    self?.rootView.conditiionLabel.textColor = .error
+                    condition = false
+                }

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

🤖 Prompt for AI Agents
In Wable-iOS/Presentation/Profile/ProfileEditViewController.swift around lines
130 to 144, the current logic treats any completion that is not a failure as a
successful nickname check, which is incorrect. Update the sink's
receiveCompletion closure to explicitly handle the failure case by checking if
completion is a failure and setting the UI to indicate the nickname is taken or
an error occurred. Only when completion is .finished should the UI show the
nickname as available. This ensures network errors or other failures do not
incorrectly mark the nickname as usable.

Copy link
Member

@youz2me youz2me left a comment

Choose a reason for hiding this comment

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

룩굿투미. 고생하셨습니다 (_ _)

Comment on lines +73 to +98
func request<D: Decodable>(
_ target: Target,
for type: D.Type
) async throws -> D {
let response = try await withCheckedThrowingContinuation { continuation in
self.request(target) { result in
switch result {
case .success(let moyaResponse):
continuation.resume(returning: moyaResponse)
case .failure(let error):
continuation.resume(throwing: NetworkError.unknown(error))
}
}
}

do {
let baseResponse = try jsonDecoder.decode(BaseResponse<D>.self, from: response.data)
return try validateResponse(baseResponse)
} catch let decodingError as DecodingError {
throw NetworkError.decodedError(decodingError)
} catch let error as NetworkError {
throw error
} catch {
throw NetworkError.unknown(error)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

👍

switch self {
case .fetchUserCommentList(memberID: let memberID, _):
return "/v3/member/\(memberID)/member-comments"
return "/v3/member/\(memberID)/comments"
Copy link
Member

Choose a reason for hiding this comment

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

ㅎㅎ ... 고생하셨씁니다

Comment on lines +214 to +227
switch profileImageURL.absoluteString {
case "PURPLE":
profileImageView.image = .imgProfilePurple
case "GREEN":
profileImageView.image = .imgProfileGreen
case "BLUE":
profileImageView.image = .imgProfileBlue
default:
profileImageView.kf.setImage(
with: profileImageURL,
placeholder: [UIImage.imgProfilePurple, UIImage.imgProfileBlue, UIImage.imgProfileGreen].randomElement()
)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

이것도 공통되는 로직이라 리팩토링할 때 한번 빼보면 좋을 것 같습니닷.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🔭 Outside diff range comments (1)
Wable-iOS/Domain/UseCase/Onboarding/UserProfileUseCase.swift (1)

20-34: 🛠️ Refactor suggestion

execute overloads are ambiguous and difficult to evolve

  1. Two overloads share the same base name execute; at every call-site you have to rely on the compiler’s contextual type inference to decide which one you meant.
    • Readability suffers (“what does execute actually do here?”).
    • Future additions will quickly break source-compatibility.

  2. The update variant exposes 6 optionals. Call-sites will end up passing long sequences of nil, hurting both clarity and safety.

Suggested approach (sketch):

 final class UserProfileUseCase {
   …
-  func execute(profile: UserProfile? = nil,
-               isPushAlarmAllowed: Bool? = nil,
-               isAlarmAllowed: Bool? = nil,
-               image: UIImage? = nil,
-               defaultProfileType: String? = nil) -> AnyPublisher<Void, WableError> {
+  func update(_ command: UpdateUserProfileCommand) -> AnyPublisher<Void, WableError> {
     return repository.updateUserProfile(
-        profile: profile,
-        isPushAlarmAllowed: isPushAlarmAllowed,
-        isAlarmAllowed: isAlarmAllowed,
-        image: image,
+        profile: command.profile,
+        isPushAlarmAllowed: command.isPushAlarmAllowed,
+        isAlarmAllowed: command.isAlarmAllowed,
+        image: command.image,
         fcmToken: repository.fetchFCMToken(),
-        defaultProfileType: defaultProfileType
+        defaultProfileType: command.defaultProfileType
     )
-  }
-
-  func execute(userID: Int) -> AnyPublisher<UserProfile, WableError> {
+      .eraseToAnyPublisher()
+  }
+
+  func fetch(userID: Int) -> AnyPublisher<UserProfile, WableError> {
     return repository.fetchUserProfile(memberID: userID)
   }
 }
+
+struct UpdateUserProfileCommand {
+  var profile: UserProfile?
+  var isPushAlarmAllowed: Bool?
+  var isAlarmAllowed: Bool?
+  var image: UIImage?
+  var defaultProfileType: String?
+}

Benefits:
• Call-sites read like profileUseCase.update(cmd) or profileUseCase.fetch(id:).
• Adding fields becomes non-breaking.
• Encapsulating the arguments opens the door for validation logic inside the command.

♻️ Duplicate comments (3)
Wable-iOS/Presentation/Profile/ProfileEditViewController.swift (3)

18-27: Tight coupling to concrete repositories – prior feedback still applies

The view-controller instantiates ProfileRepositoryImpl, AccountRepositoryImpl, and UserSessionRepositoryImpl directly. This hinders unit testing and breaks the dependency-inversion principle. Previous review already pointed this out; consider constructor injection of protocol-typed use-cases or a view-model.


107-110: Compile-time error – missing () on uppercased

String.uppercased is a method. Without the parentheses the code won’t compile.

-        defaultImage = rootView.defaultImageList[0].uppercased
+        defaultImage = rootView.defaultImageList[0].uppercased()

129-141: Nickname duplication logic treats any non-error as “usable”

Using completion == .finished assumes a network error means “nickname already in use”, which is the opposite of what you want. Explicitly distinguish .failure and surface an error to the user.

🧹 Nitpick comments (4)
Wable-iOS/Domain/UseCase/Onboarding/UserProfileUseCase.swift (1)

21-30: Publisher returned without eraseToAnyPublisher() leaks implementation detail

Returning the raw publisher you get from the repository means callers now depend on its concrete type. If the repository later changes (e.g. switches from PassthroughSubject to Future) you will break ABI.

Add .eraseToAnyPublisher() before returning.

-        )
+        )
+        .eraseToAnyPublisher()

This keeps the use-case API stable.

Wable-iOS/Presentation/Profile/ProfileEditViewController.swift (3)

17-18: Redundant = nil initialisation

Swift initialises optionals to nil by default. The assignment is unnecessary and triggers a SwiftLint warning.

-private var defaultImage: String? = nil
+private var defaultImage: String?
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 17-17: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


218-225: Unused completion parameter – replace with _

SwiftLint flags this closure parameter as unused.

-                    .sink { _ in } receiveValue: { [weak self] profile in
+                    .sink { _ in } receiveValue: { [weak self] profile in

(or simply supply only the receiveValue closure).


262-269: Typo conditiionLabelconditionLabel

The double “i” slipped through everywhere (rootView.conditiionLabel). Fixing the name improves readability and prevents future search/replace mistakes.

📜 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 574b658 and 3b87eaf.

📒 Files selected for processing (5)
  • Wable-iOS.xcodeproj/project.pbxproj (31 hunks)
  • Wable-iOS/Domain/UseCase/Onboarding/UserProfileUseCase.swift (3 hunks)
  • Wable-iOS/Presentation/Onboarding/ViewController/AgreementViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/ProfileEditViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Viewit/List/View/Cell/ViewitListCell.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • Wable-iOS/Presentation/Onboarding/ViewController/AgreementViewController.swift
  • Wable-iOS/Presentation/Viewit/List/View/Cell/ViewitListCell.swift
🧰 Additional context used
🪛 SwiftLint (0.57.0)
Wable-iOS/Presentation/Profile/ProfileEditViewController.swift

[Warning] 17-17: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 30-30: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 15-15: TODOs should be resolved (유즈케이스 리팩 후에 뷰모델 만들어 넘기기)

(todo)


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

(unused_closure_parameter)

🔇 Additional comments (9)
Wable-iOS.xcodeproj/project.pbxproj (9)

33-33: Improved naming from AccountDeleteReason to WithdrawalReason

This renaming provides better semantics by using "Withdrawal" which more precisely describes the user action of leaving the service, rather than the generic "Delete" term.


203-203: Improved naming from CreateUserProfileUseCase to UserProfileUseCase

This change broadens the scope of the use case from only creation to potentially handling multiple profile operations, making it more versatile and reusable.


242-250: Well-structured addition of new Use Cases for Profile functionality

The addition of these use cases (FetchUserProfileUseCase, FetchUserCommentListUseCase, FetchUserContentListUseCase, FetchAccountInfoUseCase, etc.) follows good separation of concerns and provides clear interfaces for different profile-related operations.


242-314: Comprehensive implementation of Profile architecture

The additions include a complete set of components for the profile feature, following MVVM architecture with:

  • View components (MyProfileViewController, ProfileInfoCell, ProfileSegmentedHeaderView)
  • View models (MyProfileViewModel)
  • Data models (ProfileViewItem, ProfileSegmentKind)
  • Associated use cases

This demonstrates good architecture and separation of concerns.


301-314: Well-structured Withdrawal flow implementation

The implementation introduces a complete withdrawal flow with:

  1. A reason selection screen (WithdrawalReasonViewController)
  2. A guide/confirmation screen (WithdrawalGuideViewController)
  3. Supporting components (cells, view models, models)

The hierarchical group structure with separate Reason and Guide subgroups keeps the code organized and maintainable.

Also applies to: 1981-2067


321-324: Clear separation of AccountInfo components

The AccountInfo implementation follows the project's architectural patterns with a dedicated view controller, cells, models, and view model, all properly organized in their own group.

Also applies to: 2131-2172


1617-1640: Proper implementation of AlarmSetting feature

The AlarmSetting components are organized into a separate group with a clear separation between view and view model, maintaining consistency with the project's architectural patterns.


1-3164: Comprehensive Profile Feature Implementation

This PR delivers a substantial enhancement to the app with a well-structured profile feature implementation that includes:

  1. My Profile section with segmented views for content/comments
  2. Account information management
  3. Alarm settings configuration
  4. Complete account withdrawal flow

The architecture follows MVVM with clean separation between views, view models, and use cases, making the code maintainable and testable.


1-3164:

Details

✅ Verification successful

Verify async/await implementation in APIProvider

Based on the PR description, a key objective was to "introduce async/await syntax to improve code readability and maintainability by moving away from complex Combine-based code" and specifically update the request method in APIProvider. However, I don't see direct changes to the APIProvider file in this project file. Please ensure those changes are included in a separate PR or file.


🏁 Script executed:

#!/bin/bash
# Check if APIProvider.swift has been modified to include async/await syntax
echo "Checking for async/await implementation in APIProvider.swift..."
grep -n "async" "Wable-iOS/Infra/Network/APIProvider.swift" || echo "No async keyword found in APIProvider.swift"

Length of output: 223


Async/Await Confirmed in APIProvider.swift

The request method in Wable-iOS/Infra/Network/APIProvider.swift has been updated to include the async throws -> D signature (line 76). No further action is needed here.

@JinUng41 JinUng41 merged commit 174590e into develop May 19, 2025
1 check passed
@JinUng41 JinUng41 deleted the feat/#182-profile branch May 19, 2025 06:35
youz2me pushed a commit that referenced this pull request Oct 26, 2025
[Feat] 프로필 중간 구현
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat 기능 또는 객체 구현 🍻 진웅 술 한잔 가온나~

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 프로필 구현

3 participants