-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] 온보딩 기능 구현 및 메인 홈, 글 작성 구현 #164
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
Conversation
WalkthroughThis pull request introduces extensive modifications to the project. New Swift files are added for various use cases and view models while some outdated ones are removed. Method signatures, especially in repository and use case classes, have been updated. The Xcode project file is modified to include new entries and reorganized groups. Additionally, multiple network target types now support multipart form data, and several UI components and view controllers have been enhanced using Combine and diffable data sources to improve user interactions and navigation flows. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant LoginViewModel
participant FetchUserAuthUseCase
participant FetchUserInformationUseCase
User->>LoginViewModel: Initiate login
LoginViewModel->>FetchUserAuthUseCase: Execute authentication
FetchUserAuthUseCase-->>LoginViewModel: Return Account details
LoginViewModel->>FetchUserInformationUseCase: Update user session
FetchUserInformationUseCase-->>LoginViewModel: Confirm session update
LoginViewModel-->>User: Login completed successfully
Assessment against linked issues
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Tip ⚡💬 Agentic Chat (Pro Plan, General Availability)
✨ 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: 10
🔭 Outside diff range comments (1)
Wable-iOS/Infra/Network/TargetType/CommentLikedTargetType.swift (1)
25-26:⚠️ Potential issueFix parameter name mismatch in switch case.
There's an inconsistency between the enum case declaration and usage in the switch statement. The enum is declared with
commentID, but the switch case usescontentID.Apply this fix:
- case .createCommentLiked(contentID: let contentID): - return "v1/comment/\(contentID)/liked" + case .createCommentLiked(commentID: let commentID, request: _): + return "v1/comment/\(commentID)/liked"
♻️ Duplicate comments (1)
Wable-iOS/Presentation/Onboarding/ViewController/ProfileRegisterViewController.swift (1)
95-95: Duplicate out-of-bounds concern.As noted previously, check for a non-empty list before indexing
[0].
🧹 Nitpick comments (45)
Wable-iOS/Domain/UseCase/Home/FetchUserContentListUseCase.swift (2)
12-18: LGTM! Consider adding documentation for better maintainability.The class definition follows good Swift practices with:
- Appropriate
finalkeyword for a class not meant to be subclassed- Private repository property following dependency injection pattern
- Clean initializer
For better maintainability, consider adding documentation comments explaining the purpose of this use case class.
+/// A use case responsible for fetching a list of user content. final class FetchUserContentListUseCase { private let repository: ContentRepository + /// Initializes the use case with the required repository. + /// - Parameter repository: The repository used to fetch user content data. init(repository: ContentRepository) { self.repository = repository } }
22-26: The execute method implementation looks good.The method correctly delegates to the repository and maintains the same return type. This follows the clean architecture pattern well.
Consider adding documentation to explain the parameters, particularly what the
cursorparameter represents (likely for pagination).extension FetchUserContentListUseCase { + /// Fetches a list of user content for a specific member. + /// - Parameters: + /// - memberID: The ID of the member whose content to fetch + /// - cursor: The pagination cursor position + /// - Returns: A publisher that emits an array of user content or an error func execute(memberID: Int, cursor: Int) -> AnyPublisher<[UserContent], WableError> { return repository.fetchUserContentList(memberID: memberID, cursor: cursor) } }Wable-iOS/Domain/UseCase/Home/DeleteContentUseCase.swift (2)
5-5: Check the creation date.The file creation date is set to 3/31/25, which appears to be in the future. This might be a typo or incorrect date format.
22-26: Consider adding documentation.While the implementation correctly delegates to the repository's
deleteContentmethod, adding documentation comments (using///or/** */) would help explain the purpose, parameters, and return value of this method.extension DeleteContentUseCase { + /// Deletes content with the specified ID + /// - Parameter contentID: The ID of the content to delete + /// - Returns: A publisher that emits when deletion is complete or an error occurs func execute(contentID: Int) -> AnyPublisher<Void, WableError> { return repository.deleteContent(contentID: contentID) } }Wable-iOS/Domain/UseCase/Profile/FetchContentInfoUseCase.swift (4)
1-7: File location doesn't match the content.The file is located in the
Profiledirectory, but it appears to be handling general content fetching rather than profile-specific functionality. Consider moving this file to a more appropriate directory (likeContentor a general UseCase directory) to maintain proper organization.
21-25: Add documentation for better code clarity.The
executemethod lacks documentation. Consider adding Swift documentation comments (///) to describe the method's purpose, parameters, and return value for better developer experience.extension FetchContentInfoUseCase { + /// Fetches detailed information for a specific content item + /// - Parameters: + /// - contentID: The unique identifier of the content + /// - title: The title of the content + /// - Returns: A publisher that emits ContentInfo on success or WableError on failure func execute(contentID: Int, title: String) -> AnyPublisher<ContentInfo, WableError> { return repository.fetchContentInfo(contentID: contentID, title: title) } }
21-25: Consider adding parameter validation.The method currently doesn't validate inputs. Consider adding basic validation for the contentID (e.g., checking for negative values) and title (e.g., checking for empty strings) to prevent potential issues.
extension FetchContentInfoUseCase { func execute(contentID: Int, title: String) -> AnyPublisher<ContentInfo, WableError> { + guard contentID > 0 else { + return Fail(error: WableError.invalidParameter("Content ID must be positive")).eraseToAnyPublisher() + } + + guard !title.isEmpty else { + return Fail(error: WableError.invalidParameter("Title cannot be empty")).eraseToAnyPublisher() + } + return repository.fetchContentInfo(contentID: contentID, title: title) } }
9-10: Consider importing Foundation.While not strictly necessary, it's common practice to import Foundation when working with basic types and frameworks in Swift. This provides access to common types and functions that might be needed.
import Combine +import FoundationWable-iOS/Infra/Network/DTO/Request/Profile/UpdateUserProfile.swift (1)
20-24: Consider clarifying the difference between alarm properties.The struct now contains both
isAlarmAllowedandisPushAlarmAllowedproperties. Consider adding comments to clarify the distinction between these two similar-sounding properties to avoid confusion.struct ProfileInfo: Encodable { let nickname: String? + // Controls in-app notification settings let isAlarmAllowed: Bool? let memberIntro: String? + // Controls device push notification settings let isPushAlarmAllowed: Bool? let fcmToken: String?Wable-iOS/Presentation/Enum/DefaultProfileType.swift (1)
16-25: Consider a more maintainable implementation for the uppercased property.Using a switch statement requires manually updating each case if the enum grows. A simpler approach would be to use the raw value's uppercased representation.
var uppercased: String { - switch self { - case .green: - return "GREEN" - case .blue: - return "BLUE" - case .purple: - return "PURPLE" - } + return self.rawValue.components(separatedBy: "_").last?.uppercased() ?? "" }Wable-iOS/Domain/UseCase/Home/DeleteCommentUseCase.swift (1)
22-26: Execute method implementation is clean and focused.The method properly delegates to the repository, maintaining a clear separation of concerns. Consider adding documentation comments to describe expected behavior, error cases, and parameter requirements.
extension DeleteCommentUseCase { + /// Deletes a comment with the specified ID + /// - Parameter commentID: The ID of the comment to delete + /// - Returns: A publisher that emits when the operation completes or fails with a WableError func execute(commentID: Int) -> AnyPublisher<Void, WableError> { return repository.deleteComment(commentID: commentID) } }Wable-iOS/Presentation/Helper/Extension/UITextView+.swift (1)
22-33: Consider clarifying line spacing calculation and behavior when replacing text.Two considerations for this implementation:
The line spacing calculation divides by the magic number 1.6. Consider adding a comment explaining why this specific divisor was chosen for better maintainability.
This method replaces any existing text and attributes in the text view. Verify this is the intended behavior, as it might be unexpected when styling text that already exists in the view.
Alternative approach for styling existing text:
func setPretendardStyle(with style: UIFont.Pretendard) { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineSpacing = (style.lineHeight - style.size) / 1.6 let attributes: [NSAttributedString.Key: Any] = [ .font: UIFont.pretendard(style), .kern: style.kerning, .paragraphStyle: paragraphStyle ] // Apply style to existing text if present let text = self.text ?? "" self.attributedText = NSAttributedString(string: text, attributes: attributes) }Wable-iOS/Domain/UseCase/Home/CreateContentLikedUseCase.swift (1)
22-26: Consider extracting the hardcoded triggerType stringWhile the implementation is functionally correct, the hardcoded string "contentLiked" would be better defined as a constant to improve maintainability.
+ private enum Constants { + static let contentLikedTriggerType = "contentLiked" + } extension CreateContentLikedUseCase { func execute(contentID: Int) -> AnyPublisher<Void, WableError> { - return repository.createContentLiked(contentID: contentID, triggerType: "contentLiked") + return repository.createContentLiked(contentID: contentID, triggerType: Constants.contentLikedTriggerType) } }Wable-iOS/Presentation/Onboarding/View/ProfileRegisterView.swift (1)
15-21: Consider making the defaultImageList property privateThis property appears to be used only internally within the class, specifically in the
configureDefaultImage()method. Following encapsulation principles, properties that are only used within the class should be marked as private.- var defaultImageList = [ + private var defaultImageList = [ DefaultProfileType.blue, DefaultProfileType.green, DefaultProfileType.purple ]Wable-iOS/Domain/UseCase/Home/FetchContentCommentListUseCase.swift (1)
22-26: Consider adding pagination documentationThe
executemethod accepts acursorparameter, indicating this implements pagination, but this isn't explicitly documented. Consider adding a comment explaining how the cursor parameter works (e.g., what value to pass for the first page, how to use the returned data to get the next page).extension FetchContentCommentListUseCase { + /// Fetches comments for a specific content with pagination support + /// - Parameters: + /// - contentID: ID of the content to fetch comments for + /// - cursor: Pagination cursor (use 0 for first page) + /// - Returns: Publisher emitting an array of ContentComment objects or WableError func execute(contentID: Int, cursor: Int) -> AnyPublisher<[ContentComment], WableError> { return repository.fetchContentCommentList(contentID: contentID, cursor: cursor) } }Wable-iOS/Domain/UseCase/Home/CreateCommentLikedUseCase.swift (1)
23-29: Consider making triggerType configurableThe execute method correctly forwards the parameters to the repository layer. However, the triggerType is hardcoded as "commentLiked", which might limit flexibility if requirements change.
Consider making triggerType a parameter with a default value of "commentLiked":
-func execute(commentID: Int, notificationText: String) -> AnyPublisher<Void, WableError> { +func execute(commentID: Int, notificationText: String, triggerType: String = "commentLiked") -> AnyPublisher<Void, WableError> { return repository.createCommentLiked( commentID: commentID, - triggerType: "commentLiked", + triggerType: triggerType, notificationText: notificationText ) }Wable-iOS/Presentation/Onboarding/ViewController/LCKYearViewController.swift (1)
84-91: Improved input validation for year selectionThe updated implementation adds proper validation to ensure a valid year is selected before navigating to the next screen. This is a good improvement for error handling.
Consider providing user feedback when no year is selected:
@objc func nextButtonDidTap() { guard let pullDownButtonLabel = rootView.pullDownButton.titleLabel?.text, let lckYear = Int(pullDownButtonLabel) else { + // Show an alert or visual indicator that a year must be selected + rootView.pullDownButton.shake() // Assuming you have a shake animation extension return } navigationController?.pushViewController(LCKTeamViewController(lckYear: lckYear), animated: true) }Wable-iOS/Presentation/Onboarding/ViewController/LCKTeamViewController.swift (1)
14-14: TODO comment should include a timeline or issue referenceWhile the TODO comment provides context about future refactoring plans, it would be more helpful to include a timeline or reference to an issue tracking system.
- // TODO: 유즈케이스 리팩 후에 뷰모델 만들어 넘기기 + // TODO: [ISSUE-XXX] 유즈케이스 리팩 후에 뷰모델 만들어 넘기기 - Q2 2025🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 14-14: TODOs should be resolved (유즈케이스 리팩 후에 뷰모델 만들어 넘기기)
(todo)
Wable-iOS/Infra/Network/TargetType/ContentTargetType.swift (1)
22-56: Well-structured multipart form data implementationThe implementation for handling multipart form data is well-organized and properly structures both the text content as JSON and the optional image data. The file name "dontbe.jpeg" appears to be hardcoded, which might not be ideal for all image types.
Consider dynamically generating the file name based on the image content or using a timestamp:
- fileName: "dontbe.jpeg", + fileName: "image_\(Date().timeIntervalSince1970).jpeg",Also, consider checking the image type to set the correct MIME type instead of assuming "image/jpeg" for all images.
Wable-iOS/Data/RepositoryImpl/ProfileRepositoryImpl.swift (1)
50-60: Address the TODO and consider optional fallback for image compression.
- There's a TODO comment at line 53 for FCM token logic. SwiftLint warns against leaving TODOs unresolved. Consider updating the comment with a plan or implementing the logic soon.
- You are converting the image to JPEG with compression quality 0.1. If quality is critical, consider letting the caller provide compression settings or gracefully handle scenarios if the image is nil.
- // TODO: fcm 토큰 넣어주는 로직 필요 + // TODO (HIGH): Implement FCM token logic or clarify steps to handle push notifications🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 53-53: TODOs should be resolved (fcm 토큰 넣어주는 로직 필요)
(todo)
Wable-iOS/Presentation/WableComponent/Button/LikeButton.swift (1)
33-34: Review visibility and encapsulation.
MakingisLikedandlikeCountpublicly accessible can lead to external classes changing them arbitrarily, possibly distorting the internal state. If you require direct external manipulation, consider at least using getters with controlled setters (e.g.,private(set)).- var isLiked: Bool = false - var likeCount: Int = 0 + private(set) var isLiked: Bool = false + private(set) var likeCount: Int = 0Wable-iOS/Presentation/Onboarding/ViewController/AgreementViewController.swift (2)
16-16: Resolve the pending TODO about the use case refactor.
The comment references creating a dedicated view model after the use case refactor. This is important for maintainable code. Consider opening a tracking issue or implementing it soon to avoid technical debt.- // TODO: 유즈케이스 리팩 후에 뷰모델 만들어 넘기기 + // TODO (HIGH): Implement ViewModel refactor and inject use cases properly🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 16-16: TODOs should be resolved (유즈케이스 리팩 후에 뷰모델 만들어 넘기기)
(todo)
176-176: Implement or refine FCM logic.
SwiftLint identifies this TODO. Ensure FCM updates are appended once the notification workflow is solidified.🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 176-176: TODOs should be resolved (FCM 구현 이후 바꿔줘야 함)
(todo)
Wable-iOS/Domain/UseCase/Home/FetchUserInformationUseCase.swift (3)
23-26: Consider asynchronous or error-emitting approach.
UsingJust(repository.fetchActiveUserID())works for quick retrieval from something like UserDefaults. If you foresee scenarios where fetching might fail or become asynchronous, consider returning an error-emitting publisher (e.g.,ResultPublisher) instead.
28-31: Likewise, review asynchronous needs for fetching session info.
This function is simple and consistent with the repository design, but think about potential error cases or asynchronous data sources.
33-36: Update method might need error handling.
Currently,updateUserSessionreturns aVoidpublisher, ignoring potential write failures or concurrency issues. Consider an error-emitting publisher if future repository updates become more complex.Wable-iOS/Presentation/WableComponent/View/PostUserInfoView.swift (2)
199-211: Switch on userProfileURL’s absoluteString.
This approach is acceptable. Consider using an enum or dictionary mapping if the set of image keys grows or changes often.
213-217: Fan team image commented out & hidden.
You’ve hiddenfanTeamImageViewand commented out its assignment. Confirm this is intentional or schedule a follow-up to reintroduce fan-team visuals.Wable-iOS/Presentation/Home/View/WritePostViewController.swift (2)
277-290: PHPickerViewControllerDelegate closure parameter usage.
You're not using theerrorparameter. Consider replacing it with_to silence the lint warning.- results.first?.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in + results.first?.itemProvider.loadObject(ofClass: UIImage.self) { (image, _) in🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 279-279: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
294-294: Address the TODO for refactoring.
A TODO comment indicates a future need to refine the text view logic. Confirm timeline or open a ticket for it.🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 294-294: TODOs should be resolved (리팩 시 개선 필요)
(todo)
Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (1)
212-218: Consider server confirmation before updating UI state.The like button's state is updated immediately without waiting for server confirmation. If the network request fails, the UI state will be inconsistent with the backend.
Consider adding a mechanism to revert the UI state if the network request fails:
@objc func likeButtonDidTap() { let newCount = likeButton.isLiked ? likeButton.likeCount - 1 : likeButton.likeCount + 1 + let previousState = likeButton.isLiked likeButton.configureButton(isLiked: !likeButton.isLiked, likeCount: newCount, postType: .content) - self.likeButtonTapHandler?() + self.likeButtonTapHandler?() }And have the handler report back success/failure so you can revert if needed.
Wable-iOS/Presentation/Login/LoginViewModel.swift (1)
62-64: Replace unused completion parameter with underscore.The
receiveCompletionparameter is unused and could be replaced with an underscore to improve code clarity..sink( - receiveCompletion: { completion in + receiveCompletion: { _ in WableLogger.log("로그인 작업 완료", for: .debug) },[static_analysis_hints]
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 62-62: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (2)
69-96: Add documentation for pagination implementation.The pagination implementation is well-structured but lacks documentation explaining the logic, which would help other developers understand and maintain the code.
Consider adding comments before this section to explain the pagination flow:
+// Pagination handling: Load more content when the last item is displayed +// Checks that we're not already loading, not at the last page, and have some content input.willDisplayLastItem .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) .filter { !isLoadingMoreSubject.value && !isLastViewSubject.value && !contentsSubject.value.isEmpty } // ...rest of the code
98-110: Consider adding user feedback for like actions.The like button action is well-implemented but doesn't provide feedback to the user in case of success or failure.
Consider adding visual feedback for like operations:
input.didHeartTappedItem .withUnretained(self) .flatMap { owner, info -> AnyPublisher<Void, Never> in if info.1 { return owner.createContentLikedUseCase.execute(contentID: info.0) + .handleEvents(receiveCompletion: { completion in + if case .failure(let error) = completion { + // Log error and potentially show feedback to the user + WableLogger.log("Like creation error: \(error)", for: .error) + } + }) .asDriver(onErrorJustReturn: ()) } else { return owner.deleteContentLikedUseCase.execute(contentID: info.0) + .handleEvents(receiveCompletion: { completion in + if case .failure(let error) = completion { + // Log error and potentially show feedback to the user + WableLogger.log("Like deletion error: \(error)", for: .error) + } + }) .asDriver(onErrorJustReturn: ()) } } .sink(receiveValue: { _ in }) .store(in: cancelBag)Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (3)
49-66: Add documentation for the CommentType enum.The
CommentTypeenum is used but not documented, making it unclear what the different types represent (e.g., what is a "ripple" comment vs. a "reply" comment).Consider adding documentation for the CommentType enum, either in this file or where it's defined:
/// Represents the type of comment being entered /// - ripple: A new top-level comment /// - reply: A response to an existing comment enum CommentType { case ripple case reply }
116-128: Add visual feedback for content like actions.The like/unlike action is well-implemented but doesn't provide feedback to the user about success or failure.
Consider adding visual feedback for like operations:
input.didContentHeartTappedItem .withUnretained(self) .flatMap { owner, info -> AnyPublisher<Void, Never> in if info { return owner.createContentLikedUseCase.execute(contentID: owner.contentID) + .handleEvents(receiveCompletion: { completion in + if case .failure(let error) = completion { + // Log error and potentially show feedback to the user + WableLogger.log("Content like error: \(error)", for: .error) + } + }) .asDriver(onErrorJustReturn: ()) } else { return owner.deleteContentLikedUseCase.execute(contentID: owner.contentID) + .handleEvents(receiveCompletion: { completion in + if case .failure(let error) = completion { + // Log error and potentially show feedback to the user + WableLogger.log("Content unlike error: \(error)", for: .error) + } + }) .asDriver(onErrorJustReturn: ()) } } .sink(receiveValue: { _ in }) .store(in: cancelBag)
162-189: Ensure consistent handling of empty comment lists in pagination.The pagination implementation is robust but should consistently handle cases where comment lists might be empty.
One small improvement in the
.sinkhandler:.sink { comment in var currentItems = commentsSubject.value + guard !comment.isEmpty else { return } currentItems.append(contentsOf: comment) commentsSubject.send(currentItems) }This adds an extra safety check, even though there's already a
.filter { !$0.isEmpty }above.Wable-iOS/Presentation/Onboarding/ViewController/ProfileRegisterViewController.swift (2)
14-21: Resolve the TODO and remove redundant optional initialization.Line 15 includes a TODO that should be addressed or removed if completed. Additionally, initializing
defaultImagetonilis redundant since optionals default tonil.- // TODO: 유즈케이스 리팩 후에 뷰모델 만들어 넘기기 - private var defaultImage: String? = nil + private var defaultImage: String?🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 19-19: Initializing an optional variable with nil is redundant
(redundant_optional_initialization)
[Warning] 15-15: TODOs should be resolved (유즈케이스 리팩 후에 뷰모델 만들어 넘기기)
(todo)
116-129: Consider renamingconditiionLabel.The logic looks fine; however, the label property name has a potential typo (
conditiionLabel). Renaming it toconditionLabelmay improve clarity.Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (4)
46-47: Address the TODO item.Todo indicates logic should move into the ViewModel. Manage or resolve this to keep technical debt low.
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 46-46: TODOs should be resolved (ViewModel로 옮겨야 할 로직)
(todo)
150-195: Consider reducing unused closure parameters.SwiftLint warns that the parameter
indexPathincontentCellRegistrationis unused. You can replace it with_to align with Swift best practices.- let contentCellRegistration = UICollectionView.CellRegistration<ContentCollectionViewCell, Content> { [weak self] cell, indexPath, item in + let contentCellRegistration = UICollectionView.CellRegistration<ContentCollectionViewCell, Content> { [weak self] cell, _, item in🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 151-151: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
196-205: Complete or remove the TODO block.Line 198 indicates a TODO about passing appropriate data. Finalize this logic to maintain clarity.
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 198-198: TODOs should be resolved (데이터 뭐 넘겨야하는지 확인하고 넘기기)
(todo)
255-267: Replace unused closure parameter for layout environment.SwiftLint points out that
layoutEnvironmentisn’t referenced, so consider using_to make the code more concise.- return UICollectionViewCompositionalLayout { [weak self] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in + return UICollectionViewCompositionalLayout { [weak self] (sectionIndex, _) -> NSCollectionLayoutSection? in🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 256-256: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (2)
156-160: Consider removing unused closure parameter and capturingselfweakly.SwiftLint warns about the unused
indexPathparameter in this closure. Additionally, to prevent potential retain cycles, it's a good practice to captureselfas[weak self]. You could apply the following refactor:- let homeCellRegistration = CellRegistration<ContentCollectionViewCell, Content> { cell, indexPath, itemID in + let homeCellRegistration = CellRegistration<ContentCollectionViewCell, Content> { [weak self] cell, _, itemID in + guard let self = self else { return } cell.configureCell(info: itemID.content.contentInfo, postType: .mine, likeButtonTapHandler: { self.didHeartTappedSubject.send((itemID.content.id, cell.likeButton.isLiked)) }) }🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 156-156: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
245-252: Avoid redundant reload after applying snapshot to preserve diffable data source efficiency.Using
collectionView.reloadData()immediately afterdataSource?.apply(snapshot)may cause unnecessary performance overhead and UI re-instantiations. The diffable data source automatically handles incremental updates. Consider removingcollectionView.reloadData():dataSource?.apply(snapshot) - collectionView.reloadData()
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
Wable-iOS/Resource/Assets.xcassets/Button/btn_remove_photo.imageset/btn_remove_photo.pngis excluded by!**/*.pngWable-iOS/Resource/Assets.xcassets/Button/btn_remove_photo.imageset/btn_remove_photo@2x.pngis excluded by!**/*.pngWable-iOS/Resource/Assets.xcassets/Button/btn_remove_photo.imageset/btn_remove_photo@3x.pngis excluded by!**/*.png
📒 Files selected for processing (62)
Wable-iOS.xcodeproj/project.pbxproj(22 hunks)Wable-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved(1 hunks)Wable-iOS/App/SceneDelegate.swift(1 hunks)Wable-iOS/Data/RepositoryImpl/ProfileRepositoryImpl.swift(2 hunks)Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift(1 hunks)Wable-iOS/Domain/Entity/UserSession.swift(1 hunks)Wable-iOS/Domain/RepositoryInterface/ProfileRepository.swift(1 hunks)Wable-iOS/Domain/RepositoryInterface/UserSessionRepository.swift(1 hunks)Wable-iOS/Domain/UseCase/Home/CreateCommentLikedUseCase.swift(1 hunks)Wable-iOS/Domain/UseCase/Home/CreateCommentUseCase.swift(1 hunks)Wable-iOS/Domain/UseCase/Home/CreateContentLikedUseCase.swift(1 hunks)Wable-iOS/Domain/UseCase/Home/CreateContentUseCase.swift(1 hunks)Wable-iOS/Domain/UseCase/Home/DeleteCommentLikedUseCase.swift(1 hunks)Wable-iOS/Domain/UseCase/Home/DeleteCommentUseCase.swift(1 hunks)Wable-iOS/Domain/UseCase/Home/DeleteContentLikedUseCase.swift(1 hunks)Wable-iOS/Domain/UseCase/Home/DeleteContentUseCase.swift(1 hunks)Wable-iOS/Domain/UseCase/Home/FetchContentCommentListUseCase.swift(1 hunks)Wable-iOS/Domain/UseCase/Home/FetchContentListUseCase.swift(1 hunks)Wable-iOS/Domain/UseCase/Home/FetchUserContentListUseCase.swift(1 hunks)Wable-iOS/Domain/UseCase/Home/FetchUserInformationUseCase.swift(1 hunks)Wable-iOS/Domain/UseCase/Login/FetchUserAuthUseCase.swift(1 hunks)Wable-iOS/Domain/UseCase/Onboarding/CreateUserProfileUseCase.swift(2 hunks)Wable-iOS/Domain/UseCase/Profile/FetchContentInfoUseCase.swift(1 hunks)Wable-iOS/Domain/UseCase/Profile/ProfileUseCase.swift(0 hunks)Wable-iOS/Infra/Network/DTO/Request/Profile/UpdateUserProfile.swift(2 hunks)Wable-iOS/Infra/Network/TargetType/AccountTargetType.swift(1 hunks)Wable-iOS/Infra/Network/TargetType/BaseTargetType.swift(3 hunks)Wable-iOS/Infra/Network/TargetType/CommentLikedTargetType.swift(1 hunks)Wable-iOS/Infra/Network/TargetType/CommentTargetType.swift(1 hunks)Wable-iOS/Infra/Network/TargetType/CommunityTargetType.swift(1 hunks)Wable-iOS/Infra/Network/TargetType/ContentLikedTargetType.swift(2 hunks)Wable-iOS/Infra/Network/TargetType/ContentTargetType.swift(1 hunks)Wable-iOS/Infra/Network/TargetType/GhostTargetType.swift(1 hunks)Wable-iOS/Infra/Network/TargetType/InformationTargetType.swift(1 hunks)Wable-iOS/Infra/Network/TargetType/LoginTargetType.swift(1 hunks)Wable-iOS/Infra/Network/TargetType/NotificationTargetType.swift(1 hunks)Wable-iOS/Infra/Network/TargetType/ProfileTargetType.swift(2 hunks)Wable-iOS/Infra/Network/TargetType/ReportTargetType.swift(1 hunks)Wable-iOS/Infra/Network/TargetType/ViewitTargetType.swift(1 hunks)Wable-iOS/Presentation/Enum/DefaultProfileType.swift(1 hunks)Wable-iOS/Presentation/Helper/Extension/UITextView+.swift(1 hunks)Wable-iOS/Presentation/Home/HomeViewController.swift(0 hunks)Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift(1 hunks)Wable-iOS/Presentation/Home/View/HomeViewController.swift(1 hunks)Wable-iOS/Presentation/Home/View/WritePostViewController.swift(1 hunks)Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift(1 hunks)Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift(1 hunks)Wable-iOS/Presentation/Home/ViewModel/WritePostViewModel.swift(1 hunks)Wable-iOS/Presentation/Login/LoginViewController.swift(1 hunks)Wable-iOS/Presentation/Login/LoginViewModel.swift(2 hunks)Wable-iOS/Presentation/Onboarding/View/ProfileRegisterView.swift(2 hunks)Wable-iOS/Presentation/Onboarding/ViewController/AgreementViewController.swift(3 hunks)Wable-iOS/Presentation/Onboarding/ViewController/LCKTeamViewController.swift(3 hunks)Wable-iOS/Presentation/Onboarding/ViewController/LCKYearViewController.swift(1 hunks)Wable-iOS/Presentation/Onboarding/ViewController/ProfileRegisterViewController.swift(6 hunks)Wable-iOS/Presentation/TabBar/TabBarController.swift(5 hunks)Wable-iOS/Presentation/WableComponent/Button/LikeButton.swift(1 hunks)Wable-iOS/Presentation/WableComponent/Cell/CommentCollectionViewCell.swift(10 hunks)Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift(10 hunks)Wable-iOS/Presentation/WableComponent/Navigation/NavigationView.swift(1 hunks)Wable-iOS/Presentation/WableComponent/View/PostUserInfoView.swift(4 hunks)Wable-iOS/Presentation/WableComponent/ViewController/LoadingViewController.swift(1 hunks)
💤 Files with no reviewable changes (2)
- Wable-iOS/Domain/UseCase/Profile/ProfileUseCase.swift
- Wable-iOS/Presentation/Home/HomeViewController.swift
🧰 Additional context used
🧬 Code Graph Analysis (22)
Wable-iOS/Domain/UseCase/Home/FetchContentListUseCase.swift (5)
Wable-iOS/Domain/UseCase/Home/CreateContentUseCase.swift (1)
execute(23-25)Wable-iOS/Domain/UseCase/Home/FetchUserContentListUseCase.swift (1)
execute(23-25)Wable-iOS/Domain/UseCase/Home/DeleteContentUseCase.swift (1)
execute(23-25)Wable-iOS/Domain/UseCase/Profile/FetchContentInfoUseCase.swift (1)
execute(22-24)Wable-iOS/Data/RepositoryImpl/ContentRepositoryImpl.swift (1)
fetchContentList(55-62)
Wable-iOS/Infra/Network/TargetType/ContentLikedTargetType.swift (1)
Wable-iOS/Data/RepositoryImpl/ContentLikedRepositoryImpl.swift (2)
createContentLiked(20-30)deleteContentLiked(32-39)
Wable-iOS/Domain/UseCase/Home/CreateCommentLikedUseCase.swift (1)
Wable-iOS/Data/RepositoryImpl/CommentLikedRepositoryImpl.swift (1)
createCommentLiked(20-33)
Wable-iOS/Domain/UseCase/Home/DeleteContentLikedUseCase.swift (2)
Wable-iOS/Domain/UseCase/Home/CreateContentLikedUseCase.swift (1)
execute(23-25)Wable-iOS/Data/RepositoryImpl/ContentLikedRepositoryImpl.swift (1)
deleteContentLiked(32-39)
Wable-iOS/Domain/UseCase/Home/CreateCommentUseCase.swift (1)
Wable-iOS/Data/RepositoryImpl/CommentRepositoryImpl.swift (1)
createComment(53-66)
Wable-iOS/Domain/UseCase/Home/CreateContentUseCase.swift (1)
Wable-iOS/Data/RepositoryImpl/ContentRepositoryImpl.swift (1)
createContent(20-35)
Wable-iOS/Domain/UseCase/Home/CreateContentLikedUseCase.swift (2)
Wable-iOS/Domain/UseCase/Home/DeleteContentLikedUseCase.swift (1)
execute(23-25)Wable-iOS/Data/RepositoryImpl/ContentLikedRepositoryImpl.swift (1)
createContentLiked(20-30)
Wable-iOS/Domain/UseCase/Profile/FetchContentInfoUseCase.swift (1)
Wable-iOS/Data/RepositoryImpl/ContentRepositoryImpl.swift (1)
fetchContentInfo(46-53)
Wable-iOS/Domain/RepositoryInterface/ProfileRepository.swift (1)
Wable-iOS/Data/RepositoryImpl/ProfileRepositoryImpl.swift (1)
updateUserProfile(38-66)
Wable-iOS/Presentation/Helper/Extension/UITextView+.swift (1)
Wable-iOS/Presentation/Helper/Extension/UIFont+.swift (1)
pretendard(93-95)
Wable-iOS/Domain/RepositoryInterface/UserSessionRepository.swift (2)
Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift (1)
updateUserSession(50-60)Wable-iOS/Domain/UseCase/Home/FetchUserInformationUseCase.swift (1)
updateUserSession(33-36)
Wable-iOS/Infra/Network/TargetType/ProfileTargetType.swift (1)
Wable-iOS/Data/RepositoryImpl/ProfileRepositoryImpl.swift (1)
updateUserProfile(38-66)
Wable-iOS/Presentation/Onboarding/ViewController/LCKTeamViewController.swift (3)
Wable-iOS/Presentation/Onboarding/ViewController/AgreementViewController.swift (1)
nextButtonDidTap(139-212)Wable-iOS/Presentation/Onboarding/ViewController/ProfileRegisterViewController.swift (1)
nextButtonDidTap(132-145)Wable-iOS/Presentation/Onboarding/ViewController/LCKYearViewController.swift (1)
nextButtonDidTap(83-91)
Wable-iOS/Presentation/Home/View/WritePostViewController.swift (7)
Wable-iOS/Presentation/Helper/Extension/UITextView+.swift (1)
setPretendard(22-33)Wable-iOS/Presentation/Helper/Extension/UIView+.swift (1)
addSubviews(32-36)Wable-iOS/Presentation/Helper/Extension/UIStackView+.swift (1)
addArrangedSubviews(38-42)Wable-iOS/Presentation/Helper/Extension/ConstraintMaker+.swift (2)
adjustedHeightEqualTo(45-48)adjustedWidthEqualTo(27-30)Wable-iOS/Presentation/Home/ViewModel/WritePostViewModel.swift (1)
transform(30-65)Wable-iOS/Presentation/WableComponent/View/ToastView.swift (1)
show(144-160)Wable-iOS/Core/Logger/WableLogger.swift (1)
log(14-25)
Wable-iOS/Presentation/Login/LoginViewModel.swift (4)
Wable-iOS/Domain/UseCase/Login/FetchUserAuthUseCase.swift (1)
execute(25-54)Wable-iOS/Core/Logger/WableLogger.swift (1)
log(14-25)Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift (1)
updateUserSession(50-60)Wable-iOS/Domain/UseCase/Home/FetchUserInformationUseCase.swift (1)
updateUserSession(33-36)
Wable-iOS/Presentation/WableComponent/Cell/CommentCollectionViewCell.swift (3)
Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (2)
likeButtonDidTap(212-218)configureCell(246-297)Wable-iOS/Presentation/WableComponent/Button/LikeButton.swift (1)
configureButton(72-94)Wable-iOS/Presentation/WableComponent/Button/CommentButton.swift (1)
configureButton(65-93)
Wable-iOS/Presentation/TabBar/TabBarController.swift (1)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (2)
setupDelegate(178-180)scrollToTop(254-256)
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (3)
Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift (1)
fetchActiveUserID(46-48)Wable-iOS/Domain/UseCase/Home/FetchUserInformationUseCase.swift (1)
fetchActiveUserID(23-26)Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (1)
transform(68-198)
Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (3)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (1)
viewWillAppear(87-93)Wable-iOS/Presentation/Login/LoginViewModel.swift (1)
transform(41-92)Wable-iOS/Domain/UseCase/Login/FetchUserAuthUseCase.swift (1)
execute(25-54)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (4)
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (6)
viewDidLoad(103-111)setupView(117-120)setupConstraint(122-148)setupDataSource(150-194)setupAction(196-205)setupBinding(207-249)Wable-iOS/Presentation/Helper/Extension/UIView+.swift (1)
addSubviews(32-36)Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (1)
transform(40-123)Wable-iOS/Presentation/Home/ViewModel/WritePostViewModel.swift (1)
transform(30-65)
Wable-iOS/Domain/UseCase/Home/FetchUserInformationUseCase.swift (1)
Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift (3)
fetchActiveUserID(46-48)fetchActiveUserSession(39-44)updateUserSession(50-60)
Wable-iOS/Infra/Network/TargetType/ContentTargetType.swift (1)
Wable-iOS/Data/RepositoryImpl/ContentRepositoryImpl.swift (1)
createContent(20-35)
🪛 SwiftLint (0.57.0)
Wable-iOS/Presentation/Login/LoginViewController.swift
[Warning] 178-178: TODOs should be resolved (온보딩 기능 개발 후 다시 되돌려야 함)
(todo)
[Warning] 179-179: Using ternary to call Void functions should be avoided
(void_function_in_ternary)
Wable-iOS/Presentation/Onboarding/ViewController/AgreementViewController.swift
[Warning] 16-16: TODOs should be resolved (유즈케이스 리팩 후에 뷰모델 만들어 넘기기)
(todo)
[Warning] 176-176: TODOs should be resolved (FCM 구현 이후 바꿔줘야 함)
(todo)
Wable-iOS/Data/RepositoryImpl/ProfileRepositoryImpl.swift
[Warning] 53-53: TODOs should be resolved (fcm 토큰 넣어주는 로직 필요)
(todo)
Wable-iOS/Presentation/Home/ViewModel/WritePostViewModel.swift
[Warning] 52-52: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
[Warning] 55-55: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
Wable-iOS/Presentation/Onboarding/ViewController/LCKTeamViewController.swift
[Warning] 14-14: TODOs should be resolved (유즈케이스 리팩 후에 뷰모델 만들어 넘기기)
(todo)
Wable-iOS/Presentation/Home/View/WritePostViewController.swift
[Warning] 294-294: TODOs should be resolved (리팩 시 개선 필요)
(todo)
[Warning] 279-279: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
Wable-iOS/Presentation/Login/LoginViewModel.swift
[Warning] 74-74: TODOs should be resolved (FCM 구현 이후 바꿔줘야 함)
(todo)
[Warning] 56-56: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
[Warning] 62-62: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
Wable-iOS/Presentation/Onboarding/ViewController/ProfileRegisterViewController.swift
[Warning] 19-19: Initializing an optional variable with nil is redundant
(redundant_optional_initialization)
[Warning] 15-15: TODOs should be resolved (유즈케이스 리팩 후에 뷰모델 만들어 넘기기)
(todo)
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift
[Warning] 46-46: TODOs should be resolved (ViewModel로 옮겨야 할 로직)
(todo)
[Warning] 198-198: TODOs should be resolved (데이터 뭐 넘겨야하는지 확인하고 넘기기)
(todo)
[Warning] 151-151: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
[Warning] 256-256: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
Wable-iOS/Presentation/Home/View/HomeViewController.swift
[Warning] 156-156: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
🔇 Additional comments (141)
Wable-iOS/Presentation/WableComponent/Navigation/NavigationView.swift (1)
233-233: UI styling update looks appropriateThe change from what was likely
.body3to.body1for the page title text styling makes sense as part of the PR's onboarding feature and UI implementation. This adjustment will give the navigation title more prominence, which is appropriate for a main navigation element.Wable-iOS/Domain/UseCase/Home/FetchUserContentListUseCase.swift (1)
1-10: Imports look good.The imports are appropriate for this use case. Combine is used for the AnyPublisher return type, and Foundation is a standard import for most Swift files.
Wable-iOS/Domain/UseCase/Home/DeleteContentUseCase.swift (3)
9-10: Imports look good.The imports include Combine for reactive programming (AnyPublisher) and Foundation for basic functionality, which are appropriate for this use case.
12-18: Well-structured class with proper dependency injection.The class is appropriately marked as
finalto prevent subclassing, and it follows good dependency injection practices by accepting the repository through the constructor.
23-25: Consider error handling enhancement.The current implementation directly forwards the repository's result. Consider if there's any use-case specific error handling that should happen at this layer.
Could you check if there are any specific error conditions that should be handled in this use case rather than relying entirely on the repository's error handling?
Wable-iOS/Domain/UseCase/Profile/FetchContentInfoUseCase.swift (1)
11-17: LGTM! Good use of dependency injection.The class follows clean architecture principles by properly injecting the repository dependency, making the code testable and maintainable.
Wable-iOS/Domain/UseCase/Home/FetchContentListUseCase.swift (2)
11-17: Well-structured use case with clean dependency injectionThe implementation correctly follows the clean architecture pattern by accepting a repository dependency and having a clear separation of concerns.
21-25: Simple and effective implementationThe execute method has a clean implementation that properly delegates to the repository, maintaining single responsibility principle.
Wable-iOS/Presentation/Home/ViewModel/WritePostViewModel.swift (2)
13-19: Well-designed ViewModel with proper dependency injectionThe ViewModel follows good practices by using dependency injection for the use case.
21-29: Clean implementation of ViewModelType protocolThe Input and Output structures clearly define the data flow for this ViewModel.
Wable-iOS.xcodeproj/project.pbxproj (1)
19-172: Project file changes align with PR objectivesThe new files added to the project support the PR objectives of implementing onboarding features, main home screen, and writing functionality. The file organization follows a clean architecture structure with proper separation of use cases, view models, and view controllers.
Wable-iOS/Infra/Network/TargetType/NotificationTargetType.swift (1)
20-23: Consistent implementation of multipartFormData propertyThis addition aligns with similar changes in other TargetType implementations, providing consistent support for multipart form data across the network layer.
Wable-iOS/Infra/Network/DTO/Request/Profile/UpdateUserProfile.swift (2)
20-21: New property added for tracking user alarm preferences.The new
isAlarmAllowedproperty has been added to theProfileInfostruct. This is correctly defined as an optional boolean, consistent with the pattern of other properties in this struct.
29-30: CodingKey correctly added for the new property.The new
isAlarmAllowedcase has been properly added to theCodingKeysenum, ensuring correct JSON serialization.Wable-iOS/Presentation/Enum/DefaultProfileType.swift (1)
15-25: Added computed property for uppercase profile type values.The new
uppercasedproperty provides string representations of profile types in uppercase format, which is useful for API interactions or UI display.Wable-iOS/Presentation/WableComponent/ViewController/LoadingViewController.swift (1)
106-106: Language correction in loading message.Changed "저의" (my) to "저희" (our) in the loading message, which corrects the Korean language grammar and shifts from an individual to a collective perspective.
Wable-iOS/Domain/Entity/UserSession.swift (1)
14-14:Details
❓ Verification inconclusive
Type change from String to optional URL for profileURL.
The
profileURLproperty has been changed fromStringtoURL?. This is a good change for two reasons:
- Using
URLprovides type safety for URL values- Making it optional accommodates users without profile images
Verify that all code accessing this property handles the possibility of it being nil:
🏁 Script executed:
#!/bin/bash # Search for places where profileURL is accessed without nil checking rg "\.profileURL\b(?!\?)" --type swiftLength of output: 278
Attention: Manual Verification Needed for
profileURLNil HandlingThe change from a
Stringto an optionalURLforprofileURLis a solid update that improves type safety and correctly accommodates cases without profile images. However, our attempt to automatically verify that all accesses to this property perform nil-checking ran into regex support issues with ripgrep. The original command failed because look-around syntax wasn’t supported without the--pcre2flag.Next Steps:
- Re-run Verification: Please re-run the search using the
--pcre2flag:rg --pcre2 "\.profileURL\b(?!\?)" --type swift- Manual Review: Alternatively, ensure that every access to
profileURLin the codebase properly handles the possibility of it being nil.Wable-iOS/Infra/Network/TargetType/ViewitTargetType.swift (1)
23-25: Added multipartFormData property for future multipart handling.The new
multipartFormDataproperty extends theViewitTargetTypeto support the handling of multipart form data uploads, which is important for implementing features that require file uploads such as images or documents. Currently returning.noneas this target type doesn't require multipart form data yet.Wable-iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved (1)
2-2: Package dependencies hash updated.The
originHashhas been updated, indicating changes in package dependencies. This is expected when adding new features that might require updated or additional dependencies. The update ensures your project is using compatible dependency versions for the new onboarding, home, and writing features.Wable-iOS/Infra/Network/TargetType/CommentLikedTargetType.swift (2)
19-21: Added multipartFormData property for consistency.The addition of
multipartFormDataproperty maintains consistency with other target types and prepares for potential future multipart form data handling in comment likes, though it's currently returning.none.
37-42: Updated requestBody handling to include request object.The
requestBodyimplementation now correctly returns the request object for thecreateCommentLikedcase, which is necessary for sending the like data to the server.Wable-iOS/Infra/Network/TargetType/ReportTargetType.swift (1)
20-22: Added multipartFormData property for consistent API.The addition of the
multipartFormDataproperty toReportTargetTypemaintains a consistent API across all target types, following the pattern established in other network components. This prepares the codebase for future multipart form data handling if needed for report functionality.Wable-iOS/Infra/Network/TargetType/CommentTargetType.swift (1)
21-23: Implementation of multipartFormData looks goodThe addition of the
multipartFormDataproperty toCommentTargetTypefollows a consistent pattern being implemented across multiple target types. Returning.noneis appropriate since none of the current API endpoints in this type require multipart form data uploads.Wable-iOS/Infra/Network/TargetType/InformationTargetType.swift (1)
22-24: Implementation of multipartFormData looks goodThe addition of the
multipartFormDataproperty toInformationTargetTypeis consistent with the implementation in other target types. Since all the operations defined in this enum are GET requests that don't require file uploads, returning.noneis the correct approach.Wable-iOS/Infra/Network/TargetType/LoginTargetType.swift (1)
20-22: Implementation of multipartFormData looks goodThe addition of the
multipartFormDataproperty toLoginTargetTypemaintains consistency with other target types. Returning.noneis appropriate for authentication-related endpoints that don't require file uploads.Wable-iOS/Infra/Network/TargetType/CommunityTargetType.swift (1)
20-22: Implementation of multipartFormData looks goodThe addition of the
multipartFormDataproperty toCommunityTargetTypefollows the consistent pattern implemented across all target types. Returning.noneis appropriate for the current endpoints which don't require multipart form data.Wable-iOS/Infra/Network/TargetType/AccountTargetType.swift (1)
20-22: Added multipart form data support.The addition of the
multipartFormDataproperty is part of a broader update to support file uploads and multipart form data in the networking layer.Wable-iOS/Infra/Network/TargetType/GhostTargetType.swift (1)
17-19: Added multipart form data support.The addition of the
multipartFormDataproperty properly implements the required protocol method, consistent with other network target types.Wable-iOS/Domain/UseCase/Home/DeleteCommentUseCase.swift (1)
12-18: Implementation follows clean architecture pattern.The
DeleteCommentUseCaseclass properly encapsulates the repository dependency through constructor injection, following good dependency injection principles.Wable-iOS/Domain/UseCase/Home/CreateContentUseCase.swift (2)
12-18: Well-structured use case implementation with proper dependency injection.The
CreateContentUseCaseclass follows good software design practices by:
- Using dependency injection through the constructor
- Maintaining single responsibility by focusing solely on content creation
- Following proper encapsulation with private repository property
This implementation aligns with clean architecture principles, which will help maintain separation of concerns in the codebase.
22-26: Good use of Combine for asynchronous content creation.The
executemethod properly delegates to the repository layer while maintaining the appropriate return typeAnyPublisher<Void, WableError>. This reactive approach facilitates proper handling of asynchronous content creation operations in the presentation layer.Wable-iOS/Presentation/Helper/Extension/UITextView+.swift (1)
12-21: Excellent documentation with clear usage example.The detailed documentation for the
setPretendardmethod clearly explains its purpose and provides a concrete usage example, which enhances code readability and makes it easier for other developers to use this extension correctly.Wable-iOS/Domain/UseCase/Home/DeleteCommentLikedUseCase.swift (2)
12-18: Well-structured use case with proper dependency injection.The
DeleteCommentLikedUseCaseclass demonstrates good architectural practices:
- Clean separation of concerns
- Proper dependency injection
- Clear encapsulation of repository dependency
This implementation maintains consistency with the clean architecture pattern used throughout the codebase.
22-26: Effective use of Combine for asynchronous operations.The
executemethod properly delegates to the repository while maintaining the appropriate return typeAnyPublisher<Void, WableError>. This approach ensures the presentation layer can handle the operation reactively with proper error handling.Wable-iOS/Domain/UseCase/Home/DeleteContentLikedUseCase.swift (2)
12-18: Well-structured use case implementation with proper dependency injection.The
DeleteContentLikedUseCaseclass demonstrates good architectural practices:
- Proper use of dependency injection
- Clear encapsulation of repository dependency
- Strong adherence to the single responsibility principle
This implementation maintains consistency with other use cases in the codebase, promoting a unified architectural approach.
22-26: Effective handling of content unliking operations.The
executemethod properly delegates to the repository'sdeleteContentLikedmethod while maintaining an appropriate return type withAnyPublisher<Void, WableError>. This ensures consistent error handling and asynchronous operation processing throughout the feature.Wable-iOS/Domain/RepositoryInterface/UserSessionRepository.swift (1)
19-19: Good simplification of the APIThe removal of the redundant
userIDparameter fromupdateUserSessionis a good change since theUserSessionobject already contains its own ID. This makes the API cleaner and prevents potential inconsistencies where the provided userID might differ from the session's ID.Wable-iOS/Domain/UseCase/Home/CreateCommentUseCase.swift (2)
12-18: LGTM! Clean implementation of the use case patternThe class properly follows the dependency injection pattern by accepting the repository through the constructor and storing it as a private property.
22-31: Well-designed execute methodThe execute method properly delegates to the repository with all necessary parameters, following the Clean Architecture pattern. The use of Combine with AnyPublisher is appropriate for asynchronous operations.
Wable-iOS/Domain/UseCase/Home/CreateContentLikedUseCase.swift (1)
12-18: LGTM! Proper implementation of the use case patternThe class correctly follows the dependency injection pattern by accepting the repository through the constructor.
Wable-iOS/Infra/Network/TargetType/ContentLikedTargetType.swift (3)
19-21: Good addition of multipartFormData supportAdding this property maintains consistency with other target types that also support multipart form data, even though it's not used for this particular target type.
25-25: Appropriate update to the endpoint caseThe change to ignore the request parameter in the endpoint calculation is correct since the request doesn't affect the endpoint URL.
37-42: Well-structured requestBody implementationThe updated requestBody property correctly returns the request object for createContentLiked and nil for deleteContentLiked, which aligns with the HTTP method usage (POST with body, DELETE without body).
Wable-iOS/Presentation/Onboarding/View/ProfileRegisterView.swift (1)
94-96: LGTM! Improved profile image selection logicThe refactoring improves code organization by moving the default image list to a class property instead of recreating it locally. This change enhances maintainability and reusability.
Wable-iOS/App/SceneDelegate.swift (1)
86-91: Enhanced login view model initialization with clearer namingThe changes improve the code in two ways:
- Renamed
useCasetofetchUserAuthUseCasefor better clarity about its purpose- Added
updateUserSessionUseCaseparameter to handle user session updates, which enhances separation of concernsThis reflects good practice by making the code more explicit about the responsibilities of each component.
Wable-iOS/Domain/UseCase/Home/FetchContentCommentListUseCase.swift (1)
12-18: Well-structured use case implementation following clean architectureThe class follows good practices by:
- Accepting the repository through dependency injection
- Keeping the repository as a private property
- Using a clear, descriptive name indicating its single responsibility
Wable-iOS/Domain/UseCase/Login/FetchUserAuthUseCase.swift (2)
43-44: Profile URL type change improves type safetyThe change from using
profileURL?.absoluteString ?? ""to directly usingprofileURLsuggests theUserSessionstruct now accepts an optional URL rather than a String. This is a good improvement as it:
- Maintains the URL type throughout the system
- Avoids unnecessary string conversions
- Preserves the nullability of the field
48-49: Simplified user session updateThe removal of the second parameter (
forUserID) simplifies the API, suggesting theUserSessionobject now contains all necessary information. This is good practice as it reduces redundancy in the method call.Wable-iOS/Domain/RepositoryInterface/ProfileRepository.swift (2)
11-11: UIKit import added to support UIImage parameterThe addition of the UIKit import is necessary to support the new UIImage parameter in the updateUserProfile method.
16-22: Method signature expanded to support more profile update optionsThe updateUserProfile method signature has been expanded to include three additional parameters:
- isAlarmAllowed: For controlling general alarm settings
- image: For updating the user's profile picture
- defaultProfileType: For specifying a default profile type
This change enables more comprehensive user profile updates, aligning with the implementation in ProfileRepositoryImpl.swift.
Consider documenting the difference between
isAlarmAllowedandisPushAlarmAllowedparameters, as their distinction isn't immediately clear from the naming alone.Wable-iOS/Domain/UseCase/Home/CreateCommentLikedUseCase.swift (1)
12-18: LGTM: Well-structured use case implementation with dependency injectionThe CreateCommentLikedUseCase class is properly structured with dependency injection via constructor, following good design principles.
Wable-iOS/Domain/UseCase/Onboarding/CreateUserProfileUseCase.swift (2)
10-10: UIKit import added to support UIImage parameterThe addition of the UIKit import is necessary to support the new UIImage parameter.
21-28:Details
❓ Verification inconclusive
Method signature updated to match repository interface
The execute method has been updated with additional parameters to align with the changes in the ProfileRepository interface. This ensures consistency across the application layers.
The use of default values for optional parameters is a good practice for API design.
Ensure that all call sites to this method have been updated to accommodate the new required parameters (isAlarmAllowed).
🏁 Script executed:
#!/bin/bash # Find all calls to execute method in CreateUserProfileUseCase to verify they've been updated rg "CreateUserProfileUseCase.*execute\(" --type swift -A 5Length of output: 60
Action Required: The updated method signature in
CreateUserProfileUseCase.swiftnow fully aligns with the repository interface and correctly uses default values for the optional parameters. However, our automated search for call sites invokingexecute((and specifically passing theisAlarmAllowedparameter) did not return any results. Please manually verify that all call sites throughout the codebase have been updated to accommodate this new parameter.
- Confirmation Points:
- The method signature now includes the
isAlarmAllowedparameter.- Default values are applied for the optional parameters, ensuring API design consistency.
- Automated search results were inconclusive—manual verification is recommended.
Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift (1)
50-50: Method signature improved with better encapsulationThe refactoring of
updateUserSessionto accept only theUserSessionparameter is a good improvement. It eliminates redundancy by using the ID from the session object itself (session.id) rather than requiring a separate userID parameter.Also applies to: 53-53, 58-58
Wable-iOS/Infra/Network/TargetType/BaseTargetType.swift (3)
16-16: Good addition of multipart form data supportAdding multipart form data support to the
BaseTargetTypeprotocol enhances its flexibility for handling file uploads and other complex data structures.
38-40: Correct task handling for multipart form dataThis implementation correctly checks for the presence of multipart form data and returns the appropriate task type when it exists.
49-53: Appropriate Content-Type header for multipart requestsThe headers implementation correctly sets the Content-Type to "multipart/form-data" when multipart form data is present, which is necessary for the server to properly parse the request.
Wable-iOS/Presentation/Onboarding/ViewController/LCKTeamViewController.swift (3)
16-16: Good implementation of the initialization patternThe addition of the
lckYearandlckTeamproperties along with the proper initializer implementation follows good Swift practices. The required initializer with the fatalError is also appropriately implemented for classes that shouldn't be initialized from a storyboard.Also applies to: 18-18, 26-34
73-79: Proper data passing between view controllersThe implementation correctly passes the
lckYearandlckTeamdata to theProfileRegisterViewController, maintaining data flow through the onboarding process.Also applies to: 83-89
104-105: Team selection handling is properly implementedThe code correctly updates the
lckTeamproperty when a team is selected in the collection view.Wable-iOS/Infra/Network/TargetType/ContentTargetType.swift (1)
84-89: Appropriate removal of createContent case from requestBodySince the createContent case is now handled by the multipartFormData property, it's correct to remove it from the requestBody implementation.
Wable-iOS/Data/RepositoryImpl/ProfileRepositoryImpl.swift (1)
38-44: Method signature expansion looks consistent.
You've added new parameters (isAlarmAllowed,image, anddefaultProfileType) to account for alarm settings and profile image handling. This is a clean approach to passing all necessary data for user profile updates.Wable-iOS/Infra/Network/TargetType/ProfileTargetType.swift (2)
57-60: Return.nonefor other cases is valid.
The fallback behavior of returningnilformultipartFormDatain non-updateUserProfilecases is logically consistent and helps avoid unintended uploads. No further changes needed.
45-54:Details
✅ Verification successful
Properly handle image data.
Appendingfiledata is correct. Ensure consistency with the server-side API, verifying that the "file" field name and accepted MIME types match the backend expectations.
🏁 Script executed:
#!/bin/bash # Verifying references to "file" in server code or docs: rg -A 5 "multipart"Length of output: 11657
Image Data Handling Verified – Confirm Server API Alignment
The implementation in
ProfileTargetType.swiftproperly appends multipart form data whenrequest.fileis provided. The use of the"file"field with a hard-coded filename ("profile.jpeg") and the MIME type"image/jpeg"follows the pattern observed in similar targets and appears correct. Please ensure that these values match the backend API specifications for handling profile image uploads.
- Verify that the
"file"field name is the expected parameter for image uploads.- Confirm that the MIME type
"image/jpeg"and the default filename"profile.jpeg"are appropriate per server requirements.Wable-iOS/Presentation/Onboarding/ViewController/AgreementViewController.swift (1)
40-49: Init parameters match usage in the next flow.
Declarative init withnickname,lckTeam,lckYear,profileImage, anddefaultImageensures clarity about required data. This is straightforward and looks good.Wable-iOS/Domain/UseCase/Home/FetchUserInformationUseCase.swift (1)
12-18: Straightforward class implementation.
This class is correctly set up with a private repository dependency. The structure is clear and uses dependency injection effectively.Wable-iOS/Presentation/TabBar/TabBarController.swift (6)
13-16: New property for controlling loading screen.
IntroducingshouldShowLoadingScreenis clear and maintains custom initialization logic for the tab bar.
19-25: Lazy initialization of HomeViewController with loading screen property.
PassingshouldShowLoadingScreenexplicitly is a good approach to conditionally configure the Home screen. Ensure changes to this variable post-initialization are handled if needed.Also applies to: 29-29
54-56: Custom initializer for TabBarController.
Defininginit(shouldShowLoadingScreen: Bool = true)is a neat pattern—no concerns here.
71-71: Delegate setup call added to viewDidLoad.
InvokingsetupDelegate()ensures the tab bar controller is its own delegate—straightforward.
102-104: Explicit delegate assignment.
setupDelegate()setsdelegate = selfto handle tab bar events. This is a clean approach without complications.
125-137: Implementing didSelect for tab switching.
Scrolling the Home screen to top when re-selected is a common UX pattern. This logic is fine, but remember to handle potential nil references if the view changes in the future.Wable-iOS/Presentation/WableComponent/View/PostUserInfoView.swift (3)
92-92: Layout tweak for profileImageView.
Bottom inset adjusted to 3. This minor spacing change seems reasonable—no issues.
111-111: GhostCountLabel bottom inset.
Reduced to 1. Please verify this won’t clip or overlap other views on small screens.
185-187: Optional user profile URL and fan team.
Allowing optional parameters forconfigureViewis more robust. Ensure that any downstream usage of these values checks for nil.Wable-iOS/Presentation/Home/View/WritePostViewController.swift (18)
1-8: New file header and metadata.
No issues with the file documentation block.
9-12: Imports for Combine, UIKit, and PhotosUI.
Appropriate imports given the reactive usage and image picker functionality.
13-21: Property definitions for view model and relays.
Use ofPassthroughRelayandCancelBagis clear. This structure nicely keeps track of subscriptions.
23-74: UI components initialization.
Defining scroll view, text views, buttons, and image view with.thenis concise. Ensure placeholders and styling remain consistent across different screen sizes.
76-86: Custom initializer for WritePostViewController.
PassingWritePostViewModelinjects dependencies effectively. Looks good.
88-96: View life cycle: viewDidLoad.
Centralizes setup calls neatly. No issues.
101-115: setupView invocation.
Adding subviews to the main view and scroll view in an organized fashion. This is consistent with your architecture.
117-158: SnapKit constraints arrangement.
All constraints appear coherent. Keep an eye on keyboard layout guide offsets for smaller screen devices.
160-168: setupAction with button targets.
Registering button taps for image picking and post submission. Straightforward approach.
170-173: Delegate assignments for TextViews.
Ensures placeholder logic and character counting can be handled internally.
175-189: Reactive binding with view model transform.
Publishing post creation success to show a toast and navigate back is well-handled.
195-210: addButtonDidTap for photo library.
Handles permission checks properly. Consider alerting the user to grant "limited" vs. "full" permission if relevant.
212-219: postButtonDidTap.
Sends user input to the view model. Straightforward approach with no major concerns.
226-245: Character counting logic.
Enforces a maximum combined length of 500 and shapes placeholder usage. This is well implemented.
247-256: Photo picker presentation.
Dismisses the picker cleanly. No apparent issues with concurrency or memory usage.
258-272: presentSettings for photo permissions.
Directly providing an alert with an option to jump to settings is user-friendly.
296-346: UITextViewDelegate placeholder logic.
Managing placeholder text is clean. The combined count check inshouldChangeTextInis well-thought-out, preventing overshoot beyond 500. Great job.
351-356: Constant placeholders for title and content.
Clear placeholders enhance user experience. Good approach to centralize these strings.Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (6)
27-27: Good pattern for like button handling.The addition of the
likeButtonTapHandlerclosure property allows for flexible event handling between the cell and its parent controller without creating tight coupling.
44-49: Good interactive image implementation.Making the image view interactive with
isUserInteractionEnabled = trueallows for the tap gesture to work correctly while preserving the existing styling.
51-59: Improved text display with UITextView.Replacing UILabel with UITextView offers better functionality like hyperlink detection while maintaining the appearance with proper configuration of container insets and padding.
85-92: Great implementation of prepareForReuse.Properly cleaning up resources by canceling download tasks and resetting UI elements in
prepareForReuse()prevents memory leaks and visual glitches when cells are reused.
160-166: Properly configured gesture recognizer.The tap gesture recognizer is correctly added to the content image view with appropriate target-action setup.
246-247: Good propagation of event handler.Correctly storing the handler in the
configureCellmethod ensures that the handler is associated with the specific instance of the cell being configured.Wable-iOS/Presentation/Login/LoginViewModel.swift (2)
17-17: Good addition of session management capability.Adding the
updateUserSessionUseCaseproperty allows the ViewModel to handle user session updates as part of the login flow.
23-26: Properly updated initializer with dependency injection.The initializer correctly initializes both use cases, following good dependency injection practices.
Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (3)
12-22: Good ViewModel structure with clear separation of responsibilities.The HomeViewModel correctly follows dependency injection by accepting use cases through its initializer and has a clean structure separating content fetching and like management functionalities.
24-38: Well-designed input/output contract using Combine.The Input and Output structs provide a clear contract for what events the ViewModel can handle and what states it can emit, following the reactive programming pattern effectively.
126-131: Good use of constants with meaningful names.The use of named constants in a private extension improves code readability and maintainability by avoiding magic numbers in the implementation.
Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (2)
12-46: Well-structured ViewModel with comprehensive use case injection.The HomeDetailViewModel is well-structured with proper dependency injection of all required use cases through the initializer, following clean architecture principles.
201-206: Good use of constants with meaningful names.The use of named constants in a private extension improves code readability and maintainability by avoiding magic numbers in the implementation.
Wable-iOS/Presentation/Onboarding/ViewController/ProfileRegisterViewController.swift (8)
29-35: Looks good!The custom initializer design with required
lckYearandlckTeamensures proper initialization and data passing in the onboarding flow.
36-38: Standardinit?(coder:)usage.This pattern is acceptable for a fully programmatic UI. No issues found here.
47-47: Tap gesture addition is good.Ensuring the keyboard dismisses on tap outside of controls improves usability.
60-60: Guard against potential out-of-bounds access.
rootView.defaultImageList[0]assumes the list is never empty. Consider verifying thatrootView.defaultImageListis non-empty to avoid a runtime error.if !rootView.defaultImageList.isEmpty { - defaultImage = rootView.defaultImageList[0].uppercased + defaultImage = rootView.defaultImageList.first?.uppercased() }
81-85: Keyboard dismissal handling is well-implemented.Adding a tap gesture recognizer is an appropriate iOS convention for dismissing the keyboard.
89-91: Method name and usage are aligned.
dismissKeyboard()effectively targets the correct behavior.
133-145: Neat navigation flow.Pushing
AgreementViewControllerwith all required values is clear. No issues found with the data passing logic.
186-186: Simple image selection toggle.Nullifying
defaultImagewhen a real image is selected is consistent with the logic of preferring a user-picked image.Wable-iOS/Presentation/WableComponent/Cell/CommentCollectionViewCell.swift (9)
13-22: Enum addition is clear.Using
CommentTypewithrippleandreplyclarifies comment categorization.
41-42: Closure-based handlers.Exposing tap handlers for like/reply encourages separation of concerns and flexible usage.
57-58: Lazy initialization forlikeButtonandreplyButton.This approach is fine, ensuring buttons are ready after the cell is instantiated.
88-88: AddingreplyButtonto subviews.No concerns; it extends the cell functionality properly.
115-119: Reply button constraint setup looks correct.Ensuring it aligns horizontally with
likeButtonis consistent with the UI layout.
155-157: Reply button tap handling is straightforward.Invoking the
replyButtonTapHandlerclosure is a clean implementation.
158-164: Like button toggling looks good.The logic for incrementing or decrementing the like count, updating the button, and invoking the closure is coherent.
193-196: ExtendedconfigureCellsignature is flexible.Allowing external injection of like/reply tap closures promotes reusability for different contexts.
217-222: Button configuration is properly specialized.Invoking
replyButton.configureButton()and configuringlikeButtonwith.commenttype is consistent with the design.Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (18)
1-7: File header is acceptable.No issues to address here.
8-10: Combine and UIKit imports are correct.No conflicts or redundancies spotted.
11-19: Section enum adds clarity to the data source.Separating content and comment sections is straightforward and beneficial for readability.
20-25: Item enum is well-structured.Utilizing associated values for content and comment items aligns well with Swift’s type safety.
29-31: Typealiases boost readability.Defining
DataSourceandSnapshotclarifies diffable data source usage.
34-45: ViewModel and subject setup is logical.All required Combine subjects and the
dataSourceproperty are conveniently declared.
48-55: Fetching user info.Instantiating
FetchUserInformationUseCasewith a repository is valid. No immediate concerns.
59-65: Collection view setup.Refresh control and vertical bounce are good UI details.
67-69: Container for comment input.Straightforward background setup. Looks good.
71-83: Comment text view and button styling.The corner radius, background color, and placeholder images reflect standard UI patterns.
85-88: Loading indicator usage is standard.Hiding it when stopped is a user-friendly behavior.
90-101: Custom initializer with navigation style.Accepting a
HomeDetailViewModelandCancelBagis a clarified approach for dependency injection.
103-111: View lifecycle entry is consistent.
viewDidLoadtriggers all setup methods. No issues spotted.
117-120: Subview addition is correct.All relevant UI elements are attached to the primary view.
122-148: Constraint definitions are appropriate.Using SnapKit effectively for layout of collection view and input bar. No layout conflicts seen.
207-249: Binding logic is well-organized.The stream of inputs and outputs aligns with Combine best practices for reactive flows.
269-291: Section layout logic is succinct.Using a compositional layout with estimated heights for content vs. comments. No issues spotted.
294-316: Diffable data source updates are correct.
updateContent(_:)andupdateComments(_:)handle snapshot changes in a clean, modern way.
| func transform(input: Input, cancelBag: CancelBag) -> Output { | ||
| let postSuccessRelay = PassthroughRelay<Void>() | ||
|
|
||
| input.postButtonDidTap | ||
| .map { (title, content, image) -> AnyPublisher<Void, Never> in | ||
| return self.createContentUseCase.execute( | ||
| title: title, | ||
| text: content ?? "", | ||
| image: image?.jpegData(compressionQuality: 0.1) | ||
| ) | ||
| .handleEvents(receiveCompletion: { completion in | ||
| if case .failure(let error) = completion { | ||
| WableLogger.log("게시물 생성 중 오류 발생: \(error)", for: .error) | ||
| } | ||
| }) | ||
| .catch { _ -> AnyPublisher<Void, Never> in | ||
| return Empty<Void, Never>().eraseToAnyPublisher() | ||
| } | ||
| .eraseToAnyPublisher() | ||
| } | ||
| .switchToLatest() | ||
| .sink( | ||
| receiveCompletion: { completion in | ||
| WableLogger.log("게시물 생성 작업 완료", for: .debug) | ||
| }, | ||
| receiveValue: { value in | ||
| postSuccessRelay.send(()) | ||
| WableLogger.log("postSuccessRelay 전송 완료", for: .debug) | ||
| } | ||
| ) | ||
| .store(in: cancelBag) | ||
|
|
||
| return Output( | ||
| postSuccess: postSuccessRelay.asDriver() | ||
| ) | ||
| } |
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
Address unused closure parameters and consider image compression quality
The transform method has solid error handling, but there are two unused parameters in closure blocks that should be replaced with underscore according to the static analysis hints.
Also, the JPEG compression quality is fixed at 0.1, which is quite aggressive and may result in visible image quality loss. Consider making this configurable or increasing it to a more balanced value like 0.5.
- .handleEvents(receiveCompletion: { completion in
+ .handleEvents(receiveCompletion: { [weak self] completion in
if case .failure(let error) = completion {
WableLogger.log("게시물 생성 중 오류 발생: \(error)", for: .error)
}
})
.catch { _ -> AnyPublisher<Void, Never> in
return Empty<Void, Never>().eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
.switchToLatest()
.sink(
- receiveCompletion: { completion in
+ receiveCompletion: { _ in
WableLogger.log("게시물 생성 작업 완료", for: .debug)
},
- receiveValue: { value in
+ receiveValue: { _ in
postSuccessRelay.send(())
WableLogger.log("postSuccessRelay 전송 완료", for: .debug)
}Also consider adjusting the compression quality:
- image: image?.jpegData(compressionQuality: 0.1)
+ image: image?.jpegData(compressionQuality: 0.5)📝 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.
| func transform(input: Input, cancelBag: CancelBag) -> Output { | |
| let postSuccessRelay = PassthroughRelay<Void>() | |
| input.postButtonDidTap | |
| .map { (title, content, image) -> AnyPublisher<Void, Never> in | |
| return self.createContentUseCase.execute( | |
| title: title, | |
| text: content ?? "", | |
| image: image?.jpegData(compressionQuality: 0.1) | |
| ) | |
| .handleEvents(receiveCompletion: { completion in | |
| if case .failure(let error) = completion { | |
| WableLogger.log("게시물 생성 중 오류 발생: \(error)", for: .error) | |
| } | |
| }) | |
| .catch { _ -> AnyPublisher<Void, Never> in | |
| return Empty<Void, Never>().eraseToAnyPublisher() | |
| } | |
| .eraseToAnyPublisher() | |
| } | |
| .switchToLatest() | |
| .sink( | |
| receiveCompletion: { completion in | |
| WableLogger.log("게시물 생성 작업 완료", for: .debug) | |
| }, | |
| receiveValue: { value in | |
| postSuccessRelay.send(()) | |
| WableLogger.log("postSuccessRelay 전송 완료", for: .debug) | |
| } | |
| ) | |
| .store(in: cancelBag) | |
| return Output( | |
| postSuccess: postSuccessRelay.asDriver() | |
| ) | |
| } | |
| func transform(input: Input, cancelBag: CancelBag) -> Output { | |
| let postSuccessRelay = PassthroughRelay<Void>() | |
| input.postButtonDidTap | |
| .map { (title, content, image) -> AnyPublisher<Void, Never> in | |
| return self.createContentUseCase.execute( | |
| title: title, | |
| text: content ?? "", | |
| image: image?.jpegData(compressionQuality: 0.5) | |
| ) | |
| .handleEvents(receiveCompletion: { [weak self] completion in | |
| if case .failure(let error) = completion { | |
| WableLogger.log("게시물 생성 중 오류 발생: \(error)", for: .error) | |
| } | |
| }) | |
| .catch { _ -> AnyPublisher<Void, Never> in | |
| return Empty<Void, Never>().eraseToAnyPublisher() | |
| } | |
| .eraseToAnyPublisher() | |
| } | |
| .switchToLatest() | |
| .sink( | |
| receiveCompletion: { _ in | |
| WableLogger.log("게시물 생성 작업 완료", for: .debug) | |
| }, | |
| receiveValue: { _ in | |
| postSuccessRelay.send(()) | |
| WableLogger.log("postSuccessRelay 전송 완료", for: .debug) | |
| } | |
| ) | |
| .store(in: cancelBag) | |
| return Output( | |
| postSuccess: postSuccessRelay.asDriver() | |
| ) | |
| } |
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 52-52: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
[Warning] 55-55: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
| // TODO: 온보딩 기능 개발 후 다시 되돌려야 함 | ||
| sessionInfo.isNewUser ? owner.navigateToOnboarding() : owner.navigateToOnboarding() |
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.
Remove temporary navigation bypass after completing onboarding implementation.
The code currently forces all users to go through onboarding regardless of their status, with a TODO indicating this is temporary. Additionally, using a ternary operator for void functions makes the code less readable.
- // TODO: 온보딩 기능 개발 후 다시 되돌려야 함
- sessionInfo.isNewUser ? owner.navigateToOnboarding() : owner.navigateToOnboarding()
+ if sessionInfo.isNewUser {
+ owner.navigateToOnboarding()
+ } else {
+ owner.navigateToHome()
+ }📝 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.
| // TODO: 온보딩 기능 개발 후 다시 되돌려야 함 | |
| sessionInfo.isNewUser ? owner.navigateToOnboarding() : owner.navigateToOnboarding() | |
| if sessionInfo.isNewUser { | |
| owner.navigateToOnboarding() | |
| } else { | |
| owner.navigateToHome() | |
| } |
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 178-178: TODOs should be resolved (온보딩 기능 개발 후 다시 되돌려야 함)
(todo)
[Warning] 179-179: Using ternary to call Void functions should be avoided
(void_function_in_ternary)
| var multipartFormData: [Moya.MultipartFormData]? { | ||
| switch self { | ||
| case .updateUserProfile(request: let request): | ||
| var multipartFormData: [MultipartFormData] = [] | ||
|
|
||
| let parameters: [String: Any?] = [ | ||
| "nickname": request.info?.nickname, | ||
| "isAlarmAllowed": request.info?.isAlarmAllowed, | ||
| "memberIntro": request.info?.memberIntro, | ||
| "isPushAlarmAllowed": request.info?.isPushAlarmAllowed, | ||
| "fcmToken": request.info?.fcmToken, | ||
| "memberLckYears": request.info?.memberLCKYears, | ||
| "memberFanTeam": request.info?.memberFanTeam, | ||
| "memberDefaultProfileImage": request.info?.memberDefaultProfileImage | ||
| ] | ||
|
|
||
| if let jsonData = try? JSONSerialization.data(withJSONObject: parameters, options: []) { | ||
| let textData = MultipartFormData( | ||
| provider: .data(jsonData), | ||
| name: "info" | ||
| ) | ||
|
|
||
| multipartFormData.append(textData) | ||
| } |
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
Ensure error handling for JSON serialization.
Currently, the JSON serialization uses try? and silently ignores failures. If serialization fails for any reason, the request will proceed without essential data. Consider logging an error or returning an early failure if jsonData is nil.
if let jsonData = try? JSONSerialization.data(withJSONObject: parameters, options: []) {
...
} else {
+ // You might want to throw or log an error here
}📝 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.
| var multipartFormData: [Moya.MultipartFormData]? { | |
| switch self { | |
| case .updateUserProfile(request: let request): | |
| var multipartFormData: [MultipartFormData] = [] | |
| let parameters: [String: Any?] = [ | |
| "nickname": request.info?.nickname, | |
| "isAlarmAllowed": request.info?.isAlarmAllowed, | |
| "memberIntro": request.info?.memberIntro, | |
| "isPushAlarmAllowed": request.info?.isPushAlarmAllowed, | |
| "fcmToken": request.info?.fcmToken, | |
| "memberLckYears": request.info?.memberLCKYears, | |
| "memberFanTeam": request.info?.memberFanTeam, | |
| "memberDefaultProfileImage": request.info?.memberDefaultProfileImage | |
| ] | |
| if let jsonData = try? JSONSerialization.data(withJSONObject: parameters, options: []) { | |
| let textData = MultipartFormData( | |
| provider: .data(jsonData), | |
| name: "info" | |
| ) | |
| multipartFormData.append(textData) | |
| } | |
| var multipartFormData: [Moya.MultipartFormData]? { | |
| switch self { | |
| case .updateUserProfile(request: let request): | |
| var multipartFormData: [MultipartFormData] = [] | |
| let parameters: [String: Any?] = [ | |
| "nickname": request.info?.nickname, | |
| "isAlarmAllowed": request.info?.isAlarmAllowed, | |
| "memberIntro": request.info?.memberIntro, | |
| "isPushAlarmAllowed": request.info?.isPushAlarmAllowed, | |
| "fcmToken": request.info?.fcmToken, | |
| "memberLckYears": request.info?.memberLCKYears, | |
| "memberFanTeam": request.info?.memberFanTeam, | |
| "memberDefaultProfileImage": request.info?.memberDefaultProfileImage | |
| ] | |
| if let jsonData = try? JSONSerialization.data(withJSONObject: parameters, options: []) { | |
| let textData = MultipartFormData( | |
| provider: .data(jsonData), | |
| name: "info" | |
| ) | |
| multipartFormData.append(textData) | |
| } else { | |
| // You might want to throw or log an error here | |
| } | |
| } | |
| } |
| userInformationUseCase.fetchActiveUserInfo() | ||
| .withUnretained(self) | ||
| .sink { owner, userSession in | ||
| guard let userSession = userSession else { return } | ||
|
|
||
| tabBarController.present(noticeViewController, animated: true) | ||
| owner.profileUseCase.execute( | ||
| profile: UserProfile( | ||
| user: User( | ||
| id: userSession.id, | ||
| nickname: owner.nickname, | ||
| profileURL: userSession.profileURL, | ||
| fanTeam: LCKTeam(rawValue: owner.lckTeam) | ||
| ), | ||
| introduction: "", | ||
| ghostCount: 0, | ||
| lckYears: owner.lckYear, | ||
| userLevel: 1 | ||
| ), | ||
| isPushAlarmAllowed: false, | ||
| isAlarmAllowed: owner.rootView.marketingAgreementItemView.checkButton.isSelected, | ||
| image: owner.profileImage, | ||
| defaultProfileType: owner.defaultImage | ||
| ) | ||
| .receive(on: DispatchQueue.main) | ||
| .sink { _ in | ||
| } receiveValue: { [weak self] _ in | ||
| guard let cancelBag = self?.cancelBag else { return } | ||
|
|
||
| self?.userInformationUseCase.updateUserSession( | ||
| session: UserSession( | ||
| id: userSession.id, | ||
| nickname: userSession.nickname, | ||
| profileURL: userSession.profileURL, | ||
| isPushAlarmAllowed: userSession.isPushAlarmAllowed, | ||
| isAdmin: userSession.isAdmin, | ||
| isAutoLoginEnabled: true, | ||
| // TODO: FCM 구현 이후 바꿔줘야 함 | ||
| notificationBadgeCount: 0 | ||
| ) | ||
| ) | ||
| .sink(receiveCompletion: { _ in | ||
| }, receiveValue: { _ in | ||
| WableLogger.log("세션 저장 완료", for: .debug) | ||
|
|
||
| guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, | ||
| let loginViewController = windowScene.windows.first?.rootViewController | ||
| else { | ||
| return | ||
| } | ||
|
|
||
| let tabBarController = TabBarController(shouldShowLoadingScreen: false) | ||
|
|
||
| self?.dismiss(animated: false) { | ||
| guard let nickname = self?.nickname else { return } | ||
|
|
||
| loginViewController.present(tabBarController, animated: true) { | ||
| let noticeViewController = WableSheetViewController( | ||
| title: "와블과 함께해 주셔서 감사합니다!", | ||
| message: "\(nickname)님\n와블의 일원이 되신 것을 환영해요.\nLCK 함께 보며 같이 즐겨요 :)" | ||
| ) | ||
|
|
||
| noticeViewController.addAction(.init(title: "와블 즐기러 가기", style: .primary)) | ||
|
|
||
| tabBarController.present(noticeViewController, animated: true) | ||
| } | ||
| } | ||
| }) | ||
| .store(in: cancelBag) | ||
| } | ||
| .store(in: owner.cancelBag) | ||
| } | ||
| } | ||
| .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.
🛠️ Refactor suggestion
Add error handling and reduce complexity in the Combine chain.
- If
profileUseCase.executefails, there's no fallback or UI update for the user. Consider a dedicated failure handling path. - The Combine chain is quite long. Extracting logic into smaller methods or adopting a ViewModel could enhance readability.
.sink { completion in
switch completion {
case .failure(let error):
+ // Prompt user with error or retry logic
case .finished:
break
}
} receiveValue: { ... }📝 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.
| userInformationUseCase.fetchActiveUserInfo() | |
| .withUnretained(self) | |
| .sink { owner, userSession in | |
| guard let userSession = userSession else { return } | |
| tabBarController.present(noticeViewController, animated: true) | |
| owner.profileUseCase.execute( | |
| profile: UserProfile( | |
| user: User( | |
| id: userSession.id, | |
| nickname: owner.nickname, | |
| profileURL: userSession.profileURL, | |
| fanTeam: LCKTeam(rawValue: owner.lckTeam) | |
| ), | |
| introduction: "", | |
| ghostCount: 0, | |
| lckYears: owner.lckYear, | |
| userLevel: 1 | |
| ), | |
| isPushAlarmAllowed: false, | |
| isAlarmAllowed: owner.rootView.marketingAgreementItemView.checkButton.isSelected, | |
| image: owner.profileImage, | |
| defaultProfileType: owner.defaultImage | |
| ) | |
| .receive(on: DispatchQueue.main) | |
| .sink { _ in | |
| } receiveValue: { [weak self] _ in | |
| guard let cancelBag = self?.cancelBag else { return } | |
| self?.userInformationUseCase.updateUserSession( | |
| session: UserSession( | |
| id: userSession.id, | |
| nickname: userSession.nickname, | |
| profileURL: userSession.profileURL, | |
| isPushAlarmAllowed: userSession.isPushAlarmAllowed, | |
| isAdmin: userSession.isAdmin, | |
| isAutoLoginEnabled: true, | |
| // TODO: FCM 구현 이후 바꿔줘야 함 | |
| notificationBadgeCount: 0 | |
| ) | |
| ) | |
| .sink(receiveCompletion: { _ in | |
| }, receiveValue: { _ in | |
| WableLogger.log("세션 저장 완료", for: .debug) | |
| guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, | |
| let loginViewController = windowScene.windows.first?.rootViewController | |
| else { | |
| return | |
| } | |
| let tabBarController = TabBarController(shouldShowLoadingScreen: false) | |
| self?.dismiss(animated: false) { | |
| guard let nickname = self?.nickname else { return } | |
| loginViewController.present(tabBarController, animated: true) { | |
| let noticeViewController = WableSheetViewController( | |
| title: "와블과 함께해 주셔서 감사합니다!", | |
| message: "\(nickname)님\n와블의 일원이 되신 것을 환영해요.\nLCK 함께 보며 같이 즐겨요 :)" | |
| ) | |
| noticeViewController.addAction(.init(title: "와블 즐기러 가기", style: .primary)) | |
| tabBarController.present(noticeViewController, animated: true) | |
| } | |
| } | |
| }) | |
| .store(in: cancelBag) | |
| } | |
| .store(in: owner.cancelBag) | |
| } | |
| } | |
| .store(in: cancelBag) | |
| userInformationUseCase.fetchActiveUserInfo() | |
| .withUnretained(self) | |
| .sink { owner, userSession in | |
| guard let userSession = userSession else { return } | |
| owner.profileUseCase.execute( | |
| profile: UserProfile( | |
| user: User( | |
| id: userSession.id, | |
| nickname: owner.nickname, | |
| profileURL: userSession.profileURL, | |
| fanTeam: LCKTeam(rawValue: owner.lckTeam) | |
| ), | |
| introduction: "", | |
| ghostCount: 0, | |
| lckYears: owner.lckYear, | |
| userLevel: 1 | |
| ), | |
| isPushAlarmAllowed: false, | |
| isAlarmAllowed: owner.rootView.marketingAgreementItemView.checkButton.isSelected, | |
| image: owner.profileImage, | |
| defaultProfileType: owner.defaultImage | |
| ) | |
| .receive(on: DispatchQueue.main) | |
| .sink { completion in | |
| switch completion { | |
| case .failure(let error): | |
| // Prompt user with error or retry logic | |
| break | |
| case .finished: | |
| break | |
| } | |
| } receiveValue: { [weak self] _ in | |
| guard let cancelBag = self?.cancelBag else { return } | |
| self?.userInformationUseCase.updateUserSession( | |
| session: UserSession( | |
| id: userSession.id, | |
| nickname: userSession.nickname, | |
| profileURL: userSession.profileURL, | |
| isPushAlarmAllowed: userSession.isPushAlarmAllowed, | |
| isAdmin: userSession.isAdmin, | |
| isAutoLoginEnabled: true, | |
| // TODO: FCM 구현 이후 바꿔줘야 함 | |
| notificationBadgeCount: 0 | |
| ) | |
| ) | |
| .sink(receiveCompletion: { _ in | |
| }, receiveValue: { _ in | |
| WableLogger.log("세션 저장 완료", for: .debug) | |
| guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, | |
| let loginViewController = windowScene.windows.first?.rootViewController | |
| else { | |
| return | |
| } | |
| let tabBarController = TabBarController(shouldShowLoadingScreen: false) | |
| self?.dismiss(animated: false) { | |
| guard let nickname = self?.nickname else { return } | |
| loginViewController.present(tabBarController, animated: true) { | |
| let noticeViewController = WableSheetViewController( | |
| title: "와블과 함께해 주셔서 감사합니다!", | |
| message: "\(nickname)님\n와블의 일원이 되신 것을 환영해요.\nLCK 함께 보며 같이 즐겨요 :)" | |
| ) | |
| noticeViewController.addAction(.init(title: "와블 즐기러 가기", style: .primary)) | |
| tabBarController.present(noticeViewController, animated: true) | |
| } | |
| } | |
| }) | |
| .store(in: cancelBag) | |
| } | |
| .store(in: owner.cancelBag) | |
| } | |
| .store(in: cancelBag) |
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 176-176: TODOs should be resolved (FCM 구현 이후 바꿔줘야 함)
(todo)
| @objc func contentImageViewDidTap() { | ||
| guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, | ||
| let rootViewController = windowScene.windows.first?.rootViewController, | ||
| let image = contentImageView.image | ||
| else { | ||
| return | ||
| } | ||
|
|
||
| rootViewController.present(PhotoDetailViewController(image: image), 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
Consider using a delegate pattern for navigation.
The current implementation directly presents a view controller by finding the root view controller through the application's window scene. This tightly couples the cell with the navigation logic.
Consider using a delegate pattern or closure for navigation to improve separation of concerns:
-@objc func contentImageViewDidTap() {
- guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
- let rootViewController = windowScene.windows.first?.rootViewController,
- let image = contentImageView.image
- else {
- return
- }
-
- rootViewController.present(PhotoDetailViewController(image: image), animated: true)
-}
+var contentImageTapHandler: ((UIImage) -> Void)?
+
+@objc func contentImageViewDidTap() {
+ guard let image = contentImageView.image else { return }
+ contentImageTapHandler?(image)
+}Then in your view controller that uses this cell, you would set the handler:
cell.contentImageTapHandler = { [weak self] image in
self?.present(PhotoDetailViewController(image: image), animated: true)
}| .flatMap { owner, flatform -> AnyPublisher<Account, Never> in | ||
| return owner.fetchUserAuthUseCase.execute(platform: flatform) | ||
| .handleEvents(receiveCompletion: { completion in | ||
| if case .failure(let error) = completion { | ||
| WableLogger.log("로그인 중 오류 발생: \(error)", for: .error) | ||
| } | ||
| }) | ||
| .catch { error -> AnyPublisher<Account, Never> in | ||
| return Empty<Account, Never>().eraseToAnyPublisher() | ||
| } | ||
| .eraseToAnyPublisher() | ||
| } |
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
Improve error handling for login failures.
The current implementation silently fails by returning an empty publisher when an error occurs. This could lead to a confusing user experience where login attempts fail without feedback.
Consider propagating login errors to the UI:
.flatMap { owner, flatform -> AnyPublisher<Account, Never> in
return owner.fetchUserAuthUseCase.execute(platform: flatform)
.handleEvents(receiveCompletion: { completion in
if case .failure(let error) = completion {
WableLogger.log("로그인 중 오류 발생: \(error)", for: .error)
+ owner.loginErrorSubject.send(error)
}
})
.catch { error -> AnyPublisher<Account, Never> in
- return Empty<Account, Never>().eraseToAnyPublisher()
+ return Empty<Account, Never>()
+ .handleEvents(receiveSubscription: { _ in
+ owner.loginErrorSubject.send(error)
+ })
+ .eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}📝 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.
| .flatMap { owner, flatform -> AnyPublisher<Account, Never> in | |
| return owner.fetchUserAuthUseCase.execute(platform: flatform) | |
| .handleEvents(receiveCompletion: { completion in | |
| if case .failure(let error) = completion { | |
| WableLogger.log("로그인 중 오류 발생: \(error)", for: .error) | |
| } | |
| }) | |
| .catch { error -> AnyPublisher<Account, Never> in | |
| return Empty<Account, Never>().eraseToAnyPublisher() | |
| } | |
| .eraseToAnyPublisher() | |
| } | |
| .flatMap { owner, flatform -> AnyPublisher<Account, Never> in | |
| return owner.fetchUserAuthUseCase.execute(platform: flatform) | |
| .handleEvents(receiveCompletion: { completion in | |
| if case .failure(let error) = completion { | |
| WableLogger.log("로그인 중 오류 발생: \(error)", for: .error) | |
| owner.loginErrorSubject.send(error) | |
| } | |
| }) | |
| .catch { error -> AnyPublisher<Account, Never> in | |
| return Empty<Account, Never>() | |
| .handleEvents(receiveSubscription: { _ in | |
| owner.loginErrorSubject.send(error) | |
| }) | |
| .eraseToAnyPublisher() | |
| } | |
| .eraseToAnyPublisher() | |
| } |
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 56-56: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
| self?.updateUserSessionUseCase.updateUserSession( | ||
| session: UserSession( | ||
| id: account.user.id, | ||
| nickname: account.user.nickname, | ||
| profileURL: account.user.profileURL, | ||
| isPushAlarmAllowed: account.isPushAlarmAllowed ?? false, | ||
| isAdmin: account.isAdmin, | ||
| isAutoLoginEnabled: true, | ||
| // TODO: FCM 구현 이후 바꿔줘야 함 | ||
| notificationBadgeCount: 0 | ||
| ) | ||
| ) | ||
| .sink(receiveCompletion: { _ in | ||
| }, receiveValue: { _ in | ||
| WableLogger.log("세션 저장 완료", for: .debug) | ||
| }) | ||
| .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.
🛠️ Refactor suggestion
Handle potential memory retention issue in nested closures.
The nested closures with capture of self could lead to memory retention issues. The outer closure uses [weak self] but the inner closure doesn't have any memory management.
Also, there's a TODO comment that needs to be addressed:
.sink(
receiveCompletion: { _ in
WableLogger.log("로그인 작업 완료", for: .debug)
},
receiveValue: { [weak self] account in
self?.updateUserSessionUseCase.updateUserSession(
session: UserSession(
id: account.user.id,
nickname: account.user.nickname,
profileURL: account.user.profileURL,
isPushAlarmAllowed: account.isPushAlarmAllowed ?? false,
isAdmin: account.isAdmin,
isAutoLoginEnabled: true,
// TODO: FCM 구현 이후 바꿔줘야 함
notificationBadgeCount: 0
)
)
- .sink(receiveCompletion: { _ in
- }, receiveValue: { _ in
+ .sink(receiveCompletion: { [weak self] _ in
+ // Handle completion if needed
+ }, receiveValue: { [weak self] _ in
WableLogger.log("세션 저장 완료", for: .debug)
})
.store(in: cancelBag)
self?.loginSuccessSubject.send(account)
}
)[static_analysis_hints]
📝 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.
| self?.updateUserSessionUseCase.updateUserSession( | |
| session: UserSession( | |
| id: account.user.id, | |
| nickname: account.user.nickname, | |
| profileURL: account.user.profileURL, | |
| isPushAlarmAllowed: account.isPushAlarmAllowed ?? false, | |
| isAdmin: account.isAdmin, | |
| isAutoLoginEnabled: true, | |
| // TODO: FCM 구현 이후 바꿔줘야 함 | |
| notificationBadgeCount: 0 | |
| ) | |
| ) | |
| .sink(receiveCompletion: { _ in | |
| }, receiveValue: { _ in | |
| WableLogger.log("세션 저장 완료", for: .debug) | |
| }) | |
| .store(in: cancelBag) | |
| .sink( | |
| receiveCompletion: { _ in | |
| WableLogger.log("로그인 작업 완료", for: .debug) | |
| }, | |
| receiveValue: { [weak self] account in | |
| self?.updateUserSessionUseCase.updateUserSession( | |
| session: UserSession( | |
| id: account.user.id, | |
| nickname: account.user.nickname, | |
| profileURL: account.user.profileURL, | |
| isPushAlarmAllowed: account.isPushAlarmAllowed ?? false, | |
| isAdmin: account.isAdmin, | |
| isAutoLoginEnabled: true, | |
| // TODO: FCM 구현 이후 바꿔줘야 함 | |
| notificationBadgeCount: 0 | |
| ) | |
| ) | |
| .sink( | |
| receiveCompletion: { [weak self] _ in | |
| // Handle completion if needed | |
| }, | |
| receiveValue: { [weak self] _ in | |
| WableLogger.log("세션 저장 완료", for: .debug) | |
| } | |
| ) | |
| .store(in: cancelBag) | |
| self?.loginSuccessSubject.send(account) | |
| } | |
| ) |
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 74-74: TODOs should be resolved (FCM 구현 이후 바꿔줘야 함)
(todo)
| loadTrigger | ||
| .handleEvents(receiveOutput: { _ in | ||
| isLoadingSubject.send(true) | ||
| isLastViewSubject.send(false) | ||
| }) | ||
| .withUnretained(self) | ||
| .flatMap { owner, _ -> AnyPublisher<[Content], Never> in | ||
| return owner.fetchContentListUseCase.execute(cursor: -1) | ||
| .replaceError(with: []) | ||
| .eraseToAnyPublisher() | ||
| } | ||
| .handleEvents(receiveOutput: { contents in | ||
| isLoadingSubject.send(false) | ||
| isLastViewSubject.send(contents.isEmpty || contents.count < Constant.defaultContentCountPerPage) | ||
| }) | ||
| .sink { contentsSubject.send($0) } | ||
| .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.
🛠️ Refactor suggestion
Improve error handling for content fetching.
The current implementation silently replaces errors with an empty array, which may lead to confusing user experiences when content fails to load without explanation.
Consider adding error handling that communicates failures to the user:
.flatMap { owner, _ -> AnyPublisher<[Content], Never> in
return owner.fetchContentListUseCase.execute(cursor: -1)
+ .handleEvents(receiveCompletion: { completion in
+ if case .failure(let error) = completion {
+ // Log error and potentially notify user via a dedicated error subject
+ WableLogger.log("Content fetching error: \(error)", for: .error)
+ }
+ })
.replaceError(with: [])
.eraseToAnyPublisher()
}📝 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.
| loadTrigger | |
| .handleEvents(receiveOutput: { _ in | |
| isLoadingSubject.send(true) | |
| isLastViewSubject.send(false) | |
| }) | |
| .withUnretained(self) | |
| .flatMap { owner, _ -> AnyPublisher<[Content], Never> in | |
| return owner.fetchContentListUseCase.execute(cursor: -1) | |
| .replaceError(with: []) | |
| .eraseToAnyPublisher() | |
| } | |
| .handleEvents(receiveOutput: { contents in | |
| isLoadingSubject.send(false) | |
| isLastViewSubject.send(contents.isEmpty || contents.count < Constant.defaultContentCountPerPage) | |
| }) | |
| .sink { contentsSubject.send($0) } | |
| .store(in: cancelBag) | |
| loadTrigger | |
| .handleEvents(receiveOutput: { _ in | |
| isLoadingSubject.send(true) | |
| isLastViewSubject.send(false) | |
| }) | |
| .withUnretained(self) | |
| .flatMap { owner, _ -> AnyPublisher<[Content], Never> in | |
| return owner.fetchContentListUseCase.execute(cursor: -1) | |
| .handleEvents(receiveCompletion: { completion in | |
| if case .failure(let error) = completion { | |
| // Log error and potentially notify user via a dedicated error subject | |
| WableLogger.log("Content fetching error: \(error)", for: .error) | |
| } | |
| }) | |
| .replaceError(with: []) | |
| .eraseToAnyPublisher() | |
| } | |
| .handleEvents(receiveOutput: { contents in | |
| isLoadingSubject.send(false) | |
| isLastViewSubject.send(contents.isEmpty || contents.count < Constant.defaultContentCountPerPage) | |
| }) | |
| .sink { contentsSubject.send($0) } | |
| .store(in: cancelBag) |
| input.didCreateTappedItem | ||
| .withUnretained(self) | ||
| .flatMap { owner, text -> AnyPublisher<Void, Never> in | ||
| return owner.createCommentUseCase.execute( | ||
| contentID: owner.contentID, | ||
| text: text, | ||
| parentID: replyParentSubject.value.0, | ||
| parentMemberID: replyParentSubject.value.1 | ||
| ) | ||
| .asDriver(onErrorJustReturn: ()) | ||
| } | ||
| .sink(receiveValue: { _ in }) | ||
| .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.
🛠️ Refactor suggestion
Add validation and feedback for comment creation.
The comment creation flow doesn't validate the text input or provide feedback on success or failure.
Consider enhancing the comment creation flow:
input.didCreateTappedItem
.withUnretained(self)
+ .filter { _, text in !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
.flatMap { owner, text -> AnyPublisher<Void, Never> in
return owner.createCommentUseCase.execute(
contentID: owner.contentID,
text: text,
parentID: replyParentSubject.value.0,
parentMemberID: replyParentSubject.value.1
)
+ .handleEvents(receiveCompletion: { completion in
+ if case .failure(let error) = completion {
+ // Log error and potentially show feedback to the user
+ WableLogger.log("Comment creation error: \(error)", for: .error)
+ }
+ })
.asDriver(onErrorJustReturn: ())
}
- .sink(receiveValue: { _ in })
+ .sink(receiveValue: { _ in
+ // Reset comment input or show success indicator
+ commentTypeSubject.send(.ripple)
+ WableLogger.log("Comment created successfully", for: .debug)
+ })
.store(in: cancelBag)📝 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.
| input.didCreateTappedItem | |
| .withUnretained(self) | |
| .flatMap { owner, text -> AnyPublisher<Void, Never> in | |
| return owner.createCommentUseCase.execute( | |
| contentID: owner.contentID, | |
| text: text, | |
| parentID: replyParentSubject.value.0, | |
| parentMemberID: replyParentSubject.value.1 | |
| ) | |
| .asDriver(onErrorJustReturn: ()) | |
| } | |
| .sink(receiveValue: { _ in }) | |
| .store(in: cancelBag) | |
| input.didCreateTappedItem | |
| .withUnretained(self) | |
| .filter { _, text in !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } | |
| .flatMap { owner, text -> AnyPublisher<Void, Never> in | |
| return owner.createCommentUseCase.execute( | |
| contentID: owner.contentID, | |
| text: text, | |
| parentID: replyParentSubject.value.0, | |
| parentMemberID: replyParentSubject.value.1 | |
| ) | |
| .handleEvents(receiveCompletion: { completion in | |
| if case .failure(let error) = completion { | |
| // Log error and potentially show feedback to the user | |
| WableLogger.log("Comment creation error: \(error)", for: .error) | |
| } | |
| }) | |
| .asDriver(onErrorJustReturn: ()) | |
| } | |
| .sink(receiveValue: { _ in | |
| // Reset comment input or show success indicator | |
| commentTypeSubject.send(.ripple) | |
| WableLogger.log("Comment created successfully", for: .debug) | |
| }) | |
| .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.
Actionable comments posted: 0
🧹 Nitpick comments (5)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (5)
94-94: Check the impact of always scrolling to top inviewWillAppear.Forcing the scroll to top whenever the view appears could frustrate users returning from a detail screen if they want to continue where they left off. Consider conditionally calling
scrollToTop()or providing an alternative approach that preserves scroll position when returning to this screen.
116-118: Prevent multiple triggers of infinite scroll threshold.When the user hovers near the bottom multiple times,
willDisplayLastItemSubjectmight emit repeatedly. Consider using a guard or an isLoading state to ensure consecutive triggers don’t overlap or cause repeated fetch calls.
248-253: RedundantreloadData()after snapshot application.Using a diffable data source typically manages UI updates without requiring
collectionView.reloadData(). Consider removing this call unless there’s a specific reason to force a full reload, as it may reduce performance benefits of the diffable data source.
263-266: Avoid fixed-time dismissal for loading screen.Dismissing the loading screen after a fixed 2-second delay might not reflect actual data load completion. Consider tying dismissal to load success events for a more accurate and responsive user experience.
289-300: Refine estimated cell height to reduce layout shifts.Using
.estimated(500)can cause abrupt re-layout if actual cell size differs significantly. For a more polished UI, consider using self-sizing cells or a more realistic estimate to minimize flicker during scrolling.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
Wable-iOS/Presentation/Home/View/HomeViewController.swift(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (4)
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (6)
viewDidLoad(103-111)setupView(117-120)setupConstraint(122-148)setupDataSource(150-194)setupAction(196-205)setupBinding(207-249)Wable-iOS/Presentation/Helper/Extension/UIView+.swift (1)
addSubviews(32-36)Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (1)
transform(40-123)Wable-iOS/Presentation/Home/ViewModel/WritePostViewModel.swift (1)
transform(30-65)
🔇 Additional comments (2)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (2)
36-36: Confirm necessity of defaulttrueforshouldShowLoadingScreen.Displaying the loading screen on every
viewWillAppearmay disrupt the user experience if it's not strictly necessary each time. Please confirm whether this behavior is intentional or if you should display it only on initial load or upon certain conditions.
160-160: Verify assumption that all posts are.mine.Here, every cell is labeled with
postType: .mine, which might incorrectly reflect ownership if the user isn't the post’s author. Please verify if all displayed posts genuinely belong to the current user or if dynamic logic is needed.
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: 3
🔭 Outside diff range comments (1)
Wable-iOS/Presentation/WableComponent/Button/CommentButton.swift (1)
49-49: 🛠️ Refactor suggestionUpdate method documentation to match implementation.
The method signature
configureButton(commentCount: Int = 0)doesn't include atypeparameter, but the documentation and examples on lines 18-20 show usage with atypeparameter. This mismatch could confuse developers using this component.Update either the method signature to include the type parameter (if it's meant to be configurable after initialization) or update the documentation to reflect the current implementation where type is set only at initialization.
🧹 Nitpick comments (7)
Wable-iOS/Presentation/WableComponent/Button/CommentButton.swift (1)
59-63: Consider centralizing constraint logic.Constraints for the
.contentcase have been added directly into the configuration method, while similar constraint logic for the.commentcase exists later in the method. This creates potential inconsistency in how constraints are managed.Consider creating a private helper method to handle constraints for both cases to improve maintainability:
private func setupConstraints(for type: PostType) { switch type { case .content: snp.makeConstraints { $0.adjustedWidthEqualTo(45) $0.adjustedHeightEqualTo(24) } case .comment: snp.makeConstraints { $0.adjustedWidthEqualTo(66) $0.adjustedHeightEqualTo(20) } } }Then call this method from your
configureButtonmethod.Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (2)
25-46: Reduce magic numbers for replyParentSubject's default.
Reply parent IDs are set to (-1, -1) by default, which could be unclear to new developers. Consider introducing a descriptive constant or an enum to clarify the meaning of these values.- private let replyParentSubject = CurrentValueSubject<(Int, Int), Never>((-1, -1)) + private let replyParentSubject = CurrentValueSubject<(Int, Int), Never>((ReplyParentConstants.invalidCommentID, ReplyParentConstants.invalidAuthorID))
118-128: Consider user notification on like/unlike failures.
Both create and delete content “like” operations usereplaceError(onErrorJustReturn: ()). This suppresses error states entirely. Users might be unaware if their like/unlike actions fail.Also applies to: 120-127
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (4)
41-41: Remove or utilize the unused subject.
didCommentHeartTappedSubjectis declared but never used or passed to the view model, suggesting dead code or a missing implementation.- private let didCommentHeartTappedSubject = PassthroughSubject<Bool, Never>()
173-197: Retain ephemeral Combine subscriptions for user info fetching.
The.sink { … } .cancel()pattern fetches user info once per cell, but might become problematic if repeated. If the user info fetch is large or slow, consider retaining the subscription via yourcancelBagor merge it into the main flow in the view model.- .sink { id in - ... - }.cancel() + .sink { id in + ... + } + .store(in: self.cancelBag)
219-224: Validate or trim comment text before sending.
Sending empty or whitespace-only strings todidCreateTappedSubjectmight clutter the UI or the back-end. Trim and validate the text before sending.createCommentButton.addAction(UIAction(handler: { _ in guard let text = self.commentTextView.text?.trimmingCharacters(in: .whitespacesAndNewlines), - !text.isEmpty else { return } + !text.isEmpty else { + // Maybe show a toast or an alert for empty comments + return + } self.didCreateTappedSubject.send(text) }), for: .touchUpInside)
239-249: Revise or remove commented-out code.
The unusedtextViewStatesubscription block is commented out. If you plan to implement these state transitions, complete it; otherwise, removing it can help keep the code clean.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
Wable-iOS/Infra/Network/TargetType/CommentTargetType.swift(1 hunks)Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift(1 hunks)Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift(1 hunks)Wable-iOS/Presentation/WableComponent/Button/CommentButton.swift(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- Wable-iOS/Infra/Network/TargetType/CommentTargetType.swift
🧰 Additional context used
🧬 Code Graph Analysis (2)
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (5)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (4)
collectionView(101-103)collectionView(105-119)viewWillAppear(87-95)setupBinding(184-241)Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift (1)
fetchActiveUserID(46-48)Wable-iOS/Domain/UseCase/Home/FetchUserInformationUseCase.swift (1)
fetchActiveUserID(23-26)Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (1)
transform(69-212)Wable-iOS/Presentation/WableComponent/View/ToastView.swift (1)
show(144-160)
Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (2)
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (1)
viewWillAppear(111-115)Wable-iOS/Presentation/Home/View/HomeViewController.swift (1)
viewWillAppear(87-95)
🪛 SwiftLint (0.57.0)
Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift
[Warning] 134-134: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
[Warning] 141-141: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
🔇 Additional comments (2)
Wable-iOS/Presentation/WableComponent/Button/CommentButton.swift (1)
49-82: Potential performance issue with constraint setup.Setting constraints in the configuration method could lead to constraints being repeatedly applied if
configureButtonis called multiple times (e.g., when updating the comment count).Verify that this method isn't called repeatedly in your app flow, or consider adding a guard to prevent re-applying constraints:
// At the beginning of configureButton private var constraintsApplied = false func configureButton(commentCount: Int = 0) { // Configure appearance... if !constraintsApplied { // Apply constraints based on type constraintsApplied = true } }Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (1)
133-136: Confirm SwiftLint’s unused parameter warning.
Static analysis flagged possibly unused closure parameters around lines 134 & 141. Here, the second parameter in line 134 is_(correct usage), but in line 141 theindexis used. This indicates a false positive for line 141. You can safely ignore or disable that specific warning for this closure.Also applies to: 141-148
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 134-134: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
| .flatMap({ owner, _ -> AnyPublisher<(ContentInfo?, [ContentComment]), Never> in | ||
| let contentPublisher = owner.fetchContentInfoUseCase.execute( | ||
| contentID: owner.contentID, | ||
| title: owner.contentTitle | ||
| ) | ||
| .map { contentInfo -> ContentInfo? in | ||
| return contentInfo | ||
| } | ||
| .replaceError(with: nil) | ||
|
|
||
| let commentsPublisher = owner.fetchContentCommentListUseCase.execute( | ||
| contentID: owner.contentID, | ||
| cursor: Constant.initialCursor | ||
| ) | ||
| .replaceError(with: []) | ||
| .eraseToAnyPublisher() |
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
Handle potential errors instead of silently discarding them.
In many cases (e.g., line 96 and 102), the errors are replaced with safe default values (nil or []). This approach prevents user feedback or logging when something goes wrong. Consider logging or propagating errors to the UI to inform the user.
| input.didReplyTappedItem | ||
| .withUnretained(self) | ||
| .sink { owner, index in | ||
| replyParentSubject.send(( | ||
| commentsSubject.value[index].comment.id, | ||
| commentsSubject.value[index].comment.author.id | ||
| )) | ||
| commentTypeSubject.send(.reply) | ||
| } | ||
| .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.
Validate the tapped index for comments.
When replying to a comment, an invalid or out-of-range index might cause a runtime error if the view is out of sync with the data. Safeguard against index-related crashes by checking array bounds before accessing commentsSubject.value[index].
+ guard index >= 0, index < commentsSubject.value.count else { return }📝 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.
| input.didReplyTappedItem | |
| .withUnretained(self) | |
| .sink { owner, index in | |
| replyParentSubject.send(( | |
| commentsSubject.value[index].comment.id, | |
| commentsSubject.value[index].comment.author.id | |
| )) | |
| commentTypeSubject.send(.reply) | |
| } | |
| .store(in: cancelBag) | |
| input.didReplyTappedItem | |
| .withUnretained(self) | |
| .sink { owner, index in | |
| guard index >= 0, index < commentsSubject.value.count else { return } | |
| replyParentSubject.send(( | |
| commentsSubject.value[index].comment.id, | |
| commentsSubject.value[index].comment.author.id | |
| )) | |
| commentTypeSubject.send(.reply) | |
| } | |
| .store(in: cancelBag) |
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 141-141: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
| // TODO: ViewModel로 옮겨야 할 로직 | ||
|
|
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
Move the user fetch logic into the view model.
The TODO comment notes that fetching active user info belongs in the view model. Keeping it in the controller can violate MVVM principles, as the view model should handle data retrieval.
Do you want help creating a new use case or adjusting the existing one to manage the user information logic in the view model?
JinUng41
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.
고생하셨습니다~
많은 내용에 보다가 숨이 넘어갈 뻔 했어요!
하지만 결국 다 봤으니 다행입니다.
사실 구현하기 쉬운 내용은 아니라고 생각이 드는데, 시간도 없다보니 다소 코드가 복잡해진 것 같습니다.
유진님도 이 점을 잘 아시고, PR에 적어주신 것 같아요.
코멘트는 남겨두었으나, 시간 관계상 넘어가고 추후에 반영해도 괜찮습니다.
화이팅하세요~
| func updateUserSession(session: UserSession) -> AnyPublisher<Void, Never> { | ||
| return Just(repository.updateUserSession(session)) | ||
| .eraseToAnyPublisher() | ||
| } |
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.
타입 메서드 .just()도 가능하답니다~
| override func viewWillAppear(_ animated: Bool) { | ||
| super.viewWillAppear(animated) | ||
|
|
||
| willAppearSubject.send() | ||
| } | ||
|
|
||
| override func viewDidLoad() { | ||
| super.viewDidLoad() | ||
|
|
||
| setupView() | ||
| setupConstraint() | ||
| setupDataSource() | ||
| setupAction() | ||
| setupBinding() | ||
| } |
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 commentCellRegistration = UICollectionView.CellRegistration<CommentCollectionViewCell, ContentComment> { cell, indexPath, item in | ||
|
|
||
| self.userInformationUseCase.fetchActiveUserID() |
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.
지금은 순환 참조의 여지가 보이네요.
약한 참조 혹은 미소유 참조를 하는 것이 좋아 보여요.
| cell.configureCell( | ||
| info: item.comment, | ||
| commentType: item.parentID == -1 ? .ripple : .reply, | ||
| postType: item.comment.author.id == id ? .mine : .others, | ||
| likeButtonTapHandler: { | ||
| self.didCommentHeartTappedSubject.send(cell.likeButton.isLiked) | ||
| }, | ||
| replyButtonTapHandler: { | ||
| self.didReplyTappedSubject.send(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.
셀을 바인딩 하는 과정에서 유즈케이스를 실행 시켜야 하는 것이 신기하네요.
이유가 있을까요?
| createCommentButton.addAction(UIAction(handler: { _ in | ||
| guard let text = self.commentTextView.text else { return } |
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.
이 부분도 순환 참조의 여지가 보여요!
| var defaultImageList = [ | ||
| DefaultProfileType.blue, | ||
| DefaultProfileType.green, | ||
| DefaultProfileType.purple | ||
| ] |
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.
고정적인 내용인 것으로 알고 있는데, 배열이 변경될 여지가 있는지 궁금합니다.
그리고 DefaultProfileType이 CaseIterable을 따르게 한다면, DefaultProfileType.allCases를 사용할 수 있어 더욱 간편할 것 같네요.
| .sink { owner, userSession in | ||
| guard let userSession = userSession else { return } | ||
|
|
||
| tabBarController.present(noticeViewController, animated: true) | ||
| owner.profileUseCase.execute( | ||
| profile: UserProfile( | ||
| user: User( | ||
| id: userSession.id, | ||
| nickname: owner.nickname, | ||
| profileURL: userSession.profileURL, | ||
| fanTeam: LCKTeam(rawValue: owner.lckTeam) | ||
| ), | ||
| introduction: "", | ||
| ghostCount: 0, | ||
| lckYears: owner.lckYear, | ||
| userLevel: 1 | ||
| ), | ||
| isPushAlarmAllowed: false, | ||
| isAlarmAllowed: owner.rootView.marketingAgreementItemView.checkButton.isSelected, | ||
| image: owner.profileImage, | ||
| defaultProfileType: owner.defaultImage | ||
| ) | ||
| .receive(on: DispatchQueue.main) | ||
| .sink { _ in | ||
| } receiveValue: { [weak self] _ in | ||
| guard let cancelBag = self?.cancelBag else { return } | ||
|
|
||
| self?.userInformationUseCase.updateUserSession( | ||
| session: UserSession( | ||
| id: userSession.id, | ||
| nickname: userSession.nickname, | ||
| profileURL: userSession.profileURL, | ||
| isPushAlarmAllowed: userSession.isPushAlarmAllowed, | ||
| isAdmin: userSession.isAdmin, | ||
| isAutoLoginEnabled: true, | ||
| // TODO: FCM 구현 이후 바꿔줘야 함 | ||
| notificationBadgeCount: 0 | ||
| ) | ||
| ) | ||
| .sink(receiveCompletion: { _ in | ||
| }, receiveValue: { _ in | ||
| WableLogger.log("세션 저장 완료", for: .debug) | ||
|
|
||
| guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, | ||
| let loginViewController = windowScene.windows.first?.rootViewController | ||
| else { | ||
| return | ||
| } | ||
|
|
||
| let tabBarController = TabBarController(shouldShowLoadingScreen: false) | ||
|
|
||
| self?.dismiss(animated: false) { | ||
| guard let nickname = self?.nickname else { return } | ||
|
|
||
| loginViewController.present(tabBarController, animated: true) { | ||
| let noticeViewController = WableSheetViewController( | ||
| title: "와블과 함께해 주셔서 감사합니다!", | ||
| message: "\(nickname)님\n와블의 일원이 되신 것을 환영해요.\nLCK 함께 보며 같이 즐겨요 :)" | ||
| ) | ||
|
|
||
| noticeViewController.addAction(.init(title: "와블 즐기러 가기", style: .primary)) | ||
|
|
||
| tabBarController.present(noticeViewController, animated: true) | ||
| } | ||
| } | ||
| }) | ||
| .store(in: cancelBag) | ||
| } | ||
| .store(in: owner.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.
이 부분은 확실히 수정의 여지가 있어보이네요.
sink 안에 sink 안에 sink라니,,
| useCase.execute(nickname: text) | ||
| .receive(on: DispatchQueue.main) | ||
| .sink(receiveCompletion: { [weak self] completion in | ||
| let condition = completion == .finished | ||
|
|
||
| self?.rootView.conditiionLabel.text = condition ? "사용 가능한 닉네임입니다." : "이미 사용 중인 닉네임입니다." | ||
| self?.rootView.conditiionLabel.textColor = condition ? .success : .error | ||
| self?.rootView.nextButton.isUserInteractionEnabled = condition | ||
| self?.rootView.nextButton.updateStyle(condition ? .primary : .gray) | ||
| }, receiveValue: { _ in | ||
| }) | ||
| .store(in: cancelBag) |
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.
버튼이 눌렸을 때 유즈케이스를 실행하는 점은 알겠으나, 버튼이 눌릴 때마다 새로운 구독 관계가 형성되어 cancelBag에 저장되기에 메모리 누수가 발생할 것 같네요.
completion으로 UI를 반영하는 것이 맞는지도 의문입니다.
| if let navigationController = viewController as? UINavigationController { | ||
| if navigationController.viewControllers.first is HomeViewController { | ||
| if tabBarController.selectedIndex == 0 { | ||
| homeViewController.scrollToTop() | ||
| } | ||
| } | ||
| } | ||
| } |
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.
아래와 같이 작성하면, 코드 뎁스도 줄이고 간결하게 표현할 수 있겠죠?
guard tabBarController.selectedIndex == 0,
let navigationController = viewController as? UINavigationController,
navigationController.viewControllers.first is HomeViewController
else {
return
}
homeViewController.scrollToTop()| /// 주어진 `Pretendard` 스타일을 `UITextView`에 적용합니다. | ||
| /// | ||
| /// - Parameter style: 적용할 `UIFont.Pretendard` 스타일 | ||
| /// - Parameter text: 설정할 텍스트 (기본값: 빈 문자열) | ||
| /// | ||
| /// 사용 예시: | ||
| /// ```swift | ||
| /// let textView = UITextView() | ||
| /// textView.setPretendard(with: .body4, text: "내용이 들어갑니다") | ||
| /// ``` | ||
| func setPretendard(with style: UIFont.Pretendard, text: String = " ") { | ||
| let paragraphStyle = NSMutableParagraphStyle() | ||
| paragraphStyle.lineSpacing = (style.lineHeight - style.size) / 1.6 | ||
|
|
||
| let attributes: [NSAttributedString.Key: Any] = [ | ||
| .font: UIFont.pretendard(style), | ||
| .kern: style.kerning, | ||
| .paragraphStyle: paragraphStyle | ||
| ] | ||
|
|
||
| self.attributedText = NSAttributedString(string: text, attributes: attributes) | ||
| } |
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.
Actionable comments posted: 1
♻️ Duplicate comments (6)
Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (6)
70-118: Consider providing user feedback or logging when errors occur.At lines 97 and 103, the errors are replaced with
nilor empty arrays, effectively discarding the errors. This repeats a previous comment regarding silent error handling. Users might be unaware that loading has partially failed.
119-132: Silent error handling on content like/unlike operations.Here, errors are transformed into empty outputs (
.asDriver(onErrorJustReturn: ())) on lines 123 and 126, and no error feedback is given to the user. Consider at least logging or indicating an error state.
133-146: Silent error handling on comment like/unlike operations.Similar to the above, lines 137 and 140 discard errors. Providing user feedback or logs would help in diagnosing potential issues.
154-163: Validate the reply index to avoid out-of-range access.As previously noted, indexing
commentsSubject.value[index]at lines 158 and 159 can cause runtime errors if the data is out of sync with the UI.🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 156-156: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
165-189: Add validation and feedback for comment creation.It might help to validate the text (e.g., check for empty or whitespace-only strings) and provide user feedback on failures. This recurs from a past suggestion.
190-218: Log or handle errors when fetching additional comments.Again,
.replaceError(with: [])discards error details. Consider logging or a user-facing message to improve diagnostics.
🧹 Nitpick comments (2)
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (2)
174-218: Data source setup is clear and well-structured.• At line 175, the closure parameters include
indexPath, which is not referenced. Consider using_if you don't need it, per SwiftLint's suggestion.-let contentCellRegistration = UICollectionView.CellRegistration<ContentCollectionViewCell, Content> { [weak self] cell, indexPath, item in +let contentCellRegistration = UICollectionView.CellRegistration<ContentCollectionViewCell, Content> { [weak self] cell, _, item in🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 175-175: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
346-386: Unused closure parameter in line 350.Inside the compositional layout closure, you do not use
layoutEnvironment. You can replace it with_to satisfy SwiftLint.-return UICollectionViewCompositionalLayout { [weak self] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in +return UICollectionViewCompositionalLayout { [weak self] (sectionIndex, _) -> NSCollectionLayoutSection? in🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 350-350: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
Wable-iOS/Infra/Network/TargetType/CommentLikedTargetType.swift(1 hunks)Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift(1 hunks)Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift(1 hunks)Wable-iOS/Presentation/WableComponent/Cell/CommentCollectionViewCell.swift(10 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- Wable-iOS/Infra/Network/TargetType/CommentLikedTargetType.swift
- Wable-iOS/Presentation/WableComponent/Cell/CommentCollectionViewCell.swift
🧰 Additional context used
🧬 Code Graph Analysis (1)
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (3)
Wable-iOS/Data/RepositoryImpl/UserSessionRepositoryImpl.swift (1)
fetchActiveUserID(46-48)Wable-iOS/Domain/UseCase/Home/FetchUserInformationUseCase.swift (1)
fetchActiveUserID(23-26)Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (1)
transform(70-227)
🪛 SwiftLint (0.57.0)
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift
[Warning] 48-48: TODOs should be resolved (ViewModel로 옮겨야 할 로직)
(todo)
[Warning] 175-175: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
[Warning] 350-350: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift
[Warning] 149-149: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
[Warning] 156-156: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
🔇 Additional comments (19)
Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (6)
1-11: No specific concerns.These header lines are purely informational.
12-46: Constructor & property initialization look clean.The approach of using private constants to store injected dependencies is straightforward, ensuring the immutability of important references.
49-69: Input/Output definitions are well-structured.These structs neatly encapsulate the reactive input signals and output streams for the view model, following clear MVVM conventions.
147-153: Event handling is straightforward.The second parameter in the sink closure is intentionally named
_, so there’s no issue with unused variables here.🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 149-149: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
219-227: Output mapping is clearly defined.No issues found here.
230-235: Use of constants is neat and organized.This private
Constantenum helps keep magic numbers grouped, promoting readability.Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (13)
1-14: No immediate concerns.This section contains routine file headers and class definitions.
15-28: Sections and items look well-defined for a diffable data source setup.Enums are clearly named and reflect domain concepts.
29-46: Property declarations are straightforward.No issues found with typealias usage or property accessibility.
61-98: UI components: creation and configuration are fine.Properties like
LottieAnimationViewandUITextVieware well-initialized. Great job ensuring corner radius, padding, and style.
101-110: Initializer is standard MVVM.Injecting the
viewModelandcancelBagis consistent with standard dependency injection.
112-127: Lifecycle methods are used appropriately.Using
viewWillAppearto send signals andviewDidLoadfor setup is consistent with best practices.
132-136: setUpView method.Basic subview hierarchy is correct. No issues found.
138-172: Constraints are well-defined.Using SnapKit provides concise and readable constraints. Keep it up.
220-229: Actions are set up properly.Creating comment and refreshing logic is straightforward and follows typical iOS patterns.
231-235: Delegate assignment is fine.No further suggestions; consistent usage of
UICollectionViewDelegate.
235-325: Binding outputs to the UI.• Great usage of Combine for reactive updates.
• The text reset and toast message upon comment success fosters a good user experience.
328-344: Delegate method for willDisplay is logically correct.Preloading the next page when approaching the end of the comments is a nice UX touch.
388-414: Update content & comments logic is sound.Diffable data source usage is correct, ensuring smooth UI updates. Scrolling to top is a nice touch after posting.
| // TODO: ViewModel로 옮겨야 할 로직 | ||
|
|
||
| private let userInformationUseCase = FetchUserInformationUseCase( | ||
| repository: UserSessionRepositoryImpl( | ||
| userDefaults: UserDefaultsStorage( | ||
| jsonEncoder: JSONEncoder(), | ||
| jsonDecoder: JSONDecoder() | ||
| ) | ||
| ) | ||
| ) | ||
|
|
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
Address the TODO by moving logic into the view model.
At line 48, SwiftLint flags a TODO. Since you’ve noted that user fetch logic ideally belongs in the view model, consider refactoring code at lines 50–58 to maintain MVVM separation of concerns.
Would you like assistance in refactoring this logic into the view model (or a dedicated use case)?
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 48-48: TODOs should be resolved (ViewModel로 옮겨야 할 로직)
(todo)
[Feat] 온보딩 기능 구현 및 메인 홈, 글 작성 구현
👻 PULL REQUEST
📄 작업 내용
구현 스크린샷
💻 주요 코드 설명
멀티파트 통신을 위한 TargetType 수정
UITextView 익스텐션을 통한 행간 구현
이외 PR 포인트
📚 참고자료
👀 기타 더 이야기해볼 점
🔗 연결된 이슈
Summary by CodeRabbit
New Features
UI Enhancements