-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] 커뮤니티 기능 구현 #167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feat] 커뮤니티 기능 구현 #167
Conversation
…o feat/#161-community
- 사전 신청이라는 의미에서 PreRegister가 적합할 수 있으나, 너무 네이밍이 길어진다고 판단하여 좀 더 간결한 형태를 유지하고자 변경함.
- MockUseCase으로 구현
|
Caution Review failedThe pull request is closed. WalkthroughThis 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
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
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
Assessment against linked issues
Poem
Tip ⚡💬 Agentic Chat (Pro Plan, General Availability)
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (3)
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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 theaddSubviewsmethod for consistency with other files.Other files in the codebase use a plural
addSubviewsmethod for adding multiple views rather than individualaddSubviewcalls.For consistency with files like
CommunityCellBaseView.swiftandCommunityInviteCell.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 headerThere's an extra space in the filename declaration:
CommunityInviteCell .swiftshould beCommunityInviteCell.swift.-// CommunityInviteCell .swift +// CommunityInviteCell.swift
142-151: Consider tracking button state more explicitlyThe 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 keyThere's a potential semantic mismatch between the property name
registrationRate(suggesting a percentage) and the JSON keycommunityNum(suggesting a count or number). This inconsistency could cause confusion for future developers maintaining this code.Consider either:
- Renaming the property to match the semantic meaning of the API response field
- 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 supportThe 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 methodThe 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 propertiesThe model has two Boolean properties with similar names:
isRegisteredandhasRegisteredCommunity. 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 parsingEven 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
UpdatePreRegistertoUpdateRegister, 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 tocommunityNameto 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 usingscaleAspectFillfor community imagesThe current
scaleAspectFitcontent mode might leave empty spaces around the image. For community logos or team images,scaleAspectFillwithclipsToBounds = truewould 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 dimensionsFor 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 usingadjustedHeightEqualTofor 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 constantsThe 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 viewYou're using the
addSubviewsextension method for adding subviews to the background view, but then using the standardaddSubviewmethod 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 forupdateRegisterThe endpoint for the
updateRegistercase 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 forupdateRegisterThe HTTP method for
updateRegisterhas 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 .getWable-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 methodcollectionViewDidRefreshonly 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.
InsidecopyLinkClosure, you callshowCopyLinkCompleteSheet()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 havedataSource?.apply(snapshot, animatingDifferences: true), which is visually appealing but can become overwhelming if invoked often. Optionally, switch tofalseif 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 consistentlyThe 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
📒 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 organizationThe 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 heightThe 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 methodThe 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 managementThe 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 subviewsEffective use of the
addSubviewsextension method to add multiple views at once, enhancing code readability.
108-132: Well-organized constraints with effective use of SnapKitThe 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 patternThe initialization follows proper UICollectionViewCell patterns with separate setup methods called in a clear order.
156-160: Clean use of computed propertiesThe 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 prepareForReuseGood practice to set the closure to nil in prepareForReuse to prevent memory leaks and stale references.
79-83: Good practice using optional chaining for closuresThe 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 encapsulationUsing 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
CommunityRegistrationstruct is well-designed and clearly represents a user's registration status for a community. TheinitialState()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
teamproperty is immutable (let) whileregistrationRateis mutable (var). Is this intentional? IfregistrationRateneeds to be updated during the lifecycle of aCommunityinstance, this makes sense. Otherwise, consider usingletfor consistency.Wable-iOS/Infra/Network/DTO/Response/Community/FetchCommunites.swift (1)
16-21: Good use of CodingKeys for mapping.The addition of
CodingKeysto map the backend fieldcommunityNumto the more semanticregistrationRateproperty 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-vcsLength 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
updatePreRegistertoupdateRegisterThe function has been renamed from
updatePreRegistertoupdateRegisterand now returns a registration rate instead of void. Ensure that all callers of this function have been updated to handle the returnedDoublevalue.
🏁 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 swiftLength of output: 1209
API Migration Verified: updateRegister is Fully Adopted
The search confirms that all references to the deprecated
updatePreRegisterhave been removed. All call sites now correctly invokeupdateRegisterand, 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., inCommunityUseCase.swift).Wable-iOS/Domain/UseCase/Community/CommunityUseCase.swift (4)
11-15: Well-designed protocol with clear responsibilitiesThe 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 layerGood 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 developmentThe 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
LCKTeamto a string value when calling the repository. Make sure therawValuematches 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 swiftLength of output: 776
Team Name Mapping Verified
The repository call in
register(for communityTeam:)correctly maps theLCKTeamenum to its raw string value. The enum inWable-iOS/Domain/Enum/LCKTeam.swiftdefines 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.swiftThis 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 featureThe 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 addedThese 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 addedThese 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 addedThese 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 directoryNew 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 RegistrationThe CommunityRegistration entity has been added to the domain layer, suggesting a structured approach to handling registration data.
1241-1241: Community use case implementation addedThe CommunityUseCase has been added to the domain layer's UseCase directory, properly organizing business logic.
1320-1348: Well-organized project structure for Community featureThe 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 structuredGood separation between data model (CommunityItem) and presentation logic (CommunityViewModel).
| 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 = "링크 복사 완료" | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| let commnunityName: String? | |
| struct IsUserRegistered: Decodable { | |
| let communityName: String? | |
| } |
| 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) | ||
| } |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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-vcsLength 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., aDIContainerclass) 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.
| $0.refreshControl = UIRefreshControl() | ||
| $0.alwaysBounceVertical = true |
There was a problem hiding this comment.
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.
| $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() | |
| } |
| init(teamName: String) { | ||
| self.teamName = teamName | ||
|
|
||
| super.init(nibName: nil, bundle: nil) | ||
|
|
||
| modalTransitionStyle = .crossDissolve | ||
| modalPresentationStyle = .overFullScreen | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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()
}| 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) | ||
| } |
There was a problem hiding this comment.
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.
| cell.registerCommunityClosure = { [weak self] in | ||
| self?.showRegisterSheet(for: indexPath.item) | ||
| } | ||
| } |
There was a problem hiding this comment.
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.itemis not valid, such as using a guard or if-statement to prevent further execution when the index is out-of-range.
There was a problem hiding this 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
📒 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 goodThe new
isUserRegisteredmethod 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 updatedThe enum cases have been correctly updated to reflect the API changes. The previous typo in
isUserRegisteredhas been fixed.
23-29: Version change in endpointThe 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.
| 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() | ||
| } |
There was a problem hiding this comment.
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.
| case .updateRegister: | ||
| return .patch | ||
| case .fetchCommunityList: | ||
| return .get | ||
| case .isUserRegistered: | ||
| return .get | ||
| } |
There was a problem hiding this comment.
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.
youz2me
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
| static let text = """ | ||
| 팀별 커뮤니티 공간을 준비중이에요. 팀별 일정 이상의 | ||
| 팬이 모여야 팀별 공간이 열립니다. | ||
| *계정 1개당 1개의 팀별 공간에만 참여 가능해요! | ||
| """ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
헉 굿
| private extension CommunityInviteCell { | ||
| var communityImageView: UIImageView { baseView.communityImageView } | ||
| var titleLabel: UILabel { baseView.titleLabel } | ||
| var copyLinkButton: UIButton { baseView.primaryButton } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 ... 이렇게 할 수도 있네요 굿
| 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 | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요건 나중에 같이 이야기해봅시당
…o feat/#161-community
👻 PULL REQUEST
📄 작업 내용
🔗 연결된 이슈
Summary by CodeRabbit
Summary by CodeRabbit
New Features
Bug Fixes
Refactor