-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] 홈 화면 기능 구현 #179
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] 홈 화면 기능 구현 #179
Conversation
WalkthroughThis update introduces new use cases and expands the Home screen's interactive capabilities. Three new use case files— Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant HomeViewController
participant HomeViewModel
participant UseCases
participant Repository
User->>HomeViewController: Tap on content cell (ghost/ban/report/delete)
HomeViewController->>HomeViewModel: Send event via subject
HomeViewModel->>UseCases: Call appropriate use case (ghost/report/ban/delete)
UseCases->>Repository: Perform repository operation
Repository-->>UseCases: Return result (success/failure)
UseCases-->>HomeViewModel: Complete publisher
HomeViewModel-->>HomeViewController: Publish updated state/output
HomeViewController-->>User: Update UI (sheet, toast, cell state)
Assessment against linked issues
Possibly related PRs
Poem
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
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: 5
🔭 Outside diff range comments (1)
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (1)
185-201: 💡 Verification agent🧩 Analysis chain
Added new tap handlers for content cell configuration.
You've added empty handler closures for setting button, profile image view, and ghost button taps. This enhancement aligns with the broader implementation of content moderation features. Consider implementing these handlers to mirror the functionality in HomeViewController for consistency across screens.
🏁 Script executed:
#!/bin/bash # Check if HomeViewController implements these handlers rg -A 5 "settingButtonTapHandler|profileImageViewTapHandler|ghostButtonTapHandler" --type swiftLength of output: 7171
Implement missing tap handlers in HomeDetailViewController
The
settingButtonTapHandler,profileImageViewTapHandler, andghostButtonTapHandlerclosures in HomeDetailViewController.swift (around lines 185–201) are currently empty. To keep the experience consistent with HomeViewController, please implement them using the same logic:• settingButtonTapHandler: present WableBottomSheetController and add delete/report actions for the post author
• profileImageViewTapHandler: push ProfileViewController
• ghostButtonTapHandler: present WableSheetViewController with the moderation prompt and actionsLocations to update:
- Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift, inside the
cell.configureCell(…)call (lines 185–201)
🧹 Nitpick comments (6)
Wable-iOS/Domain/UseCase/Home/CreateReportUseCase.swift (1)
22-26: Clean implementation of the execute methodThe method properly delegates to the repository layer, maintaining separation of concerns and following the established pattern in your codebase.
Consider adding basic validation for the input parameters (e.g., checking if text is empty) if this validation isn't already handled at the UI or repository level:
func execute(nickname: String, text: String) -> AnyPublisher<Void, WableError> { + guard !text.isEmpty else { + return Fail(error: WableError.invalidInput).eraseToAnyPublisher() + } return repository.createReport(nickname: nickname, text: text) }Wable-iOS/Domain/UseCase/Home/FetchGhostUseCase.swift (1)
23-30: Consider more descriptive method naming and string mappingThe implementation works correctly but has some opportunities for improvement.
Consider these refinements:
- Rename the method to better reflect its action (e.g.,
reduceGhostormarkAsGhostinstead of the genericexecute)- Move the string mapping logic to a dedicated mapper or to the repository layer:
-func execute(type: PostType, targetID: Int, userID: Int) -> AnyPublisher<Void, WableError> { +func reduceGhost(type: PostType, targetID: Int, userID: Int) -> AnyPublisher<Void, WableError> { return repository.postGhostReduction( - alarmTriggerType: type == .comment ? "commentGhost" : "contentGhost", + alarmTriggerType: type.ghostTriggerType, alarmTriggerID: targetID, targetMemberID: userID, reason: "" ) }With an extension on PostType:
extension PostType { var ghostTriggerType: String { return self == .comment ? "commentGhost" : "contentGhost" } }Why is the
reasonparameter always an empty string? If it's not used, consider removing it or documenting why it's empty.Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (1)
252-261: Consider consistent parameter ordering in configureCell.The order of parameters in the method signature is different from the order in which they're assigned to properties in the method body. For better maintainability, consider keeping them in the same order.
func configureCell( info: ContentInfo, postType: AuthorType, cellType: CellType = .list, likeButtonTapHandler: (() -> Void)?, + profileImageViewTapHandler: (() -> Void)?, settingButtonTapHandler: (() -> Void)?, - profileImageViewTapHandler: (() -> Void)?, ghostButtonTapHandler: (() -> Void)? ) { self.cellType = cellType self.likeButtonTapHandler = likeButtonTapHandler + self.profileImageViewTapHandler = profileImageViewTapHandler self.ghostButtonTapHandler = ghostButtonTapHandler - self.profileImageViewTapHandler = profileImageViewTapHandler self.settingButtonTapHandler = settingButtonTapHandler // ...Wable-iOS/Presentation/Home/View/HomeViewController.swift (2)
168-169: Fix the unused parameter warning.SwiftLint identified an unused parameter in the closure.
- let homeCellRegistration = CellRegistration<ContentCollectionViewCell, Content> { [weak self] cell, indexPath, itemID in + let homeCellRegistration = CellRegistration<ContentCollectionViewCell, Content> { [weak self] cell, _, itemID in🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 168-168: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
403-413: Fix toast message typo and simplify conditional logic.There's a typo in the toast message and the conditional logic could be simplified.
output.isReportSucceed .receive(on: DispatchQueue.main) .sink { isSucceed in let toast = ToastView( status: .complete, - message: "신고 접수가 완료되었어요.\n24시간 이내에 조치할 예정이예요." + message: "신고 접수가 완료되었어요.\n24시간 이내에 조치할 예정이에요." ) - isSucceed ? toast.show() : nil + if isSucceed { + toast.show() + } } .store(in: cancelBag)Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (1)
16-40: Constructor growing to ≥10 dependencies – consider grouping use casesThe initializer now requires ten separate use-case injections. This is a red flag for the Interface Segregation & Single-Responsibility principles:
• The view-model is directly aware of many domain concepts (ghosting, banning, reporting, …).
• Tests and call-sites become increasingly verbose.Consider introducing a small facade/struct (e.g.
HomeActionProvider) that bundles moderation-related actions. That keeps the view-model signature stable while you evolve the feature set.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
Wable-iOS.xcodeproj/project.pbxproj(6 hunks)Wable-iOS/Domain/Entity/Opacity.swift(1 hunks)Wable-iOS/Domain/UseCase/Home/CreateBannedUseCase.swift(1 hunks)Wable-iOS/Domain/UseCase/Home/CreateReportUseCase.swift(1 hunks)Wable-iOS/Domain/UseCase/Home/FetchGhostUseCase.swift(1 hunks)Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift(1 hunks)Wable-iOS/Presentation/Home/View/HomeViewController.swift(4 hunks)Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift(3 hunks)Wable-iOS/Presentation/TabBar/TabBarController.swift(1 hunks)Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift(4 hunks)Wable-iOS/Presentation/WableComponent/ViewController/WableSheetViewController.swift(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
Wable-iOS/Domain/UseCase/Home/CreateReportUseCase.swift (2)
Wable-iOS/Domain/UseCase/Home/CreateBannedUseCase.swift (1)
execute(23-25)Wable-iOS/Data/RepositoryImpl/ReportRepositoryImpl.swift (1)
createReport(20-32)
Wable-iOS/Domain/UseCase/Home/FetchGhostUseCase.swift (1)
Wable-iOS/Data/RepositoryImpl/GhostRepositoryImpl.swift (1)
postGhostReduction(18-37)
Wable-iOS/Domain/UseCase/Home/CreateBannedUseCase.swift (2)
Wable-iOS/Domain/UseCase/Home/CreateReportUseCase.swift (1)
execute(23-25)Wable-iOS/Data/RepositoryImpl/ReportRepositoryImpl.swift (1)
createBan(34-47)
Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (4)
Wable-iOS/Presentation/WableComponent/View/PostUserInfoView.swift (1)
settingButtonDidTap(133-135)Wable-iOS/Presentation/WableComponent/Button/CommentButton.swift (1)
configureButton(49-82)Wable-iOS/Presentation/WableComponent/Button/LikeButton.swift (1)
configureButton(72-94)Wable-iOS/Presentation/WableComponent/Button/GhostButton.swift (1)
configureButton(72-102)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (4)
Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (1)
configureCell(248-310)Wable-iOS/Presentation/WableComponent/ViewController/WableSheetViewController.swift (1)
addActions(122-124)Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (1)
transform(66-316)Wable-iOS/Presentation/WableComponent/View/ToastView.swift (1)
show(144-160)
🪛 SwiftLint (0.57.0)
Wable-iOS/Presentation/Home/View/HomeViewController.swift
[Warning] 267-267: TODOs should be resolved (프로필 구현되는 대로 추가적인 설정 필요)
(todo)
[Warning] 168-168: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
🔇 Additional comments (14)
Wable-iOS/Presentation/WableComponent/ViewController/WableSheetViewController.swift (1)
65-65: Font style change from.head1to.head2looks good.The change from
.head1to.head2for the title label's attributed text style is consistent with UI changes described in the summary. The modification reduces the title's emphasis, which is appropriate for bottom sheets used in content moderation flows.Wable-iOS/Domain/Entity/Opacity.swift (1)
72-78: Good addition of non-mutatingreduced()method.This non-mutating version of the
reduce()method is a nice addition that follows functional programming principles. It allows getting a newOpacityinstance with reduced value without modifying the original, which is useful for previewing opacity changes or working with immutable state patterns.Wable-iOS.xcodeproj/project.pbxproj (2)
161-163: Files properly added to the project structureThe three new use case files (FetchGhostUseCase, CreateBannedUseCase, and CreateReportUseCase) are correctly added to the project's build sources.
459-461: File references properly configuredFile references for the new use cases are correctly set up in the project file.
Wable-iOS/Domain/UseCase/Home/CreateReportUseCase.swift (1)
12-18: Well-structured use case with proper dependency injectionThe class follows clean architecture principles by injecting the repository dependency, making it testable and maintainable.
Wable-iOS/Domain/UseCase/Home/FetchGhostUseCase.swift (1)
12-18: Well-structured use case with proper dependency injectionGood use of dependency injection and final class, following the clean architecture pattern in the project.
Wable-iOS/Domain/UseCase/Home/CreateBannedUseCase.swift (1)
1-27: Clean implementation of the CreateBannedUseCase.The implementation is well-structured following the clean architecture pattern. The use case properly delegates to the repository and handles asynchronous operations using Combine. Good job on the separation of concerns and dependency injection.
Wable-iOS/Presentation/TabBar/TabBarController.swift (1)
24-36: Well-structured dependency injection for HomeViewModel.The addition of new use cases to HomeViewModel is properly implemented with appropriate repository instantiations. The code follows the project's architecture pattern for dependency injection.
Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (3)
36-38: Good addition of tap handler properties for expanded interactivity.The new optional closure properties are well-designed for flexible event handling.
201-210: Handler implementations correctly use the new tap handlers.The methods have been properly updated to invoke the handler closures instead of containing debug messages or TODO comments.
289-290: Improved ghost button visibility logic.The change to hide the ghost button for own content or already ghosted content is a meaningful improvement to the user experience.
Wable-iOS/Presentation/Home/View/HomeViewController.swift (3)
35-44: Good addition of subjects for new content moderation actions.The new PassthroughSubjects for ghosting, deletion, banning, and reporting are well-designed for reactive handling of user interactions.
333-337: Good integration of new reactive inputs.The additional inputs for the new content moderation features are properly integrated with the view model transform method.
342-355: Well-implemented user state management.The bindings for admin status and active user ID follow a consistent pattern and properly update the controller's state.
| self.present(viewController, animated: true) | ||
| }, | ||
| profileImageViewTapHandler: { | ||
| // TODO: 프로필 구현되는 대로 추가적인 설정 필요 |
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
Resolve the TODO comment for profile implementation.
There's a TODO comment that should be addressed when the profile functionality is available.
🏁 Script executed:
#!/bin/bash
# Check if profile functionality is implemented elsewhere in the codebase
# Look for ProfileViewController implementation
echo "Checking ProfileViewController implementation:"
rg -A 3 "class ProfileViewController" --type swiftLength of output: 393
Implement Profile Integration and Remove TODO
ProfileViewController is already implemented (Wable-iOS/Presentation/Profile/ProfileViewController.swift), so please remove the TODO at line 267 in HomeViewController.swift and replace it with the actual code to present or push the profile screen.
• File: Wable-iOS/Presentation/Home/View/HomeViewController.swift
Line: ~267 – replace
// TODO: 프로필 구현되는 대로 추가적인 설정 필요with something like:
let profileVC = ProfileViewController()
navigationController?.pushViewController(profileVC, animated: true)🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 267-267: TODOs should be resolved (프로필 구현되는 대로 추가적인 설정 필요)
(todo)
| settingButtonTapHandler: { | ||
| let viewController = WableBottomSheetController() | ||
|
|
||
| if self.activeUserID == itemID.content.contentInfo.author.id { | ||
| viewController.addActions(WableBottomSheetAction(title: "삭제하기", handler: { | ||
| viewController.dismiss(animated: true, completion: { | ||
| let viewController = WableSheetViewController(title: "게시글을 삭제하시겠어요?", message: "게시글이 영구히 삭제됩니다.") | ||
| viewController.addActions( | ||
| WableSheetAction( | ||
| title: "취소", | ||
| style: .gray, | ||
| handler: { | ||
| viewController.dismiss(animated: true) | ||
| } | ||
| ), | ||
| WableSheetAction( | ||
| title: "삭제하기", | ||
| style: .primary, | ||
| handler: { | ||
| viewController.dismiss(animated: true, completion: { | ||
| self.didDeleteTappedSubject.send(itemID.content.id) | ||
| }) | ||
| } | ||
| ) | ||
| ) | ||
|
|
||
| self.present(viewController, animated: true) | ||
| }) | ||
| })) | ||
| } else if self.isActiveUserAdmin ?? false { | ||
| viewController.addActions(WableBottomSheetAction(title: "신고하기", handler: { | ||
| viewController.dismiss(animated: true, completion: { | ||
| let viewController = WableSheetViewController(title: "신고하시겠어요?") | ||
| viewController.addActions( | ||
| WableSheetAction( | ||
| title: "취소", | ||
| style: .gray, | ||
| handler: { | ||
| viewController.dismiss(animated: true) | ||
| } | ||
| ), | ||
| WableSheetAction( | ||
| title: "신고하기", | ||
| style: .primary, | ||
| handler: { | ||
| viewController.dismiss(animated: true, completion: { | ||
| self.didReportTappedSubject.send((itemID.content.contentInfo.title, itemID.content.contentInfo.text)) | ||
| }) | ||
| } | ||
| ) | ||
| ) | ||
|
|
||
| self.present(viewController, animated: true) | ||
| }) | ||
| }), WableBottomSheetAction(title: "밴하기", handler: { | ||
| self.didBannedTappedSubject.send((itemID.content.contentInfo.author.id, itemID.content.id)) | ||
| }) | ||
| ) | ||
| } else { | ||
| viewController.addActions(WableBottomSheetAction(title: "신고하기", handler: { | ||
| viewController.dismiss(animated: true, completion: { | ||
| let viewController = WableSheetViewController(title: "신고하시겠어요?") | ||
| viewController.addActions( | ||
| WableSheetAction( | ||
| title: "취소", | ||
| style: .gray, | ||
| handler: { | ||
| viewController.dismiss(animated: true) | ||
| } | ||
| ), | ||
| WableSheetAction( | ||
| title: "신고하기", | ||
| style: .primary, | ||
| handler: { | ||
| viewController.dismiss(animated: true, completion: { | ||
| self.didReportTappedSubject.send((itemID.content.contentInfo.title, itemID.content.contentInfo.text)) | ||
| }) | ||
| } | ||
| ) | ||
| ) | ||
|
|
||
| self.present(viewController, animated: true) | ||
| }) | ||
| })) | ||
| } | ||
|
|
||
| self.present(viewController, animated: true) | ||
| }, | ||
| profileImageViewTapHandler: { | ||
| // TODO: 프로필 구현되는 대로 추가적인 설정 필요 | ||
| let viewController = ProfileViewController() | ||
|
|
||
| self.navigationController?.pushViewController(viewController, animated: true) | ||
| }, | ||
| ghostButtonTapHandler: { | ||
| let viewController = WableSheetViewController(title: "와블의 온화한 문화를 해치는\n누군가를 발견하신 건가요?") | ||
| viewController.addActions( | ||
| WableSheetAction( | ||
| title: "고민할게요", | ||
| style: .gray, | ||
| handler: { | ||
| viewController.dismiss(animated: true) | ||
| } | ||
| ), | ||
| WableSheetAction( | ||
| title: "네 맞아요", | ||
| style: .primary, | ||
| handler: { | ||
| viewController.dismiss(animated: true, completion: { | ||
| self.didGhostTappedSubject.send((itemID.content.id, itemID.content.contentInfo.author.id)) | ||
| }) | ||
| } | ||
| ) | ||
| ) | ||
|
|
||
| self.present(viewController, animated: 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
Extract bottom sheet presentation logic into helper methods.
The cell configuration closure contains complex nested logic for presenting different bottom sheets based on user roles. This makes the code difficult to read and maintain.
Consider extracting this logic into separate helper methods to improve readability and maintainability:
// Add these helper methods
+ private func presentDeleteConfirmationSheet(for contentID: Int) {
+ let viewController = WableSheetViewController(title: "게시글을 삭제하시겠어요?", message: "게시글이 영구히 삭제됩니다.")
+ viewController.addActions(
+ WableSheetAction(
+ title: "취소",
+ style: .gray,
+ handler: { viewController.dismiss(animated: true) }
+ ),
+ WableSheetAction(
+ title: "삭제하기",
+ style: .primary,
+ handler: {
+ viewController.dismiss(animated: true) {
+ self.didDeleteTappedSubject.send(contentID)
+ }
+ }
+ )
+ )
+ self.present(viewController, animated: true)
+ }
+
+ private func presentReportConfirmationSheet(title: String, text: String) {
+ let viewController = WableSheetViewController(title: "신고하시겠어요?")
+ viewController.addActions(
+ WableSheetAction(
+ title: "취소",
+ style: .gray,
+ handler: { viewController.dismiss(animated: true) }
+ ),
+ WableSheetAction(
+ title: "신고하기",
+ style: .primary,
+ handler: {
+ viewController.dismiss(animated: true) {
+ self.didReportTappedSubject.send((title, text))
+ }
+ }
+ )
+ )
+ self.present(viewController, animated: true)
+ }
+
+ private func presentGhostConfirmationSheet(contentID: Int, authorID: Int) {
+ let viewController = WableSheetViewController(title: "와블의 온화한 문화를 해치는\n누군가를 발견하신 건가요?")
+ viewController.addActions(
+ WableSheetAction(
+ title: "고민할게요",
+ style: .gray,
+ handler: { viewController.dismiss(animated: true) }
+ ),
+ WableSheetAction(
+ title: "네 맞아요",
+ style: .primary,
+ handler: {
+ viewController.dismiss(animated: true) {
+ self.didGhostTappedSubject.send((contentID, authorID))
+ }
+ }
+ )
+ )
+ self.present(viewController, animated: true)
+ }Then simplify the cell configuration:
settingButtonTapHandler: {
let viewController = WableBottomSheetController()
if self.activeUserID == itemID.content.contentInfo.author.id {
viewController.addActions(WableBottomSheetAction(title: "삭제하기", handler: {
viewController.dismiss(animated: true) {
- let viewController = WableSheetViewController(title: "게시글을 삭제하시겠어요?", message: "게시글이 영구히 삭제됩니다.")
- viewController.addActions(
- WableSheetAction(
- title: "취소",
- style: .gray,
- handler: {
- viewController.dismiss(animated: true)
- }
- ),
- WableSheetAction(
- title: "삭제하기",
- style: .primary,
- handler: {
- viewController.dismiss(animated: true, completion: {
- self.didDeleteTappedSubject.send(itemID.content.id)
- })
- }
- )
- )
-
- self.present(viewController, animated: true)
+ self.presentDeleteConfirmationSheet(for: itemID.content.id)
}
}))
} else if self.isActiveUserAdmin ?? false {
viewController.addActions(WableBottomSheetAction(title: "신고하기", handler: {
viewController.dismiss(animated: true) {
- let viewController = WableSheetViewController(title: "신고하시겠어요?")
- viewController.addActions(
- WableSheetAction(
- title: "취소",
- style: .gray,
- handler: {
- viewController.dismiss(animated: true)
- }
- ),
- WableSheetAction(
- title: "신고하기",
- style: .primary,
- handler: {
- viewController.dismiss(animated: true, completion: {
- self.didReportTappedSubject.send((itemID.content.contentInfo.title, itemID.content.contentInfo.text))
- })
- }
- )
- )
-
- self.present(viewController, animated: true)
+ self.presentReportConfirmationSheet(title: itemID.content.contentInfo.title, text: itemID.content.contentInfo.text)
}
}), /* rest of the code */
}
}),
// ...
ghostButtonTapHandler: {
- let viewController = WableSheetViewController(title: "와블의 온화한 문화를 해치는\n누군가를 발견하신 건가요?")
- viewController.addActions(
- WableSheetAction(
- title: "고민할게요",
- style: .gray,
- handler: {
- viewController.dismiss(animated: true)
- }
- ),
- WableSheetAction(
- title: "네 맞아요",
- style: .primary,
- handler: {
- viewController.dismiss(animated: true, completion: {
- self.didGhostTappedSubject.send((itemID.content.id, itemID.content.contentInfo.author.id))
- })
- }
- )
- )
-
- self.present(viewController, animated: true)
+ self.presentGhostConfirmationSheet(contentID: itemID.content.id, authorID: itemID.content.contentInfo.author.id)
}📝 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.
| settingButtonTapHandler: { | |
| let viewController = WableBottomSheetController() | |
| if self.activeUserID == itemID.content.contentInfo.author.id { | |
| viewController.addActions(WableBottomSheetAction(title: "삭제하기", handler: { | |
| viewController.dismiss(animated: true, completion: { | |
| let viewController = WableSheetViewController(title: "게시글을 삭제하시겠어요?", message: "게시글이 영구히 삭제됩니다.") | |
| viewController.addActions( | |
| WableSheetAction( | |
| title: "취소", | |
| style: .gray, | |
| handler: { | |
| viewController.dismiss(animated: true) | |
| } | |
| ), | |
| WableSheetAction( | |
| title: "삭제하기", | |
| style: .primary, | |
| handler: { | |
| viewController.dismiss(animated: true, completion: { | |
| self.didDeleteTappedSubject.send(itemID.content.id) | |
| }) | |
| } | |
| ) | |
| ) | |
| self.present(viewController, animated: true) | |
| }) | |
| })) | |
| } else if self.isActiveUserAdmin ?? false { | |
| viewController.addActions(WableBottomSheetAction(title: "신고하기", handler: { | |
| viewController.dismiss(animated: true, completion: { | |
| let viewController = WableSheetViewController(title: "신고하시겠어요?") | |
| viewController.addActions( | |
| WableSheetAction( | |
| title: "취소", | |
| style: .gray, | |
| handler: { | |
| viewController.dismiss(animated: true) | |
| } | |
| ), | |
| WableSheetAction( | |
| title: "신고하기", | |
| style: .primary, | |
| handler: { | |
| viewController.dismiss(animated: true, completion: { | |
| self.didReportTappedSubject.send((itemID.content.contentInfo.title, itemID.content.contentInfo.text)) | |
| }) | |
| } | |
| ) | |
| ) | |
| self.present(viewController, animated: true) | |
| }) | |
| }), WableBottomSheetAction(title: "밴하기", handler: { | |
| self.didBannedTappedSubject.send((itemID.content.contentInfo.author.id, itemID.content.id)) | |
| }) | |
| ) | |
| } else { | |
| viewController.addActions(WableBottomSheetAction(title: "신고하기", handler: { | |
| viewController.dismiss(animated: true, completion: { | |
| let viewController = WableSheetViewController(title: "신고하시겠어요?") | |
| viewController.addActions( | |
| WableSheetAction( | |
| title: "취소", | |
| style: .gray, | |
| handler: { | |
| viewController.dismiss(animated: true) | |
| } | |
| ), | |
| WableSheetAction( | |
| title: "신고하기", | |
| style: .primary, | |
| handler: { | |
| viewController.dismiss(animated: true, completion: { | |
| self.didReportTappedSubject.send((itemID.content.contentInfo.title, itemID.content.contentInfo.text)) | |
| }) | |
| } | |
| ) | |
| ) | |
| self.present(viewController, animated: true) | |
| }) | |
| })) | |
| } | |
| self.present(viewController, animated: true) | |
| }, | |
| profileImageViewTapHandler: { | |
| // TODO: 프로필 구현되는 대로 추가적인 설정 필요 | |
| let viewController = ProfileViewController() | |
| self.navigationController?.pushViewController(viewController, animated: true) | |
| }, | |
| ghostButtonTapHandler: { | |
| let viewController = WableSheetViewController(title: "와블의 온화한 문화를 해치는\n누군가를 발견하신 건가요?") | |
| viewController.addActions( | |
| WableSheetAction( | |
| title: "고민할게요", | |
| style: .gray, | |
| handler: { | |
| viewController.dismiss(animated: true) | |
| } | |
| ), | |
| WableSheetAction( | |
| title: "네 맞아요", | |
| style: .primary, | |
| handler: { | |
| viewController.dismiss(animated: true, completion: { | |
| self.didGhostTappedSubject.send((itemID.content.id, itemID.content.contentInfo.author.id)) | |
| }) | |
| } | |
| ) | |
| ) | |
| self.present(viewController, animated: true) | |
| } | |
| ) | |
| // ──────────────────────────────────────────────────────────────────────────────── | |
| // Add these helper methods inside HomeViewController | |
| // ──────────────────────────────────────────────────────────────────────────────── | |
| private func presentDeleteConfirmationSheet(for contentID: Int) { | |
| let viewController = WableSheetViewController( | |
| title: "게시글을 삭제하시겠어요?", | |
| message: "게시글이 영구히 삭제됩니다." | |
| ) | |
| viewController.addActions( | |
| WableSheetAction( | |
| title: "취소", | |
| style: .gray, | |
| handler: { viewController.dismiss(animated: true) } | |
| ), | |
| WableSheetAction( | |
| title: "삭제하기", | |
| style: .primary, | |
| handler: { | |
| viewController.dismiss(animated: true) { | |
| self.didDeleteTappedSubject.send(contentID) | |
| } | |
| } | |
| ) | |
| ) | |
| self.present(viewController, animated: true) | |
| } | |
| private func presentReportConfirmationSheet(title: String, text: String) { | |
| let viewController = WableSheetViewController(title: "신고하시겠어요?") | |
| viewController.addActions( | |
| WableSheetAction( | |
| title: "취소", | |
| style: .gray, | |
| handler: { viewController.dismiss(animated: true) } | |
| ), | |
| WableSheetAction( | |
| title: "신고하기", | |
| style: .primary, | |
| handler: { | |
| viewController.dismiss(animated: true) { | |
| self.didReportTappedSubject.send((title, text)) | |
| } | |
| } | |
| ) | |
| ) | |
| self.present(viewController, animated: true) | |
| } | |
| private func presentGhostConfirmationSheet(contentID: Int, authorID: Int) { | |
| let viewController = WableSheetViewController( | |
| title: "와블의 온화한 문화를 해치는\n누군가를 발견하신 건가요?" | |
| ) | |
| viewController.addActions( | |
| WableSheetAction( | |
| title: "고민할게요", | |
| style: .gray, | |
| handler: { viewController.dismiss(animated: true) } | |
| ), | |
| WableSheetAction( | |
| title: "네 맞아요", | |
| style: .primary, | |
| handler: { | |
| viewController.dismiss(animated: true) { | |
| self.didGhostTappedSubject.send((contentID, authorID)) | |
| } | |
| } | |
| ) | |
| ) | |
| self.present(viewController, animated: true) | |
| } | |
| // ──────────────────────────────────────────────────────────────────────────────── | |
| // Then, inside your cell configuration closure: | |
| // ──────────────────────────────────────────────────────────────────────────────── | |
| settingButtonTapHandler: { | |
| let viewController = WableBottomSheetController() | |
| if self.activeUserID == itemID.content.contentInfo.author.id { | |
| viewController.addActions( | |
| WableBottomSheetAction(title: "삭제하기", handler: { | |
| viewController.dismiss(animated: true) { | |
| self.presentDeleteConfirmationSheet(for: itemID.content.id) | |
| } | |
| }) | |
| ) | |
| } else if self.isActiveUserAdmin ?? false { | |
| viewController.addActions( | |
| WableBottomSheetAction(title: "신고하기", handler: { | |
| viewController.dismiss(animated: true) { | |
| self.presentReportConfirmationSheet( | |
| title: itemID.content.contentInfo.title, | |
| text: itemID.content.contentInfo.text | |
| ) | |
| } | |
| }), | |
| WableBottomSheetAction(title: "밴하기", handler: { | |
| self.didBannedTappedSubject.send( | |
| (itemID.content.contentInfo.author.id, itemID.content.id) | |
| ) | |
| }) | |
| ) | |
| } else { | |
| viewController.addActions( | |
| WableBottomSheetAction(title: "신고하기", handler: { | |
| viewController.dismiss(animated: true) { | |
| self.presentReportConfirmationSheet( | |
| title: itemID.content.contentInfo.title, | |
| text: itemID.content.contentInfo.text | |
| ) | |
| } | |
| }) | |
| ) | |
| } | |
| self.present(viewController, animated: true) | |
| }, | |
| ghostButtonTapHandler: { | |
| self.presentGhostConfirmationSheet( | |
| contentID: itemID.content.id, | |
| authorID: itemID.content.contentInfo.author.id | |
| ) | |
| } |
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 267-267: TODOs should be resolved (프로필 구현되는 대로 추가적인 설정 필요)
(todo)
| .sink(receiveValue: { userID in | ||
| var updatedContents = contentsSubject.value | ||
|
|
||
| guard let index = updatedContents.firstIndex(where: { $0.content.contentInfo.author.id == userID }) else { return } | ||
|
|
||
| let content = updatedContents[index] | ||
| let contentInfo = content.content.contentInfo | ||
| let userContent = content.content | ||
| let opacity = contentInfo.opacity.reduced() | ||
|
|
||
| let updatedContent = Content( | ||
| content: UserContent( | ||
| id: userContent.id, | ||
| contentInfo: ContentInfo( | ||
| author: contentInfo.author, | ||
| createdDate: contentInfo.createdDate, | ||
| title: contentInfo.title, | ||
| imageURL: contentInfo.imageURL, | ||
| text: contentInfo.text, | ||
| status: .ghost, | ||
| like: contentInfo.like, | ||
| opacity: opacity, | ||
| commentCount: contentInfo.commentCount | ||
| ) | ||
| ), | ||
| isDeleted: content.isDeleted | ||
| ) | ||
|
|
||
| updatedContents[index] = updatedContent | ||
| contentsSubject.send(updatedContents) | ||
| }) |
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.
didGhostTappedItem updates only the first matching post – update all authored posts
Currently the logic stops after modifying the first content written by the target user:
guard let index = updatedContents.firstIndex(where: { $0.content.contentInfo.author.id == userID }) else { return }
...
updatedContents[index] = updatedContentIf the user has authored multiple posts in the timeline the remaining ones keep their original status, so they are still visible even though the backend has marked them as ghosted.
Replace the single‐index update with a map (or indices.filter) so every post from that author is updated.
-guard let index = updatedContents.firstIndex(where: { $0.content.contentInfo.author.id == userID }) else { return }
-
-let content = updatedContents[index]
-...
-updatedContents[index] = updatedContent
+updatedContents = updatedContents.map { content in
+ guard content.content.contentInfo.author.id == userID else { return content }
+
+ let info = content.content.contentInfo
+ let opacity = info.opacity.reduced()
+
+ return Content(
+ content: UserContent(
+ id: content.content.id,
+ contentInfo: ContentInfo(
+ author: info.author,
+ createdDate: info.createdDate,
+ title: info.title,
+ imageURL: info.imageURL,
+ text: info.text,
+ status: .ghost,
+ like: info.like,
+ opacity: opacity,
+ commentCount: info.commentCount
+ )
+ ),
+ isDeleted: content.isDeleted
+ )
+}This guarantees UI consistency with backend state for all affected cells.
| .sink(receiveValue: { userID in | ||
| var updatedContents = contentsSubject.value | ||
|
|
||
| guard let index = updatedContents.firstIndex(where: { $0.content.contentInfo.author.id == userID }) else { return } | ||
|
|
||
| let content = updatedContents[index] | ||
| let contentInfo = content.content.contentInfo | ||
| let userContent = content.content | ||
| let opacity = contentInfo.opacity.reduced() | ||
|
|
||
| let updatedContent = Content( | ||
| content: UserContent( | ||
| id: userContent.id, | ||
| contentInfo: ContentInfo( | ||
| author: contentInfo.author, | ||
| createdDate: contentInfo.createdDate, | ||
| title: contentInfo.title, | ||
| imageURL: contentInfo.imageURL, | ||
| text: contentInfo.text, | ||
| status: .blind, | ||
| like: contentInfo.like, | ||
| opacity: opacity, | ||
| commentCount: contentInfo.commentCount | ||
| ) | ||
| ), | ||
| isDeleted: content.isDeleted | ||
| ) | ||
|
|
||
| updatedContents[index] = updatedContent | ||
| contentsSubject.send(updatedContents) | ||
| }) |
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
Same multi-post issue & code duplication in didBannedTappedItem
didBannedTappedItem suffers from the identical “first match only” bug and duplicates 40+ lines that were just written for ghosting.
Refactor the updating logic into a small helper that takes (authorID, newStatus) and returns an updated [Content]. That eliminates duplication and prevents the two branches from diverging in future maintenance.
Example sketch:
private func apply(status: ContentStatus,
toAuthor authorID: Int,
on contents: [Content]) -> [Content] {
return contents.map { content in
guard content.content.contentInfo.author.id == authorID else { return content }
let info = content.content.contentInfo
return content.replacing(info: info.copy(status: status,
opacity: info.opacity.reduced()))
}
}Then both sinks become a terse:
updatedContents = apply(status: .ghost, toAuthor: userID, on: updatedContents)| let activeUserIDSubject = CurrentValueSubject<Int?, Never>(nil) | ||
| let isAdminSubject = CurrentValueSubject<Bool?, Never>(false) | ||
| let isReportSucceedSubject = CurrentValueSubject<Bool, Never>(false) | ||
|
|
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.
Use PassthroughSubject for one-shot “report succeeded” event
isReportSucceedSubject is declared as a CurrentValueSubject, meaning it always keeps the latest boolean in memory and immediately re-emits it to every new subscriber.
For a one-off toast / banner trigger this is usually undesirable because:
- A later subscriber (e.g. when the view re-appears) will receive a stale
trueand think a new report just succeeded. - You have to remember to manually reset the subject back to
false, otherwise the side effect can fire repeatedly.
A PassthroughSubject conveys the event only when it actually happens and keeps no state, which is exactly the semantic we need.
-let isReportSucceedSubject = CurrentValueSubject<Bool, Never>(false)
+let isReportSucceedSubject = PassthroughSubject<Bool, Never>()No other change is needed because you already call isReportSucceedSubject.send(true); the stream type remains AnyPublisher<Bool, Never>.
Also applies to: 251-261
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.
고생하셨습니다.
워낙 기능이 많고, 방대한 양이라 구현하시는데에 절 한번 올리겠습니다. 🙇🙇🏻
끝으로 뷰모델에서 튜플을 바로 사용하던데 content.0와 같은 경우, 정확하게 어떠한 것을 리턴하는지는 코드를 보고는 바로 알 수 없기 때문에, 이러한 경우는 지양해 주시면 감사하겠습니다.
| settingButtonTapHandler: { | ||
|
|
||
| }, | ||
| profileImageViewTapHandler: { | ||
|
|
||
| }, | ||
| ghostButtonTapHandler: { | ||
|
|
||
| } |
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.
아무것도 넘겨주지 않아도 괜찮은 걸까요?
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.
이 부분은 세부 화면 쪽에서 구현된 부분이라 해당 PR에서는 반영하지 않았습니다 ~!!
| cell.configureCell( | ||
| info: itemID.content.contentInfo, | ||
| postType: itemID.content.contentInfo.author.id == self.activeUserID ? .mine : .others, | ||
| cellType: .list, | ||
| likeButtonTapHandler: { | ||
| self.didHeartTappedSubject.send((itemID.content.id, cell.likeButton.isLiked)) | ||
| }, | ||
| settingButtonTapHandler: { | ||
| let viewController = WableBottomSheetController() | ||
|
|
||
| if self.activeUserID == itemID.content.contentInfo.author.id { | ||
| viewController.addActions(WableBottomSheetAction(title: "삭제하기", handler: { | ||
| viewController.dismiss(animated: true, completion: { | ||
| let viewController = WableSheetViewController(title: "게시글을 삭제하시겠어요?", message: "게시글이 영구히 삭제됩니다.") | ||
| viewController.addActions( | ||
| WableSheetAction( | ||
| title: "취소", | ||
| style: .gray, | ||
| handler: { | ||
| viewController.dismiss(animated: true) | ||
| } | ||
| ), | ||
| WableSheetAction( | ||
| title: "삭제하기", | ||
| style: .primary, | ||
| handler: { | ||
| viewController.dismiss(animated: true, completion: { | ||
| self.didDeleteTappedSubject.send(itemID.content.id) | ||
| }) | ||
| } | ||
| ) | ||
| ) | ||
|
|
||
| self.present(viewController, animated: true) | ||
| }) | ||
| })) | ||
| } else if self.isActiveUserAdmin ?? false { | ||
| viewController.addActions(WableBottomSheetAction(title: "신고하기", handler: { | ||
| viewController.dismiss(animated: true, completion: { | ||
| let viewController = WableSheetViewController(title: "신고하시겠어요?") | ||
| viewController.addActions( | ||
| WableSheetAction( | ||
| title: "취소", | ||
| style: .gray, | ||
| handler: { | ||
| viewController.dismiss(animated: true) | ||
| } | ||
| ), | ||
| WableSheetAction( | ||
| title: "신고하기", | ||
| style: .primary, | ||
| handler: { | ||
| viewController.dismiss(animated: true, completion: { | ||
| self.didReportTappedSubject.send((itemID.content.contentInfo.title, itemID.content.contentInfo.text)) | ||
| }) | ||
| } | ||
| ) | ||
| ) | ||
|
|
||
| self.present(viewController, animated: true) | ||
| }) | ||
| }), WableBottomSheetAction(title: "밴하기", handler: { | ||
| self.didBannedTappedSubject.send((itemID.content.contentInfo.author.id, itemID.content.id)) | ||
| }) | ||
| ) | ||
| } else { | ||
| viewController.addActions(WableBottomSheetAction(title: "신고하기", handler: { | ||
| viewController.dismiss(animated: true, completion: { | ||
| let viewController = WableSheetViewController(title: "신고하시겠어요?") | ||
| viewController.addActions( | ||
| WableSheetAction( | ||
| title: "취소", | ||
| style: .gray, | ||
| handler: { | ||
| viewController.dismiss(animated: true) | ||
| } | ||
| ), | ||
| WableSheetAction( | ||
| title: "신고하기", | ||
| style: .primary, | ||
| handler: { | ||
| viewController.dismiss(animated: true, completion: { | ||
| self.didReportTappedSubject.send((itemID.content.contentInfo.title, itemID.content.contentInfo.text)) | ||
| }) | ||
| } | ||
| ) | ||
| ) | ||
|
|
||
| self.present(viewController, animated: true) | ||
| }) | ||
| })) | ||
| } | ||
|
|
||
| self.present(viewController, animated: 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.
확실히 이러한 부분은 중복되는 코드를 줄이는 방향으로 개선할 수 있을 것 같아요
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.
ㅎㅎ 세부 화면과도 겹치는 로직이 너무 많아서... 이후에 함께 관리할 수 있는 방법을 고민해봐야 할 것 같습니다. 좋은 방법이 있으시다면 공유 부탁드립니닷 🥹
| handler: { | ||
| viewController.dismiss(animated: 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.
dismiss는 하지 않아도 내부적으로 하게끔 작성해 두었습니다.
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.
헉 🫢 제가 확인을 제대로 못했네요 반영해두었습니다 ~!
| output.isAdmin | ||
| .receive(on: DispatchQueue.main) | ||
| .sink { [weak self] isAdmin in | ||
| self?.isActiveUserAdmin = isAdmin | ||
| } | ||
| .store(in: cancelBag) | ||
|
|
||
| output.activeUserID | ||
| .receive(on: DispatchQueue.main) | ||
| .sink { [weak self] id in | ||
| self?.activeUserID = id | ||
| } | ||
| .store(in: cancelBag) |
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.
반응형으로 동작해야 하지 않는 작업의 경우는, 단순하게 뷰모델의 프로퍼티로 관리해도 좋을 것 같아요.
불필요한 Input- Output을 타야 하는 건 번거로울 것 같습니다.
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.
저도 이 부분 고민을 좀 했었는데요...! isAdmin과 activeUserID가 초기 로딩만 하고 거의 변하지 않는 값이라는 점에서 저도 굳이 이렇게 관리해야 하나? 라는 의문이 있었습니다. 그런데 모든 프로퍼티를 Input-Output으로 관리해야 하는줄 알고 있어서 ㅋㅋ ㅜㅜ 이렇게 구현했는데 프로퍼티로 빼도 되는 거였군요 ,,,
다만 리팩토링을 시도해봤는데 뷰모델에서 유저의 어드민 여부와 아이디를 받아온 후 해당 값을 뷰컨에서 받아 이후에 뷰들을 그려주는 과정이 필요한데 프로퍼티로 구현하게 되면 유저의 어드민 여부와 아이디를 받아오기 전에 뷰컨이 업데이트되면서 뷰가 제대로 그려지지 않는 오류가 있었습니다.
그래서 output에 isReportSucceed를 추가하고 값을 받아온 이후에 이벤트를 발행해서 뷰를 다시 그릴까 하는 생각도 했는데 그러면 지금 구조를 유지해도 괜찮지 않나? 하는 생각이 들더라구요. 뭔가 놓치고 있는 것 같기도 합니다...
이 부분은 일단 머지해둘테니 몸 나아지시면 다음에 한번 같이 봐주시면 좋을 것 ,,, 같습니닷 ,,, ㅜㅜ
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: 0
♻️ Duplicate comments (3)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (3)
254-276: 🛠️ Refactor suggestionExtract ghost confirmation sheet logic to a helper method.
The ghost confirmation sheet logic should also be extracted to improve readability and maintainability.
+ private func presentGhostConfirmationSheet(contentID: Int, authorID: Int) { + let viewController = WableSheetViewController(title: "와블의 온화한 문화를 해치는\n누군가를 발견하신 건가요?") + viewController.addActions( + WableSheetAction( + title: "고민할게요", + style: .gray + ), + WableSheetAction( + title: "네 맞아요", + style: .primary, + handler: { + viewController.dismiss(animated: true) { + self.didGhostTappedSubject.send((contentID, authorID)) + } + } + ) + ) + self.present(viewController, animated: true) + }Then you can simplify the call:
- let viewController = WableSheetViewController(title: "와블의 온화한 문화를 해치는\n누군가를 발견하신 건가요?") - viewController.addActions( - WableSheetAction( - title: "고민할게요", - style: .gray, - handler: { - viewController.dismiss(animated: true) - } - ), - WableSheetAction( - title: "네 맞아요", - style: .primary, - handler: { - viewController.dismiss(animated: true, completion: { - self.didGhostTappedSubject.send((itemID.content.id, itemID.content.contentInfo.author.id)) - }) - } - ) - ) - - self.present(viewController, animated: true) + self.presentGhostConfirmationSheet(contentID: itemID.content.id, authorID: itemID.content.contentInfo.author.id)
202-243: 🛠️ Refactor suggestionExtract report confirmation sheet logic to a helper method.
Similar to the deletion confirmation, the reporting flow is duplicated twice in this method. Extract it to reduce duplication.
+ private func presentReportConfirmationSheet(title: String, text: String) { + let viewController = WableSheetViewController(title: "신고하시겠어요?") + viewController.addActions( + WableSheetAction(title: "취소", style: .gray), + WableSheetAction( + title: "신고하기", + style: .primary, + handler: { + viewController.dismiss(animated: true) { + self.didReportTappedSubject.send((title, text)) + } + } + ) + ) + self.present(viewController, animated: true) + }Then you can simplify both report action implementations.
182-200: 🛠️ Refactor suggestionExtract deletion confirmation sheet logic to a helper method.
The deletion confirmation sheet presentation contains complex nested logic that makes the code difficult to read and maintain.
Consider extracting this into a dedicated helper method:
+ private func presentDeleteConfirmationSheet(for contentID: Int) { + let viewController = WableSheetViewController(title: "게시글을 삭제하시겠어요?", message: "게시글이 영구히 삭제됩니다.") + viewController.addActions( + WableSheetAction(title: "취소", style: .gray), + WableSheetAction( + title: "삭제하기", + style: .primary, + handler: { + viewController.dismiss(animated: true) { + self.didDeleteTappedSubject.send(contentID) + } + } + ) + ) + self.present(viewController, animated: true) + }Then you can simplify the call:
- viewController.dismiss(animated: true, completion: { - let viewController = WableSheetViewController(title: "게시글을 삭제하시겠어요?", message: "게시글이 영구히 삭제됩니다.") - viewController.addActions( - WableSheetAction(title: "취소", style: .gray), - WableSheetAction( - title: "삭제하기", - style: .primary, - handler: { - viewController.dismiss(animated: true, completion: { - self.didDeleteTappedSubject.send(itemID.content.id) - }) - } - ) - ) - - self.present(viewController, animated: true) - }) + viewController.dismiss(animated: true) { + self.presentDeleteConfirmationSheet(for: itemID.content.id) + }
🧹 Nitpick comments (4)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (4)
249-252: Remove TODO comment as the implementation is already provided.The TODO comment mentions waiting for profile implementation, but the implementation is already there in the following lines.
- // TODO: 프로필 구현되는 대로 추가적인 설정 필요 let viewController = ProfileViewController() self.navigationController?.pushViewController(viewController, animated: true)🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 249-249: TODOs should be resolved (프로필 구현되는 대로 추가적인 설정 필요)
(todo)
168-168: Fix unused parameter warning in closure.The
cellparameter is not used in the closure, which triggers a SwiftLint warning.- let homeCellRegistration = CellRegistration<ContentCollectionViewCell, Content> { [weak self] cell, indexPath, itemID in + let homeCellRegistration = CellRegistration<ContentCollectionViewCell, Content> { [weak self] cell, _, itemID in🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 168-168: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
260-262: Remove redundant dismiss call in WableSheetAction.From previous review comments, it was noted that the dismiss functionality is already handled internally.
title: "고민할게요", style: .gray, - handler: { - viewController.dismiss(animated: true) - } + handler: nil ),
385-396: Fix typo in toast message.There's a small typo in the success toast message.
- message: "신고 접수가 완료되었어요.\n24시간 이내에 조치할 예정이예요." + message: "신고 접수가 완료되었어요.\n24시간 이내에 조치할 예정이에요."
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
Wable-iOS/Presentation/Home/View/HomeViewController.swift(5 hunks)
🧰 Additional context used
🪛 SwiftLint (0.57.0)
Wable-iOS/Presentation/Home/View/HomeViewController.swift
[Warning] 249-249: TODOs should be resolved (프로필 구현되는 대로 추가적인 설정 필요)
(todo)
[Warning] 168-168: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
🔇 Additional comments (1)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (1)
324-336: Consider alternative approaches for managing relatively static values.As mentioned in previous review comments, consider whether reactive binding is necessary for values like
isAdminandactiveUserIDthat rarely change. However, also consider the timing issues that were highlighted in past review discussions.If these values are only needed at initial load and don't change during the lifecycle of the view controller, a simpler approach using properties might be possible.
👻 PULL REQUEST
📄 작업 내용
💻 주요 코드 설명
Opacity구조체reduced()메서드 추가 구현output으로 전송하는 과정이 필요합니다.Opacity구조체 내에Opacity구조체를 반환하는reduced()메서드를 구현했습니다.해당 코드가 있는 파일명
📚 참고자료
👀 기타 더 이야기해볼 점
🔗 연결된 이슈
Summary by CodeRabbit
Summary by CodeRabbit
New Features
Enhancements
Bug Fixes
Style
Chores