Skip to content

Conversation

@JinUng41
Copy link
Collaborator

@JinUng41 JinUng41 commented Apr 11, 2025

👻 PULL REQUEST

📄 작업 내용

  • 커뮤니티 기능을 구현하였습니다.
  • UI 테스트는 Mock 유즈케이스를 사용하였습니다.
  • BFX와 DNF는 팀 색상이 아직 구현되지 않아 임시로 와블 색상으로 설정해 두었습니다.
구현 내용 IPhone 13 mini
사전신청 전
사전신청 동작
사전신청 후
팬 더 데려오기 동작

🔗 연결된 이슈

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced new screens for community registration, invitations, and completion feedback.
    • Added a community view with improved item display and interaction features.
    • Implemented a view model to manage community-related functionalities effectively.
    • Added new components for community registration and user status checks.
  • Bug Fixes

    • Resolved issues related to user registration status checks and community list fetching.
  • Refactor

    • Updated community data handling to reflect team information and registration rates accurately.
    • Streamlined underlying interactions and API endpoints for a more consistent and robust experience.

@JinUng41 JinUng41 added ✨ feat 기능 또는 객체 구현 🍻 진웅 술 한잔 가온나~ labels Apr 11, 2025
@JinUng41 JinUng41 added this to the 리팩토링 마감 milestone Apr 11, 2025
@JinUng41 JinUng41 requested a review from youz2me April 11, 2025 15:29
@JinUng41 JinUng41 self-assigned this Apr 11, 2025
@coderabbitai
Copy link

coderabbitai bot commented Apr 11, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

This pull request restructures the community functionality module across multiple layers. The changes include renaming and removal of outdated use case files, updates to domain entities (switching from “name” to “team” and from “participantsCount” to “registrationRate”), and modifications in the repository and network layers (with updated DTOs, endpoints, and HTTP methods). In addition, several new UI components, view controllers, and a view model have been introduced alongside dependency injection updates in the TabBarController.

Changes

File(s) Change Summary
Wable-iOS.xcodeproj/project.pbxproj, .../FetchCommunityListUseCase.swift, .../UpdatePreRegisterUseCase.swift Renamed UpdatePreRegister to UpdateRegister, removed obsolete use case files, and added new community-related source files.
Wable-iOS/Data/Mapper/CommunityMapper.swift, Wable-iOS/Data/RepositoryImpl/CommunityRepositoryImpl.swift, Wable-iOS/Domain/Entity/Community.swift, Wable-iOS/Domain/Entity/CommunityRegistration.swift, Wable-iOS/Domain/RepositoryInterface/CommunityRepository.swift, Wable-iOS/Domain/UseCase/Community/CommunityUseCase.swift Updated mapping logic to use team and registrationRate; changed repository method signatures (including renaming and return type update) and added a new CommunityRegistration entity along with a new CommunityUseCase protocol and its implementations.
Wable-iOS/Infra/Network/DTO/Request/Community/UpdateRegister.swift, .../DTO/Response/Community/FetchCommunites.swift, .../DTO/Response/Community/IsUserRegistered.swift, .../DTO/Response/RegisterResult.swift, Wable-iOS/Infra/Network/TargetType/CommunityTargetType.swift Renamed DTO structures and updated JSON mapping (e.g. from communityNum to registrationRate), modified API endpoints and HTTP methods, and introduced new network responses and cases to support user registration status.
Wable-iOS/Presentation/Community/... (including new files such as CommunityItem.swift, CommunityHeaderView.swift, CommunityInviteCell.swift, CommunityRegisterCell.swift, CommunityRegisterCompleteViewController.swift, CommunityViewController.swift, CommunityCellBaseView.swift, CommunityView.swift, CommunityViewModel.swift) and Wable-iOS/Presentation/TabBar/TabBarController.swift Introduced multiple new UI components and view controllers for community features; implemented a new view model with diffable data source logic; and updated dependency injection in the TabBarController to initialize the community flow with its view model and use case.

Sequence Diagram(s)

sequenceDiagram
    participant VC as CommunityViewController
    participant VM as CommunityViewModel
    participant UC as CommunityUseCase/Repository
    participant API as Network API

    VC->>VM: User taps register button (communityTeam)
    VM->>UC: register(for communityTeam)
    UC->>API: Send updateRegister request with communityName
    API-->>UC: Return RegisterResult (registrationRate)
    UC-->>VM: Emit registrationRate
    VM-->>VC: Update UI with registration completion
Loading
sequenceDiagram
    participant VC as CommunityViewController
    participant VM as CommunityViewModel
    participant UC as CommunityUseCase/Repository
    participant API as Network API

    VC->>VM: View loads/refreshed
    VM->>UC: isUserRegistered()
    UC->>API: Send isUserRegistered GET request
    API-->>UC: Return IsUserRegistered DTO
    UC-->>VM: Emit CommunityRegistration state
    VM-->>VC: Render registration status in UI
Loading

Assessment against linked issues

Objective Addressed Explanation
커뮤니티 탭 화면 구성 (#161)

Poem

I'm a code bunny with ears so keen,
Hopping through modules, all pristine.
New cells, views, and methods align,
In my digital warren, everything's fine!
With each commit, I dance with cheer 🐇💻
Celebrating changes light and clear.

Tip

⚡💬 Agentic Chat (Pro Plan, General Availability)
  • We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.

📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 14daa0e and 4556f9d.

📒 Files selected for processing (3)
  • Wable-iOS.xcodeproj/project.pbxproj (24 hunks)
  • Wable-iOS/Infra/Network/TargetType/CommunityTargetType.swift (3 hunks)
  • Wable-iOS/Presentation/TabBar/TabBarController.swift (1 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

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

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Documentation and Community

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

@JinUng41 JinUng41 moved this to In Review in Wable-iOS Apr 11, 2025
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: 14

🧹 Nitpick comments (29)
Wable-iOS/Presentation/Community/View/Cell/CommunityHeaderView.swift (4)

17-25: UI component implementation looks good but consider accessibility improvements.

The UI components are well-structured with appropriate styling. The purple color theme aligns with the PR objectives mentioning temporary color settings for the community feature.

Consider adding accessibility support to improve the experience for users with disabilities:

private let textLabel = UILabel().then {
    $0.attributedText = Constant.text.pretendardString(with: .body4)
    $0.textColor = .purple100
    $0.numberOfLines = 0
+   $0.isAccessibilityElement = true
+   $0.accessibilityLabel = Constant.text
}

44-49: Consider using the addSubviews method for consistency with other files.

Other files in the codebase use a plural addSubviews method for adding multiple views rather than individual addSubview calls.

For consistency with files like CommunityCellBaseView.swift and CommunityInviteCell.swift, consider:

func setupView() {
-   backgroundView.addSubview(textLabel)
-   
-   addSubview(backgroundView)
+   backgroundView.addSubviews(textLabel)
+   
+   addSubviews(backgroundView)
}

66-73: Consider localization support for text content.

The hardcoded Korean text may need localization support if the application is intended for international use.

Consider moving the text to a localization file:

enum Constant {
-   static let text = """
-                     팀별 커뮤니티 공간을 준비중이에요. 팀별 일정 이상의
-                     팬이 모여야 팀별 공간이 열립니다.
-                     *계정 1개당 1개의 팀별 공간에만 참여 가능해요!
-                     """
+   static let text = NSLocalizedString(
+       "community.header.description",
+       comment: "Text explaining that team community spaces are being prepared and will be available when a sufficient number of fans join"
+   )
}

13-40: Class structure follows good practices, but consider interface refinement.

The overall class structure with UIComponents, initializers, and setup methods follows good Swift practices. The use of @available(*, unavailable) for the required initializer is appropriate.

Consider adding a method to expose updating the text or style if needed in the future:

func configure(with text: String? = nil) {
    if let text = text {
        textLabel.attributedText = text.pretendardString(with: .body4)
    }
}
Wable-iOS/Presentation/Community/View/Cell/CommunityInviteCell.swift (2)

2-2: Fix the filename in the file header

There's an extra space in the filename declaration: CommunityInviteCell .swift should be CommunityInviteCell.swift.

-//  CommunityInviteCell .swift
+//  CommunityInviteCell.swift

142-151: Consider tracking button state more explicitly

The button state check relies on comparing the configuration title with a constant string. It might be more robust to track the button state using a boolean property.

private extension CommunityInviteCell {
+    enum CopyButtonState {
+        case ready
+        case completed
+    }
+    
+    private var copyButtonState: CopyButtonState = .ready
+    
    func resetCopyLinkButton() {
        var config = copyLinkButton.configuration
        config?.attributedTitle = Constant.defaultTitle.pretendardString(with: .body3)
        config?.image = nil
        copyLinkButton.configuration = config
+        copyButtonState = .ready
    }
    
    func showCopyLinkCompletedState() {
        var config = copyLinkButton.configuration
        config?.attributedTitle = Constant.copyLinkCompletedTitle.pretendardString(with: .body3)
        config?.image = .icCheck
        config?.imagePlacement = .leading
        copyLinkButton.configuration = config
+        copyButtonState = .completed
    }
}

// In the action method:
@objc func copyLinkButtonDidTap(_ sender: UIButton) {
-    guard sender.configuration?.title == Constant.defaultTitle else { return }
+    guard copyButtonState == .ready else { return }
    
    copyLinkClosure?()
    
    showCopyLinkCompletedState()
    DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
        self.resetCopyLinkButton()
    }
}
Wable-iOS/Infra/Network/DTO/Response/Community/RegisterResult.swift (1)

14-18: Consider renaming for semantic consistency between property and JSON key

There's a potential semantic mismatch between the property name registrationRate (suggesting a percentage) and the JSON key communityNum (suggesting a count or number). This inconsistency could cause confusion for future developers maintaining this code.

Consider either:

  1. Renaming the property to match the semantic meaning of the API response field
  2. Requesting an API change to make the backend field name more aligned with its purpose
struct RegisterResult: Decodable {
    let registrationRate: Double
    
    enum CodingKeys: String, CodingKey {
-       case registrationRate = "communityNum"
+       case registrationRate = "registrationRate" // If API can be changed
    }
}
Wable-iOS/Presentation/Community/View/Cell/CommunityRegisterCell.swift (2)

48-53: Consider adding accessibility support

The cell doesn't have accessibility labels or traits set, which could make the app less usable for people with disabilities.

func configure(image: UIImage?, title: String, hasRegisteredTeam: Bool = false) {
    communityImageView.image = image
    titleLabel.text = title
    
    registerButton.isHidden = hasRegisteredTeam
+   
+   // Set accessibility properties
+   titleLabel.isAccessibilityElement = true
+   communityImageView.isAccessibilityElement = true
+   registerButton.isAccessibilityElement = true
+   
+   titleLabel.accessibilityLabel = title
+   communityImageView.accessibilityLabel = "\(title) 이미지"
+   registerButton.accessibilityLabel = Constant.defaultButtonTitle
}

62-63: Consider extracting string styles to a dedicated method

The line using pretendardString(with: .body3) suggests you're applying a text style. For better maintainability and reusability, consider extracting this into a dedicated method.

func setupView() {
    contentView.addSubview(baseView)
    
-   registerButton.configuration?.attributedTitle = Constant.defaultButtonTitle.pretendardString(with: .body3)
+   registerButton.configuration?.attributedTitle = applyDefaultButtonStyle(to: Constant.defaultButtonTitle)
}

+ private func applyDefaultButtonStyle(to text: String) -> AttributedString {
+     return text.pretendardString(with: .body3)
+ }
Wable-iOS/Presentation/Community/Model/CommunityItem.swift (1)

10-14: Consider clarifying the difference between similar Boolean properties

The model has two Boolean properties with similar names: isRegistered and hasRegisteredCommunity. The distinction between these properties isn't immediately clear and could lead to confusion.

Consider adding documentation comments or renaming one of the properties to better reflect its specific purpose.

struct CommunityItem: Hashable {
    let community: Community
+   /// Indicates if the current user has registered for this specific community
    let isRegistered: Bool
+   /// Indicates if the user has registered for any community in the system
    let hasRegisteredCommunity: Bool
}
Wable-iOS/Infra/Network/DTO/Response/Community/IsUserRegistered.swift (1)

12-16: Consider adding a CodingKeys enum for safer JSON parsing

Even though the property name might match the JSON key, adding a CodingKeys enum provides future flexibility if the API changes and makes the code more maintainable.

struct IsUserRegistered: Decodable {
    let commnunityName: String?
+   
+   enum CodingKeys: String, CodingKey {
+       case commnunityName
+   }
}
Wable-iOS/Infra/Network/DTO/Request/Community/UpdateRegister.swift (1)

2-2: Consider updating the comment to match renamed entity.

The structure has been renamed from UpdatePreRegister to UpdateRegister, but the comment on line 11 still mentions "커뮤니티 사전 참여" (community pre-participation). For consistency, consider updating the comment to match the new naming convention.

Also applies to: 14-14

Wable-iOS/Infra/Network/DTO/Response/Community/FetchCommunites.swift (1)

2-2: Typo in file/struct name.

There appears to be a typo in the file name and struct name: "FetchCommunites" should be "FetchCommunities" (missing an 'i'). Consider fixing this for better clarity and consistency with naming conventions.

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

26-26: Fix typo in property name.

There's a spelling error in the property name commnunityName (extra 'n'). This should be corrected to communityName to maintain naming consistency and avoid potential bugs.

-        guard let teamName = response.commnunityName else {
+        guard let teamName = response.communityName else {
Wable-iOS/Domain/RepositoryInterface/CommunityRepository.swift (1)

13-15: Add documentation for protocol methods.

Consider adding documentation comments to describe what each method does, its parameters, and return values. This would improve code readability and make it easier for other developers to understand how to use this interface.

 protocol CommunityRepository {
+    /// Updates the user's registration for a community
+    /// - Parameter communityName: The name of the community to register for
+    /// - Returns: A publisher that emits the updated registration rate or an error
     func updateRegister(communityName: String) -> AnyPublisher<Double, WableError>
+    
+    /// Fetches the list of available communities
+    /// - Returns: A publisher that emits the list of communities or an error
     func fetchCommunityList() -> AnyPublisher<[Community], WableError>
+    
+    /// Checks if the current user has registered for a team
+    /// - Returns: A publisher that emits the registration status or an error
     func isUserRegistered() -> AnyPublisher<CommunityRegistration, WableError>
 }
Wable-iOS/Presentation/Community/View/Subview/CommunityView.swift (2)

13-45: Add documentation comments for the class.

Consider adding documentation comments to explain the purpose of this view and how it should be used. This would help other developers understand the component's role in the application architecture.

+/**
+ * A view that displays the community features including team registration and fan invitation.
+ * It contains a collection view for displaying teams and an action button for requesting new teams.
+ */
 final class CommunityView: UIView {

25-31: Extract string formatting logic to a separate method.

The complex string manipulation for the button title would be cleaner if extracted to a separate helper method, making the UI setup more focused on layout rather than text formatting.

 let askButton = WableButton(style: .black).then {
     var config = $0.configuration
-    config?.attributedTitle = Constant.askButtonTitle
-        .pretendardString(with: .body3)
-        .highlight(textColor: .sky50, to: "요청하기")
+    config?.attributedTitle = configureAskButtonTitle()
     $0.configuration = config
 }

+private func configureAskButtonTitle() -> NSAttributedString {
+    return Constant.askButtonTitle
+        .pretendardString(with: .body3)
+        .highlight(textColor: .sky50, to: "요청하기")
+}
Wable-iOS/Presentation/Community/View/Subview/CommunityCellBaseView.swift (2)

17-19: Consider using scaleAspectFill for community images

The current scaleAspectFit content mode might leave empty spaces around the image. For community logos or team images, scaleAspectFill with clipsToBounds = true would typically provide a better visual appearance.

let communityImageView = UIImageView().then {
-        $0.contentMode = .scaleAspectFit
+        $0.contentMode = .scaleAspectFill
+        $0.clipsToBounds = true
    }

61-65: Use adjustedHeight for consistent dimensions

For the image view, you're using make.height.equalTo(communityImageView.snp.width) to create a square aspect ratio. However, this differs from how you handle the button's height. Consider using adjustedHeightEqualTo for consistency.

    communityImageView.snp.makeConstraints { make in
        make.verticalEdges.leading.equalToSuperview()
        make.adjustedWidthEqualTo(64)
-        make.height.equalTo(communityImageView.snp.width)
+        make.adjustedHeightEqualTo(64)
    }
Wable-iOS/Presentation/Community/View/CommunityRegisterCompleteViewController.swift (2)

61-71: Extract hardcoded text to a separate function or constants

The multi-line description text is hardcoded directly in the view controller. Consider extracting it to a separate function or constants to improve maintainability and readability.

+    private func getDescriptionText(for teamName: String) -> String {
+        return """
+               \(teamName)팀을 응원하는 팬분들이 더 모여야
+               \(teamName) 라운지가 오픈돼요!
+               팬 더 데려오기를 통해 링크를 복사하여
+               함께 응원할 팬을 데려와주세요!
+               """
+    }

    let descriptionLabel = UILabel().then {
-        $0.attributedText = """
-                            \(teamName)팀을 응원하는 팬분들이 더 모여야
-                            \(teamName) 라운지가 오픈돼요!
-                            팬 더 데려오기를 통해 링크를 복사하여
-                            함께 응원할 팬을 데려와주세요!
-                            """.pretendardString(with: .body2)
+        $0.attributedText = getDescriptionText(for: teamName).pretendardString(with: .body2)
        $0.textColor = .gray700
        $0.textAlignment = .center
        $0.numberOfLines = 0
    }

73-80: Use the UIView extension method for adding the background view

You're using the addSubviews extension method for adding subviews to the background view, but then using the standard addSubview method for adding the background view to the main view. For consistency, consider using the extension method for both.

        backgroundView.addSubviews(
            imageView,
            titleLabel,
            descriptionLabel
        )
        
-        view.addSubview(backgroundView)
+        view.addSubviews(backgroundView)
Wable-iOS/Infra/Network/TargetType/CommunityTargetType.swift (2)

23-29: Document API version change for updateRegister

The endpoint for the updateRegister case has been updated from v1 to v2, which suggests significant changes in the API contract. Consider adding a comment to document this change and any new behavior.

        case .updateRegister:
+            // Updated from v1 to v2 - new endpoint returns registration rate
            return "/v2/community/prein"
        case .fetchCommunityList:
            return "/v1/community/list"
-        case .isUserRegisterd:
+        case .isUserRegistered:
            return "/v1/community/member"

49-54: Document HTTP method change for updateRegister

The HTTP method for updateRegister has been changed from POST to PATCH, which indicates a semantic change from creation to modification. Consider adding a comment to document this change.

        case .updateRegister:
+            // Changed from POST to PATCH to reflect that this is updating existing registration
            return .patch
        case .fetchCommunityList:
            return .get
-        case .isUserRegisterd:
+        case .isUserRegistered:
            return .get
Wable-iOS/Presentation/Community/ViewModel/CommunityViewModel.swift (2)

48-60: Reduce duplicated fetch logic.
You’re making almost the same fetch request for the community list at both line 48 and line 67. Consider wrapping the fetch request logic (including error handling and empty list fallback) into a dedicated helper function to keep your code DRY and easier to maintain.

 private func fetchCommunityListPublisher() -> AnyPublisher<[Community], Never> {
     useCase.fetchCommunityList()
         .catch { error -> AnyPublisher<[Community], Never> in
             WableLogger.log("\(error.localizedDescription)", for: .error)
             return .just([])
         }
         .eraseToAnyPublisher()
 }
 
 // In transform(...):
 input.viewDidLoad
-    .withUnretained(self)
-    .flatMap { owner, _ -> AnyPublisher<[Community], Never> in
-        owner.useCase.fetchCommunityList()
-            .catch { error -> AnyPublisher<[Community], Never> in
-                WableLogger.log("\(error.localizedDescription)", for: .error)
-                return .just([])
-            }
-            .eraseToAnyPublisher()
-    }
+    .flatMap { [weak self] _ -> AnyPublisher<[Community], Never> in
+        guard let self = self else { return .just([]) }
+        return self.fetchCommunityListPublisher()
     }
     .filter { !$0.isEmpty }
     .sink { communityListRelay.send($0) }
     .store(in: cancelBag)

 Publishers.Merge(input.viewDidLoad, viewDidRefresh)
-    .withUnretained(self)
-    .flatMap { owner, _ -> AnyPublisher<[Community], Never> in
-        owner.useCase.fetchCommunityList()
-            .catch { error -> AnyPublisher<[Community], Never> in
-                WableLogger.log("\(error.localizedDescription)", for: .error)
-                return .just([])
-            }
-            .eraseToAnyPublisher()
-    }
+    .flatMap { [weak self] _ -> AnyPublisher<[Community], Never> in
+        guard let self = self else { return .just([]) }
+        return self.fetchCommunityListPublisher()
     }
     .filter { !$0.isEmpty }
     .sink { communityListRelay.send($0) }
     .store(in: cancelBag)

110-124: Confirm sorting logic for better user experience.
You’re sorting so that registered items appear first. Ensure this matches the expected UI design. If needed, consider offering user-configurable sorting or groupings to improve discoverability.

Wable-iOS/Presentation/Community/View/CommunityViewController.swift (3)

74-79: Use a clearer name for refresh action.
The method collectionViewDidRefresh only triggers the refresh relay. Consider a more descriptive name to indicate the action is re-fetching community data.


110-114: Consider surfacing success feedback for copied links.
Inside copyLinkClosure, you call showCopyLinkCompleteSheet() and do a silent clipboard copy. Confirm that the user also sees any visual or haptic feedback indicating the link is successfully copied.


180-186: Use non-animating snapshot updates if data changes are frequent.
You have dataSource?.apply(snapshot, animatingDifferences: true), which is visually appealing but can become overwhelming if invoked often. Optionally, switch to false if refreshes occur rapidly or if the user doesn't need a visual transition every time.

Wable-iOS/Domain/UseCase/Community/CommunityUseCase.swift (1)

48-59: Consider sorting mock data consistently

The mock data appears to be roughly sorted by registration rate, but not entirely. Consider either documenting the intended order or sorting consistently (by registration rate, alphabetically by team, etc.).

func fetchCommunityList() -> AnyPublisher<[Community], WableError> {
    let communityMockData: [Community] = [
-        Community(team: .t1, registrationRate: 0.91),
-        Community(team: .gen, registrationRate: 0.88),
-        Community(team: .hle, registrationRate: 0.72),
-        Community(team: .dk, registrationRate: 0.79),
-        Community(team: .kt, registrationRate: 0.65),
-        Community(team: .ns, registrationRate: 0.54),
-        Community(team: .drx, registrationRate: 0.49),
-        Community(team: .bro, registrationRate: 0.37),
-        Community(team: .bfx, registrationRate: 0.42),
-        Community(team: .dnf, registrationRate: 0.33)
+        // Sorted by registration rate (highest to lowest)
+        Community(team: .t1, registrationRate: 0.91),
+        Community(team: .gen, registrationRate: 0.88),
+        Community(team: .dk, registrationRate: 0.79),
+        Community(team: .hle, registrationRate: 0.72),
+        Community(team: .kt, registrationRate: 0.65),
+        Community(team: .ns, registrationRate: 0.54),
+        Community(team: .drx, registrationRate: 0.49),
+        Community(team: .bfx, registrationRate: 0.42),
+        Community(team: .bro, registrationRate: 0.37),
+        Community(team: .dnf, registrationRate: 0.33)
    ]
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 83271fb and f22006e.

📒 Files selected for processing (25)
  • Wable-iOS.xcodeproj/project.pbxproj (24 hunks)
  • Wable-iOS/Data/Mapper/CommunityMapper.swift (1 hunks)
  • Wable-iOS/Data/RepositoryImpl/CommunityRepositoryImpl.swift (2 hunks)
  • Wable-iOS/Domain/Entity/Community.swift (1 hunks)
  • Wable-iOS/Domain/Entity/CommunityRegistration.swift (1 hunks)
  • Wable-iOS/Domain/RepositoryInterface/CommunityRepository.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Community/CommunityUseCase.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Community/FetchCommunityListUseCase.swift (0 hunks)
  • Wable-iOS/Domain/UseCase/Community/UpdatePreRegisterUseCase.swift (0 hunks)
  • Wable-iOS/Infra/Network/DTO/Request/Community/UpdateRegister.swift (2 hunks)
  • Wable-iOS/Infra/Network/DTO/Response/Community/FetchCommunites.swift (1 hunks)
  • Wable-iOS/Infra/Network/DTO/Response/Community/IsUserRegistered.swift (1 hunks)
  • Wable-iOS/Infra/Network/DTO/Response/Community/RegisterResult.swift (1 hunks)
  • Wable-iOS/Infra/Network/TargetType/CommunityTargetType.swift (2 hunks)
  • Wable-iOS/Presentation/Community/CommunityViewController.swift (0 hunks)
  • Wable-iOS/Presentation/Community/Model/CommunityItem.swift (1 hunks)
  • Wable-iOS/Presentation/Community/View/Cell/CommunityHeaderView.swift (1 hunks)
  • Wable-iOS/Presentation/Community/View/Cell/CommunityInviteCell.swift (1 hunks)
  • Wable-iOS/Presentation/Community/View/Cell/CommunityRegisterCell.swift (1 hunks)
  • Wable-iOS/Presentation/Community/View/CommunityRegisterCompleteViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Community/View/CommunityViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Community/View/Subview/CommunityCellBaseView.swift (1 hunks)
  • Wable-iOS/Presentation/Community/View/Subview/CommunityView.swift (1 hunks)
  • Wable-iOS/Presentation/Community/ViewModel/CommunityViewModel.swift (1 hunks)
  • Wable-iOS/Presentation/TabBar/TabBarController.swift (1 hunks)
💤 Files with no reviewable changes (3)
  • Wable-iOS/Presentation/Community/CommunityViewController.swift
  • Wable-iOS/Domain/UseCase/Community/FetchCommunityListUseCase.swift
  • Wable-iOS/Domain/UseCase/Community/UpdatePreRegisterUseCase.swift
🧰 Additional context used
🧬 Code Graph Analysis (9)
Wable-iOS/Presentation/Community/View/Subview/CommunityView.swift (4)
Wable-iOS/Presentation/Community/View/CommunityRegisterCompleteViewController.swift (1)
  • setupView (47-103)
Wable-iOS/Presentation/Community/View/Cell/CommunityInviteCell.swift (1)
  • setupView (97-106)
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/Community/View/CommunityRegisterCompleteViewController.swift (2)
Wable-iOS/Presentation/Helper/Extension/UIView+.swift (1)
  • addSubviews (32-36)
Wable-iOS/Presentation/Helper/Extension/ConstraintMaker+.swift (2)
  • adjustedWidthEqualTo (27-30)
  • adjustedHeightEqualTo (45-48)
Wable-iOS/Presentation/Community/ViewModel/CommunityViewModel.swift (4)
Wable-iOS/Domain/UseCase/Community/CommunityUseCase.swift (6)
  • register (32-34)
  • register (66-70)
  • isUserRegistered (24-26)
  • isUserRegistered (40-45)
  • fetchCommunityList (28-30)
  • fetchCommunityList (47-64)
Wable-iOS/Domain/Entity/CommunityRegistration.swift (1)
  • initialState (14-16)
Wable-iOS/Data/RepositoryImpl/CommunityRepositoryImpl.swift (2)
  • isUserRegistered (42-46)
  • fetchCommunityList (33-40)
Wable-iOS/Core/Logger/WableLogger.swift (1)
  • log (14-25)
Wable-iOS/Presentation/Community/View/CommunityViewController.swift (3)
Wable-iOS/Presentation/Community/View/CommunityRegisterCompleteViewController.swift (1)
  • viewDidLoad (37-41)
Wable-iOS/Presentation/Community/View/Cell/CommunityRegisterCell.swift (1)
  • configure (48-53)
Wable-iOS/Presentation/Community/ViewModel/CommunityViewModel.swift (1)
  • transform (32-135)
Wable-iOS/Presentation/Community/View/Subview/CommunityCellBaseView.swift (2)
Wable-iOS/Presentation/Helper/Extension/UIView+.swift (1)
  • addSubviews (32-36)
Wable-iOS/Presentation/Helper/Extension/ConstraintMaker+.swift (2)
  • adjustedWidthEqualTo (27-30)
  • adjustedHeightEqualTo (45-48)
Wable-iOS/Infra/Network/TargetType/CommunityTargetType.swift (2)
Wable-iOS/Data/RepositoryImpl/CommunityRepositoryImpl.swift (2)
  • updateRegister (20-31)
  • fetchCommunityList (33-40)
Wable-iOS/Domain/UseCase/Community/CommunityUseCase.swift (2)
  • fetchCommunityList (28-30)
  • fetchCommunityList (47-64)
Wable-iOS/Presentation/Community/View/Cell/CommunityInviteCell.swift (3)
Wable-iOS/Presentation/Community/View/Subview/CommunityCellBaseView.swift (2)
  • setupView (52-58)
  • setupConstraint (60-77)
Wable-iOS/Presentation/Helper/Extension/UIView+.swift (1)
  • addSubviews (32-36)
Wable-iOS/Presentation/Helper/Extension/ConstraintMaker+.swift (2)
  • adjustedWidthEqualTo (27-30)
  • adjustedHeightEqualTo (45-48)
Wable-iOS/Domain/UseCase/Community/CommunityUseCase.swift (1)
Wable-iOS/Data/RepositoryImpl/CommunityRepositoryImpl.swift (3)
  • isUserRegistered (42-46)
  • fetchCommunityList (33-40)
  • updateRegister (20-31)
Wable-iOS/Presentation/Community/View/Cell/CommunityHeaderView.swift (3)
Wable-iOS/Presentation/Community/View/Cell/CommunityInviteCell.swift (2)
  • setupView (97-106)
  • setupConstraint (108-132)
Wable-iOS/Presentation/Community/View/Cell/CommunityRegisterCell.swift (2)
  • setupView (59-63)
  • setupConstraint (65-70)
Wable-iOS/Presentation/Community/View/Subview/CommunityCellBaseView.swift (2)
  • setupView (52-58)
  • setupConstraint (60-77)
🪛 SwiftLint (0.57.0)
Wable-iOS/Presentation/Community/View/CommunityViewController.swift

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

(unused_closure_parameter)

🔇 Additional comments (31)
Wable-iOS/Presentation/Community/View/Cell/CommunityHeaderView.swift (1)

51-61: Layout constraints look good and follow project conventions.

The constraints are properly configured using SnapKit, with appropriate insets to ensure proper spacing between elements. The pattern matches other components in the project.

Wable-iOS/Presentation/Community/View/Cell/CommunityInviteCell.swift (8)

14-31: Well-structured UI components with clear organization

The UI components are well-organized with appropriate MARK comments and clean initialization using the Then library. This makes the code readable and maintainable.


26-31: Potential mismatch between progress bar corner radius and height

The progress bar has a corner radius of 8, but its height is set to 12 (on line 130). This might result in a fully rounded appearance rather than just rounded corners.

Consider adjusting either the corner radius or ensuring it doesn't exceed half the height value for a proper rounded corner appearance:

- $0.layer.cornerRadius = 8
+ $0.layer.cornerRadius = 6  // Half of the height (12) to ensure proper rounded corners

60-72: Well-designed configuration method

The configure method provides a clean API for setting up the cell with all necessary properties. Good practice to have animated progress updates.


78-91: Clear helper methods for button state management

The helper methods for handling the copy link button states are well-defined and maintain clear separation of concerns.


97-106: Good use of UIView extension for adding subviews

Effective use of the addSubviews extension method to add multiple views at once, enhancing code readability.


108-132: Well-organized constraints with effective use of SnapKit

The constraints are clearly defined using SnapKit, with good use of the custom extensions for adjusted width and height. Good organization with each view's constraints in separate blocks.


39-45: Good initialization pattern

The initialization follows proper UICollectionViewCell patterns with separate setup methods called in a clear order.


156-160: Clean use of computed properties

The computed properties provide a clean way to access baseView components, enhancing code readability.

Wable-iOS/Presentation/Community/View/Cell/CommunityRegisterCell.swift (3)

42-46: Clear all closure references in prepareForReuse

Good practice to set the closure to nil in prepareForReuse to prevent memory leaks and stale references.


79-83: Good practice using optional chaining for closures

The use of optional chaining for the closure is a good practice to prevent runtime crashes if the closure is nil.


87-91: Good use of computed properties for encapsulation

Using computed properties to access the baseView's components maintains good encapsulation while providing a clean interface for the cell's implementation.

Wable-iOS/Domain/Entity/CommunityRegistration.swift (1)

10-17: Clean domain entity implementation.

The CommunityRegistration struct is well-designed and clearly represents a user's registration status for a community. The initialState() method provides a convenient way to create an initial state with default values.

Wable-iOS/Domain/Entity/Community.swift (1)

13-14: Property mutability inconsistency.

The team property is immutable (let) while registrationRate is mutable (var). Is this intentional? If registrationRate needs to be updated during the lifecycle of a Community instance, this makes sense. Otherwise, consider using let for consistency.

Wable-iOS/Infra/Network/DTO/Response/Community/FetchCommunites.swift (1)

16-21: Good use of CodingKeys for mapping.

The addition of CodingKeys to map the backend field communityNum to the more semantic registrationRate property is a good practice. This maintains the correct data model while ensuring compatibility with the backend API.

Wable-iOS/Presentation/Community/View/Subview/CommunityView.swift (1)

117-129:

Details

❓ Verification inconclusive

Register a header view class for the collection view.

You've configured a header for the collection view layout, but you need to register a view class that will be used for this header.

Add the following code to your setupView() method:

collectionView.register(
    YourHeaderViewClass.self,
    forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
    withReuseIdentifier: "HeaderIdentifier"
)

🏁 Script executed:

#!/bin/bash
# Check if a header view is registered elsewhere in the codebase
echo "Searching for collection view header registration"
rg -t swift "registerClass|register\(.*elementKindSectionHeader" --no-ignore-vcs

Length of output: 191


Action Required: Manually Verify Header View Registration

The current changes configure a collection view header in the layout, but our search did not reveal any code that registers a header view class. Please ensure you add header view registration in your view setup (for example, in your setupView() method) by inserting the following code:

collectionView.register(
    YourHeaderViewClass.self,
    forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
    withReuseIdentifier: "HeaderIdentifier"
)

Double-check that the class name and identifier match your intended implementation.

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

20-31:

Details

✅ Verification successful

Verify API migration from updatePreRegister to updateRegister

The function has been renamed from updatePreRegister to updateRegister and now returns a registration rate instead of void. Ensure that all callers of this function have been updated to handle the returned Double value.


🏁 Script executed:

#!/bin/bash
# Checking all references to the old and new function names in the codebase
echo "Searching for references to updatePreRegister:"
rg "updatePreRegister" --type swift

echo -e "\nSearching for references to updateRegister:"
rg "updateRegister" --type swift

Length of output: 1209


API Migration Verified: updateRegister is Fully Adopted

The search confirms that all references to the deprecated updatePreRegister have been removed. All call sites now correctly invoke updateRegister and, as shown in the repository, the function returns a registration rate (Double) as intended. Please ensure that the double value is properly handled where it's used (e.g., in CommunityUseCase.swift).

Wable-iOS/Domain/UseCase/Community/CommunityUseCase.swift (4)

11-15: Well-designed protocol with clear responsibilities

The protocol defines three distinct operations with appropriate return types using Combine publishers, making the contract clear and testable.


17-35: Implementation properly delegates to repository layer

Good use of dependency injection through the constructor. The implementation is clean and correctly delegates to the repository methods, maintaining separation of concerns.


37-71: Mock implementation facilitates testing and development

The mock implementation provides realistic test data with simulated network delays, which is excellent for testing and development. The random delay range (0.7-1.3 seconds) provides a realistic simulation.


32-34:

Details

✅ Verification successful

Verify team name mapping to repository call

This method correctly maps from the domain enum LCKTeam to a string value when calling the repository. Make sure the rawValue matches the expected format on the server side.


🏁 Script executed:

#!/bin/bash
# Check if the LCKTeam enum's raw values match what the API expects

echo "Examining LCKTeam enum raw values:"
rg -A 15 "enum LCKTeam" --type swift

Length of output: 776


Team Name Mapping Verified

The repository call in register(for communityTeam:) correctly maps the LCKTeam enum to its raw string value. The enum in Wable-iOS/Domain/Enum/LCKTeam.swift defines the raw values (e.g., "T1", "GEN", "HLE", etc.) exactly as expected by the repository call. No changes are needed.

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

115-115: File rename observed: UpdatePreRegister.swift to UpdateRegister.swift

This change indicates a naming convention update, likely reflecting a shift from a "pre-registration" concept to a more general "registration" process for communities.


205-207: New UI components added for community feature

The addition of these cell classes suggests a list-based UI for the community feature, with dedicated cells for registration and invitations, supported by a base view.


257-258: Main community view components added

These new view classes will form the core UI for the community feature, including a completion screen for the registration flow.


271-273: Core community domain components added

These files implement the business logic for the community feature:

  • CommunityUseCase for handling use cases
  • IsUserRegistered and RegisterResult provide response model structures for API interactions

274-277: Community data models and presentation layer added

These files complete the architecture by providing:

  • Domain model (CommunityRegistration)
  • View model data structure (CommunityItem)
  • Presentation logic (CommunityViewModel)
  • UI component for the header section (CommunityHeaderView)

884-885: Response DTOs added to Community directory

New response data transfer objects have been added to handle API responses for community registration status checks and results.


1044-1044: New domain entity added for Community Registration

The CommunityRegistration entity has been added to the domain layer, suggesting a structured approach to handling registration data.


1241-1241: Community use case implementation added

The CommunityUseCase has been added to the domain layer's UseCase directory, properly organizing business logic.


1320-1348: Well-organized project structure for Community feature

The project organization follows a clear feature-based architecture with proper separation of concerns:

  • Model: Data structures
  • ViewModel: Presentation logic
  • View: UI components organized into cells and subviews

This organization will help maintain the codebase as the feature evolves.


1811-1826: Model and ViewModel directories properly structured

Good separation between data model (CommunityItem) and presentation logic (CommunityViewModel).

Comment on lines +14 to +169
final class CommunityInviteCell: UICollectionViewCell {

// MARK: - UIComponent

private let baseView = CommunityCellBaseView()

private let progressTitleLabel = UILabel().then {
$0.attributedText = "진행도".pretendardString(with: .caption1)
}

private let progressImageView = UIImageView(image: .icFan)

private let progressBar = UIProgressView(progressViewStyle: .bar).then {
$0.trackTintColor = .gray200
$0.layer.cornerRadius = 8
$0.setProgress(0.0, animated: false)
$0.clipsToBounds = true
}

// MARK: - Property

var copyLinkClosure: (() -> Void)?

// MARK: - Initializer

override init(frame: CGRect) {
super.init(frame: frame)

setupView()
setupConstraint()
setupAction()
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func prepareForReuse() {
super.prepareForReuse()

communityImageView.image = nil
copyLinkClosure = nil
resetCopyLinkButton()
}

func configure(
image: UIImage?,
title: String,
progress: Float,
progressBarColor: UIColor
) {
communityImageView.image = image

titleLabel.text = title

progressBar.progressTintColor = progressBarColor
progressBar.setProgress(progress, animated: true)
}
}

// MARK: - Helper Method

private extension CommunityInviteCell {
func resetCopyLinkButton() {
var config = copyLinkButton.configuration
config?.attributedTitle = Constant.defaultTitle.pretendardString(with: .body3)
config?.image = nil
copyLinkButton.configuration = config
}

func showCopyLinkCompletedState() {
var config = copyLinkButton.configuration
config?.attributedTitle = Constant.copyLinkCompletedTitle.pretendardString(with: .body3)
config?.image = .icCheck
config?.imagePlacement = .leading
copyLinkButton.configuration = config
}
}

// MARK: - Setup Method

private extension CommunityInviteCell {
func setupView() {
contentView.addSubviews(
baseView,
progressTitleLabel,
progressImageView,
progressBar
)

resetCopyLinkButton()
}

func setupConstraint() {
baseView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(16)
make.horizontalEdges.equalToSuperview()
}

progressTitleLabel.snp.makeConstraints { make in
make.top.equalTo(baseView.snp.bottom).offset(8)
make.leading.equalToSuperview()
}

progressImageView.snp.makeConstraints { make in
make.centerY.equalTo(progressTitleLabel)
make.leading.equalTo(progressTitleLabel.snp.trailing)
make.adjustedWidthEqualTo(16)
make.height.equalTo(progressImageView.snp.width)
}

progressBar.snp.makeConstraints { make in
make.top.equalTo(progressTitleLabel.snp.bottom).offset(4)
make.horizontalEdges.equalToSuperview()
make.bottom.equalToSuperview().offset(-16)
make.adjustedHeightEqualTo(12)
}
}

func setupAction() {
copyLinkButton.addTarget(self, action: #selector(copyLinkButtonDidTap(_:)), for: .touchUpInside)
}
}

// MARK: - Action Method

private extension CommunityInviteCell {
@objc func copyLinkButtonDidTap(_ sender: UIButton) {
guard sender.configuration?.title == Constant.defaultTitle else { return }

copyLinkClosure?()

showCopyLinkCompletedState()
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self.resetCopyLinkButton()
}
}
}

// MARK: - Computed Property

private extension CommunityInviteCell {
var communityImageView: UIImageView { baseView.communityImageView }
var titleLabel: UILabel { baseView.titleLabel }
var copyLinkButton: UIButton { baseView.primaryButton }
}

// MARK: - Constant

private extension CommunityInviteCell {
enum Constant {
static let defaultTitle = "팬 더 데려오기"
static let copyLinkCompletedTitle = "링크 복사 완료"
}
}
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 adding accessibility support

The cell doesn't implement accessibility features. Consider enhancing it with proper accessibility labels, hints, and traits for users who rely on VoiceOver or other assistive technologies.

For example, you could add in the configure method:

func configure(
    image: UIImage?,
    title: String,
    progress: Float,
    progressBarColor: UIColor
) {
    communityImageView.image = image
    
    titleLabel.text = title
    
    progressBar.progressTintColor = progressBarColor
    progressBar.setProgress(progress, animated: true)
+    
+    // Accessibility
+    let progressPercentage = Int(progress * 100)
+    contentView.isAccessibilityElement = true
+    contentView.accessibilityLabel = "\(title), 진행도 \(progressPercentage)%"
+    copyLinkButton.accessibilityLabel = Constant.defaultTitle
+    copyLinkButton.accessibilityHint = "탭하여 커뮤니티 초대 링크를 복사합니다"
}

// Also update in the helper methods:
func showCopyLinkCompletedState() {
    // Existing code...
+    copyLinkButton.accessibilityLabel = Constant.copyLinkCompletedTitle
+    copyLinkButton.accessibilityTraits.insert(.notEnabled)
}

func resetCopyLinkButton() {
    // Existing code...
+    copyLinkButton.accessibilityLabel = Constant.defaultTitle
+    copyLinkButton.accessibilityTraits.remove(.notEnabled)
}
📝 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
final class CommunityInviteCell: UICollectionViewCell {
// MARK: - UIComponent
private let baseView = CommunityCellBaseView()
private let progressTitleLabel = UILabel().then {
$0.attributedText = "진행도".pretendardString(with: .caption1)
}
private let progressImageView = UIImageView(image: .icFan)
private let progressBar = UIProgressView(progressViewStyle: .bar).then {
$0.trackTintColor = .gray200
$0.layer.cornerRadius = 8
$0.setProgress(0.0, animated: false)
$0.clipsToBounds = true
}
// MARK: - Property
var copyLinkClosure: (() -> Void)?
// MARK: - Initializer
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
setupConstraint()
setupAction()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
communityImageView.image = nil
copyLinkClosure = nil
resetCopyLinkButton()
}
func configure(
image: UIImage?,
title: String,
progress: Float,
progressBarColor: UIColor
) {
communityImageView.image = image
titleLabel.text = title
progressBar.progressTintColor = progressBarColor
progressBar.setProgress(progress, animated: true)
}
}
// MARK: - Helper Method
private extension CommunityInviteCell {
func resetCopyLinkButton() {
var config = copyLinkButton.configuration
config?.attributedTitle = Constant.defaultTitle.pretendardString(with: .body3)
config?.image = nil
copyLinkButton.configuration = config
}
func showCopyLinkCompletedState() {
var config = copyLinkButton.configuration
config?.attributedTitle = Constant.copyLinkCompletedTitle.pretendardString(with: .body3)
config?.image = .icCheck
config?.imagePlacement = .leading
copyLinkButton.configuration = config
}
}
// MARK: - Setup Method
private extension CommunityInviteCell {
func setupView() {
contentView.addSubviews(
baseView,
progressTitleLabel,
progressImageView,
progressBar
)
resetCopyLinkButton()
}
func setupConstraint() {
baseView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(16)
make.horizontalEdges.equalToSuperview()
}
progressTitleLabel.snp.makeConstraints { make in
make.top.equalTo(baseView.snp.bottom).offset(8)
make.leading.equalToSuperview()
}
progressImageView.snp.makeConstraints { make in
make.centerY.equalTo(progressTitleLabel)
make.leading.equalTo(progressTitleLabel.snp.trailing)
make.adjustedWidthEqualTo(16)
make.height.equalTo(progressImageView.snp.width)
}
progressBar.snp.makeConstraints { make in
make.top.equalTo(progressTitleLabel.snp.bottom).offset(4)
make.horizontalEdges.equalToSuperview()
make.bottom.equalToSuperview().offset(-16)
make.adjustedHeightEqualTo(12)
}
}
func setupAction() {
copyLinkButton.addTarget(self, action: #selector(copyLinkButtonDidTap(_:)), for: .touchUpInside)
}
}
// MARK: - Action Method
private extension CommunityInviteCell {
@objc func copyLinkButtonDidTap(_ sender: UIButton) {
guard sender.configuration?.title == Constant.defaultTitle else { return }
copyLinkClosure?()
showCopyLinkCompletedState()
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self.resetCopyLinkButton()
}
}
}
// MARK: - Computed Property
private extension CommunityInviteCell {
var communityImageView: UIImageView { baseView.communityImageView }
var titleLabel: UILabel { baseView.titleLabel }
var copyLinkButton: UIButton { baseView.primaryButton }
}
// MARK: - Constant
private extension CommunityInviteCell {
enum Constant {
static let defaultTitle = "팬 더 데려오기"
static let copyLinkCompletedTitle = "링크 복사 완료"
}
}
final class CommunityInviteCell: UICollectionViewCell {
// MARK: - UIComponent
private let baseView = CommunityCellBaseView()
private let progressTitleLabel = UILabel().then {
$0.attributedText = "진행도".pretendardString(with: .caption1)
}
private let progressImageView = UIImageView(image: .icFan)
private let progressBar = UIProgressView(progressViewStyle: .bar).then {
$0.trackTintColor = .gray200
$0.layer.cornerRadius = 8
$0.setProgress(0.0, animated: false)
$0.clipsToBounds = true
}
// MARK: - Property
var copyLinkClosure: (() -> Void)?
// MARK: - Initializer
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
setupConstraint()
setupAction()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
communityImageView.image = nil
copyLinkClosure = nil
resetCopyLinkButton()
}
func configure(
image: UIImage?,
title: String,
progress: Float,
progressBarColor: UIColor
) {
communityImageView.image = image
titleLabel.text = title
progressBar.progressTintColor = progressBarColor
progressBar.setProgress(progress, animated: true)
// Accessibility
let progressPercentage = Int(progress * 100)
contentView.isAccessibilityElement = true
contentView.accessibilityLabel = "\(title), 진행도 \(progressPercentage)%"
copyLinkButton.accessibilityLabel = Constant.defaultTitle
copyLinkButton.accessibilityHint = "탭하여 커뮤니티 초대 링크를 복사합니다"
}
}
// MARK: - Helper Method
private extension CommunityInviteCell {
func resetCopyLinkButton() {
var config = copyLinkButton.configuration
config?.attributedTitle = Constant.defaultTitle.pretendardString(with: .body3)
config?.image = nil
copyLinkButton.configuration = config
copyLinkButton.accessibilityLabel = Constant.defaultTitle
copyLinkButton.accessibilityTraits.remove(.notEnabled)
}
func showCopyLinkCompletedState() {
var config = copyLinkButton.configuration
config?.attributedTitle = Constant.copyLinkCompletedTitle.pretendardString(with: .body3)
config?.image = .icCheck
config?.imagePlacement = .leading
copyLinkButton.configuration = config
copyLinkButton.accessibilityLabel = Constant.copyLinkCompletedTitle
copyLinkButton.accessibilityTraits.insert(.notEnabled)
}
}
// MARK: - Setup Method
private extension CommunityInviteCell {
func setupView() {
contentView.addSubviews(
baseView,
progressTitleLabel,
progressImageView,
progressBar
)
resetCopyLinkButton()
}
func setupConstraint() {
baseView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(16)
make.horizontalEdges.equalToSuperview()
}
progressTitleLabel.snp.makeConstraints { make in
make.top.equalTo(baseView.snp.bottom).offset(8)
make.leading.equalToSuperview()
}
progressImageView.snp.makeConstraints { make in
make.centerY.equalTo(progressTitleLabel)
make.leading.equalTo(progressTitleLabel.snp.trailing)
make.adjustedWidthEqualTo(16)
make.height.equalTo(progressImageView.snp.width)
}
progressBar.snp.makeConstraints { make in
make.top.equalTo(progressTitleLabel.snp.bottom).offset(4)
make.horizontalEdges.equalToSuperview()
make.bottom.equalToSuperview().offset(-16)
make.adjustedHeightEqualTo(12)
}
}
func setupAction() {
copyLinkButton.addTarget(self, action: #selector(copyLinkButtonDidTap(_:)), for: .touchUpInside)
}
}
// MARK: - Action Method
private extension CommunityInviteCell {
@objc func copyLinkButtonDidTap(_ sender: UIButton) {
guard sender.configuration?.title == Constant.defaultTitle else { return }
copyLinkClosure?()
showCopyLinkCompletedState()
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self.resetCopyLinkButton()
}
}
}
// MARK: - Computed Property
private extension CommunityInviteCell {
var communityImageView: UIImageView { baseView.communityImageView }
var titleLabel: UILabel { baseView.titleLabel }
var copyLinkButton: UIButton { baseView.primaryButton }
}
// MARK: - Constant
private extension CommunityInviteCell {
enum Constant {
static let defaultTitle = "팬 더 데려오기"
static let copyLinkCompletedTitle = "링크 복사 완료"
}
}


extension DTO.Response {
struct IsUserRegistered: Decodable {
let commnunityName: String?
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix typo in property name 'commnunityName'

There's a typo in the property name commnunityName (extra 'n'). This should be corrected to communityName for consistency and to avoid confusion.

struct IsUserRegistered: Decodable {
-   let commnunityName: String?
+   let communityName: String?
}
📝 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
let commnunityName: String?
struct IsUserRegistered: Decodable {
let communityName: String?
}

Comment on lines +25 to +31
static func toDomain(_ response: DTO.Response.IsUserRegistered) -> CommunityRegistration {
guard let teamName = response.commnunityName else {
return CommunityRegistration(team: nil, hasRegisteredTeam: false)
}

return CommunityRegistration(team: LCKTeam(rawValue: teamName), hasRegisteredTeam: true)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider handling invalid team names.

The current implementation handles the case when commnunityName is nil, but doesn't address what happens if the name doesn't match any valid LCKTeam enum value. This could result in an unexpected nil value being passed to the CommunityRegistration constructor.

 static func toDomain(_ response: DTO.Response.IsUserRegistered) -> CommunityRegistration {
     guard let teamName = response.commnunityName else {
         return CommunityRegistration(team: nil, hasRegisteredTeam: false)
     }
     
-    return CommunityRegistration(team: LCKTeam(rawValue: teamName), hasRegisteredTeam: true)
+    let team = LCKTeam(rawValue: teamName)
+    return CommunityRegistration(team: team, hasRegisteredTeam: team != nil)
 }

}

private let communityViewController = CommunityViewController().then {
private let communityViewController = CommunityViewController(viewModel: CommunityViewModel(useCase: MockCommunityUseCaseImpl())).then {
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

Replace mock implementation with real implementation for production.

Using MockCommunityUseCaseImpl directly in the TabBarController could lead to issues in production. Consider creating a factory or dependency injection container to provide the appropriate implementation based on the current environment.

-    private let communityViewController = CommunityViewController(viewModel: CommunityViewModel(useCase: MockCommunityUseCaseImpl())).then {
+    private let communityViewController = CommunityViewController(viewModel: CommunityViewModel(useCase: DIContainer.shared.resolve(CommunityUseCase.self))).then {

Alternatively, if a DI container isn't available:

-    private let communityViewController = CommunityViewController(viewModel: CommunityViewModel(useCase: MockCommunityUseCaseImpl())).then {
+    private let communityViewController = CommunityViewController(viewModel: CommunityViewModel(useCase: CommunityUseCaseImpl(repository: CommunityRepositoryImpl()))).then {

🏁 Script executed:

#!/bin/bash
# Check if there's a real implementation of CommunityUseCase
echo "Searching for real implementation of CommunityUseCase"
rg -t swift "class \w+CommunityUseCase\w*Impl\b.*CommunityUseCase" --no-ignore-vcs
echo "Searching for DIContainer implementation"
rg -t swift "class DIContainer" --no-ignore-vcs

Length of output: 457


Action Needed: Replace Mock Use Case with Production-Ready Implementation

The CommunityViewController in Wable-iOS/Presentation/TabBar/TabBarController.swift is currently configured with MockCommunityUseCaseImpl. Our verification confirms that the only implementation available is the mock (found in Wable-iOS/Domain/UseCase/Community/CommunityUseCase.swift), and no production implementation or DI container (DIContainer) exists in the repository. To prevent potential issues in production, please refactor this dependency. Consider the following approaches:

  • Using Dependency Injection:
    If you plan to use a DI container, implement one (e.g., a DIContainer class) and update the code as follows:

    -    private let communityViewController = CommunityViewController(viewModel: CommunityViewModel(useCase: MockCommunityUseCaseImpl())).then {
    +    private let communityViewController = CommunityViewController(viewModel: CommunityViewModel(useCase: DIContainer.shared.resolve(CommunityUseCase.self))).then {
  • Direct Replacement:
    Alternatively, directly replace the mock with a real implementation:

    -    private let communityViewController = CommunityViewController(viewModel: CommunityViewModel(useCase: MockCommunityUseCaseImpl())).then {
    +    private let communityViewController = CommunityViewController(viewModel: CommunityViewModel(useCase: CommunityUseCaseImpl(repository: CommunityRepositoryImpl()))).then {

Please ensure that a production-ready version of CommunityUseCase is created and integrated, as continuing to use the mock could lead to runtime issues in production.

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

Comment on lines +21 to +22
$0.refreshControl = UIRefreshControl()
$0.alwaysBounceVertical = true
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Connect the refresh control to an action.

The refresh control is initialized but not connected to any action. You should add a closure or target-action to handle the refresh event.

 lazy var collectionView = UICollectionView(
     frame: .zero,
     collectionViewLayout: collectionViewLayout
 ).then {
     $0.refreshControl = UIRefreshControl()
+    $0.refreshControl?.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
     $0.alwaysBounceVertical = true
 }

+@objc private func handleRefresh() {
+    // Notify the view model or delegate about the refresh action
+    // Then end refreshing when data is loaded
+    // collectionView.refreshControl?.endRefreshing()
+}
📝 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
$0.refreshControl = UIRefreshControl()
$0.alwaysBounceVertical = true
lazy var collectionView = UICollectionView(
frame: .zero,
collectionViewLayout: collectionViewLayout
).then {
$0.refreshControl = UIRefreshControl()
$0.refreshControl?.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
$0.alwaysBounceVertical = true
}
@objc private func handleRefresh() {
// Notify the view model or delegate about the refresh action
// Then end refreshing when data is loaded
// collectionView.refreshControl?.endRefreshing()
}

Comment on lines +21 to +28
init(teamName: String) {
self.teamName = teamName

super.init(nibName: nil, bundle: nil)

modalTransitionStyle = .crossDissolve
modalPresentationStyle = .overFullScreen
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add a way to dismiss the modal view controller

This modal view controller is presented with .overFullScreen style but doesn't provide any way for the user to dismiss it. Consider adding a dismiss button or a tap gesture recognizer.

    init(teamName: String) {
        self.teamName = teamName
        
        super.init(nibName: nil, bundle: nil)
        
        modalTransitionStyle = .crossDissolve
        modalPresentationStyle = .overFullScreen
    }
+    
+    private func addDismissGesture() {
+        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissView))
+        view.addGestureRecognizer(tapGesture)
+    }
+    
+    @objc private func dismissView() {
+        dismiss(animated: true)
+    }

And in viewDidLoad():

    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupView()
+        addDismissGesture()
    }

Comment on lines +81 to +95
input.register
.compactMap { communityListRelay.value[$0].team }
.handleEvents(receiveOutput: { team in
registrationRelay.send(.init(team: team, hasRegisteredTeam: true))
})
.withUnretained(self)
.flatMap { owner, team -> AnyPublisher<Double, Never> in
return owner.useCase.register(for: team)
.map { value -> Double? in
return value
}
.catch { error -> AnyPublisher<Double?, Never> in
WableLogger.log("\(error.localizedDescription)", for: .error)
return .just(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

Add robust error handling or user feedback for registration failures.
Right now, the code logs the error and returns nil to skip updating, but the user receives no feedback if registration fails. Consider showing an alert or user-facing message to clarify that the registration didn’t succeed.

Comment on lines +95 to +98
cell.registerCommunityClosure = { [weak self] in
self?.showRegisterSheet(for: indexPath.item)
}
}
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

Use safer indexing for registering.
Here you directly pass indexPath.item to showRegisterSheet(for:) and then to registerRelay.send(item). Ensure that indexPath.item is valid, or handle edge cases to avoid potential out-of-bounds issues if the data source changes unexpectedly.


🏁 Script executed:

#!/bin/bash
# Validate that no calls to `showRegisterSheet` or `registerRelay.send` occur if indexPath.item
# is out of range for collectionView data.
rg "showRegisterSheet|registerRelay\.send"

Length of output: 396


Ensure safe indexing when using indexPath.item for registration

The closure currently passes indexPath.item directly into both showRegisterSheet(for:) and registerRelay.send(item), without verifying that the index is within the valid bounds of your collection view data. This could lead to out-of-bounds errors if the data source changes unexpectedly.

Action Items:

  • Add a bounds check against your collection view’s data source count before calling showRegisterSheet(for:).
  • Consider refactoring the closure to safely handle cases where indexPath.item is not valid, such as using a guard or if-statement to prevent further execution when the index is out-of-range.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between f22006e and 14daa0e.

📒 Files selected for processing (2)
  • Wable-iOS/Data/RepositoryImpl/CommunityRepositoryImpl.swift (2 hunks)
  • Wable-iOS/Infra/Network/TargetType/CommunityTargetType.swift (2 hunks)
🔇 Additional comments (3)
Wable-iOS/Data/RepositoryImpl/CommunityRepositoryImpl.swift (1)

42-46: New method implementation looks good

The new isUserRegistered method is well-implemented following the repository pattern and uses proper error handling.

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

15-17: Enum cases properly updated

The enum cases have been correctly updated to reflect the API changes. The previous typo in isUserRegistered has been fixed.


23-29: Version change in endpoint

The endpoint for community registration has been updated from v1 to v2 (/v2/community/prein). This version change might indicate breaking changes in the API contract.

Verify that this version change was intentional and documented in your API specifications. Also ensure that all other components that may use this endpoint are updated accordingly.

Comment on lines +20 to 31
func updateRegister(communityName: String) -> AnyPublisher<Double, WableError> {
return provider.request(
.updatePreRegister(
request: DTO.Request.UpdatePreRegister(
communityName: communityName.rawValue
.updateRegister(
request: DTO.Request.UpdateRegister(
communityName: communityName
)
),
for: DTO.Response.Empty.self
for: DTO.Response.RegisterResult.self
)
.asVoid()
.map { $0.registrationRate }
.mapWableError()
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

API contract change in updateRegister method

The method has been renamed from updatePreRegister to updateRegister and now returns a registration rate (Double) instead of Void. This is a significant API change that would require updates in all calling code.

Make sure all existing callers of this method are updated to handle the new return type. Consider adding inline documentation to explain what the registration rate represents and how it should be interpreted.

Comment on lines +49 to 55
case .updateRegister:
return .patch
case .fetchCommunityList:
return .get
case .isUserRegistered:
return .get
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

HTTP method change from POST to PATCH

The HTTP method for updateRegister has changed from POST to PATCH. This is a significant change in REST semantics - PATCH is typically used for partial updates while POST is for creating new resources.

Ensure this change aligns with the API's intended behavior. If the endpoint now updates an existing registration instead of creating a new one, then PATCH is appropriate. Otherwise, consider whether POST would be more semantically correct.

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.

LGTM

Comment on lines +68 to +72
static let text = """
팀별 커뮤니티 공간을 준비중이에요. 팀별 일정 이상의
팬이 모여야 팀별 공간이 열립니다.
*계정 1개당 1개의 팀별 공간에만 참여 가능해요!
"""
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 +156 to +160
private extension CommunityInviteCell {
var communityImageView: UIImageView { baseView.communityImageView }
var titleLabel: UILabel { baseView.titleLabel }
var copyLinkButton: UIButton { baseView.primaryButton }
}
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 +50 to +71
let backgroundView = UIView(backgroundColor: .wableWhite).then {
$0.layer.cornerRadius = 16
}

let imageView = UIImageView(image: .icCircleCheck)

let titleLabel = UILabel().then {
$0.attributedText = "신청을 완료했어요".pretendardString(with: .head1)
$0.textAlignment = .center
}

let descriptionLabel = UILabel().then {
$0.attributedText = """
\(teamName)팀을 응원하는 팬분들이 더 모여야
\(teamName) 라운지가 오픈돼요!
팬 더 데려오기를 통해 링크를 복사하여
함께 응원할 팬을 데려와주세요!
""".pretendardString(with: .body2)
$0.textColor = .gray700
$0.textAlignment = .center
$0.numberOfLines = 0
}
Copy link
Member

Choose a reason for hiding this comment

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

요건 나중에 같이 이야기해봅시당

@JinUng41 JinUng41 merged commit 7cde67d into develop Apr 11, 2025
1 check was pending
@JinUng41 JinUng41 deleted the feat/#161-community branch April 11, 2025 15:45
@github-project-automation github-project-automation bot moved this from In Review to Done in Wable-iOS Apr 11, 2025
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

Status: Done

Development

Successfully merging this pull request may close these issues.

[Feat] 커뮤니티 UI 구현

3 participants