Skip to content

Conversation

@youz2me
Copy link
Member

@youz2me youz2me commented Apr 10, 2025

👻 PULL REQUEST

📄 작업 내용

  • 온보딩 닉네임 중복 확인, 프로필 수정 기능을 구현했어요.
  • 메인 홈 화면 UI, 기능을 구현했어요.
  • 글 작성 UI, 기능을 구현했어요.
구현 스크린샷
구현 내용 온보딩 과정 메인 홈 피드 글 작성
IPhone 15 pro

💻 주요 코드 설명

  • 나머지 기능 구현을 위해 너무 자세히는 쓰지 않겠습니다..

멀티파트 통신을 위한 TargetType 수정

  • 기존 BaseTargetType에는 멀티파트 통신을 위한 변수가 설정되어 있지 않아 멀티파트 통신이 불가능했습니다.
  • BaseTargetType에 Multipart 변수를 추가해 멀티파트 세팅을 할 수 있도록 수정했습니다.
  • 모든 TargetType 클래스에도 해당 변수를 추가했습니다.
  • 멀티파트 통신 시 RequestBody로 보내는 게 아니라 Multipart 변수를 통해 세팅을 진행해야 합니다.

UITextView 익스텐션을 통한 행간 구현

  • UITextView에는 기존 익스텐션을 통해 행간을 구현할 수 없는 이슈를 발견했습니다. (사유는 모르겠습니다...)
  • 따라서 UITextView 익스텐션을 구현해 행간을 구현했습니다.

이외 PR 포인트

  • 현재 아키텍처가 제대로 구성되어 있지 않아 코드 가독성이 많이 떨어집니다. (미리 죄송합니다..............)
    • 온보딩 및 홈 화면 쪽이 특히 그런데 이 부분은 QA 이후에 따로 이슈 파서 반영할게요!
  • 작은 로직들 위주로 먼저 리뷰를 부탁드립니다! 큰 로직은 내일 모각작 or QA 이후에 같이 보면서 이야기해보면 좋을 듯 합니다. 따로 정리해보겠습니다...

📚 참고자료

  • 클로드야 정말 고마워

👀 기타 더 이야기해볼 점

  • 기능 작업하다 보니 PR 단위가 너무 커졌네요 ㅎㅎ ... 리뷰하기 힘드실텐데 죄송하다는 말씀 미리 전합니다. 앞으로는 기능 단위로 쪼개서 올리도록 하겠습니다 ㅜㅜ

🔗 연결된 이슈

Summary by CodeRabbit

  • New Features

    • Added functionality for managing likes and comments on content, including creating and deleting likes and comments.
    • Introduced new view controllers for detailed content views and post creation, enhancing user interaction.
    • Enhanced user session management during login, allowing for smoother transitions and updates.
  • UI Enhancements

    • Refined layout and design of home and detail screens for better user experience.
    • Improved image handling and text input in post creation, ensuring a more intuitive interface.

youz2me added 25 commits April 1, 2025 20:52
@youz2me youz2me added ✨ feat 기능 또는 객체 구현 🦉 유진 🛌🛌🛌🛌🛌🛌🛌🛌🛌🛌 labels Apr 10, 2025
@youz2me youz2me requested a review from JinUng41 April 10, 2025 17:27
@youz2me youz2me self-assigned this Apr 10, 2025
@coderabbitai
Copy link

coderabbitai bot commented Apr 10, 2025

Walkthrough

This 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

File(s) Change Summary
Wable-iOS.xcodeproj/... Added new file entries for many use case and view controller Swift files; reorganized groups; removed obsolete ProfileUseCase.swift; updated Package.resolved originHash.
Wable-iOS/App/SceneDelegate.swift, Wable-iOS/Data/RepositoryImpl/... Renamed parameters and updated method signatures in configureLoginScreen, updateUserProfile, and updateUserSession methods to support new session and profile logic.
Wable-iOS/Domain/Entity/..., Wable-iOS/Domain/RepositoryInterface/..., Wable-iOS/Domain/UseCase/... Updated UserSession’s profileURL type; extended repository protocols; added numerous new use case classes (e.g., Create/Delete/Fetch use cases for content and comments); refactored CreateUserProfileUseCase; removed legacy ProfileUseCase.swift.
Wable-iOS/Infra/Network/DTO/..., Wable-iOS/Infra/Network/TargetType/... Added isAlarmAllowed in update DTO; introduced a new optional multipartFormData property and adjustments in request body logic across various TargetType enums to support multipart uploads.
Wable-iOS/Presentation/Home/... Introduced new view controllers (HomeViewController, HomeDetailViewController, WritePostViewController) and view models (HomeViewModel, HomeDetailViewModel, WritePostViewModel) that integrate diffable data sources and Combine for the home feed and post creation.
Wable-iOS/Presentation/Onboarding/... Updated onboarding and profile registration flows by adding new parameters (e.g., lckYear, lckTeam, default images) and implementing asynchronous handling with Combine; refined nickname duplication checks.
Wable-iOS/Presentation/WableComponent/..., Wable-iOS/Presentation/Helper/... Modified UI components and helpers: exposed like button properties; added new closures for comment cell actions; replaced UILabel with a UITextView in content cells; enhanced text styling via an extension; adjusted constraints and styling in various UI views.
Wable-iOS/Presentation/TabBar/TabBarController.swift Introduced a loading screen flag and delegate setup; added logic to scroll the home view to the top when its tab is reselected.

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
Loading

Assessment against linked issues

Objective Addressed Explanation
[Feat] 메인 홈 피드 화면 UI 구현 (#139)

Possibly related PRs

Suggested labels

🛠️ fix

Suggested reviewers

  • JinUng41

Poem

I'm a little rabbit with a coder’s delight,
Hopping through changes in the pale moonlight.
New use cases sprout like carrots in fresh soil,
UI flows and updates now make my heart recoil.
With a twitch of my ear and a skip so spry,
I celebrate these changes as I hop by!

Tip

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

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

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

CodeRabbit Commands (Invoked using PR comments)

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

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Documentation and Community

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🔭 Outside diff range comments (1)
Wable-iOS/Infra/Network/TargetType/CommentLikedTargetType.swift (1)

25-26: ⚠️ Potential issue

Fix 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 uses contentID.

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 final keyword 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 cursor parameter 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 deleteContent method, 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 Profile directory, but it appears to be handling general content fetching rather than profile-specific functionality. Consider moving this file to a more appropriate directory (like Content or a general UseCase directory) to maintain proper organization.


21-25: Add documentation for better code clarity.

The execute method 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 Foundation
Wable-iOS/Infra/Network/DTO/Request/Profile/UpdateUserProfile.swift (1)

20-24: Consider clarifying the difference between alarm properties.

The struct now contains both isAlarmAllowed and isPushAlarmAllowed properties. 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:

  1. 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.

  2. 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 string

While 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 private

This 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 documentation

The execute method accepts a cursor parameter, 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 configurable

The 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 selection

The 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 reference

While 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 implementation

The 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.

  1. 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.
  2. 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.
Making isLiked and likeCount publicly 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 = 0
Wable-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.
Using Just(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, updateUserSession returns a Void publisher, 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 hidden fanTeamImageView and 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 the error parameter. 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 receiveCompletion parameter 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 CommentType enum 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 .sink handler:

.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 defaultImage to nil is redundant since optionals default to nil.

- // 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 renaming conditiionLabel.

The logic looks fine; however, the label property name has a potential typo (conditiionLabel). Renaming it to conditionLabel may 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 indexPath in contentCellRegistration is 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 layoutEnvironment isn’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 capturing self weakly.

SwiftLint warns about the unused indexPath parameter in this closure. Additionally, to prevent potential retain cycles, it's a good practice to capture self as [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 after dataSource?.apply(snapshot) may cause unnecessary performance overhead and UI re-instantiations. The diffable data source automatically handles incremental updates. Consider removing collectionView.reloadData():

     dataSource?.apply(snapshot)
-    collectionView.reloadData()
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 67b6df2 and 6beee59.

⛔ Files ignored due to path filters (3)
  • Wable-iOS/Resource/Assets.xcassets/Button/btn_remove_photo.imageset/btn_remove_photo.png is excluded by !**/*.png
  • Wable-iOS/Resource/Assets.xcassets/Button/btn_remove_photo.imageset/btn_remove_photo@2x.png is excluded by !**/*.png
  • Wable-iOS/Resource/Assets.xcassets/Button/btn_remove_photo.imageset/btn_remove_photo@3x.png is 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 appropriate

The change from what was likely .body3 to .body1 for 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 final to 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 injection

The implementation correctly follows the clean architecture pattern by accepting a repository dependency and having a clear separation of concerns.


21-25: Simple and effective implementation

The 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 injection

The ViewModel follows good practices by using dependency injection for the use case.


21-29: Clean implementation of ViewModelType protocol

The 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 objectives

The 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 property

This 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 isAlarmAllowed property has been added to the ProfileInfo struct. 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 isAlarmAllowed case has been properly added to the CodingKeys enum, ensuring correct JSON serialization.

Wable-iOS/Presentation/Enum/DefaultProfileType.swift (1)

15-25: Added computed property for uppercase profile type values.

The new uppercased property 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 profileURL property has been changed from String to URL?. This is a good change for two reasons:

  1. Using URL provides type safety for URL values
  2. 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 swift

Length of output: 278


Attention: Manual Verification Needed for profileURL Nil Handling

The change from a String to an optional URL for profileURL is 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 --pcre2 flag.

Next Steps:

  • Re-run Verification: Please re-run the search using the --pcre2 flag:
    rg --pcre2 "\.profileURL\b(?!\?)" --type swift
  • Manual Review: Alternatively, ensure that every access to profileURL in 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 multipartFormData property extends the ViewitTargetType to 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 .none as 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 originHash has 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 multipartFormData property 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 requestBody implementation now correctly returns the request object for the createCommentLiked case, 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 multipartFormData property to ReportTargetType maintains 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 good

The addition of the multipartFormData property to CommentTargetType follows a consistent pattern being implemented across multiple target types. Returning .none is 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 good

The addition of the multipartFormData property to InformationTargetType is 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 .none is the correct approach.

Wable-iOS/Infra/Network/TargetType/LoginTargetType.swift (1)

20-22: Implementation of multipartFormData looks good

The addition of the multipartFormData property to LoginTargetType maintains consistency with other target types. Returning .none is 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 good

The addition of the multipartFormData property to CommunityTargetType follows the consistent pattern implemented across all target types. Returning .none is 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 multipartFormData property 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 multipartFormData property 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 DeleteCommentUseCase class 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 CreateContentUseCase class 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 execute method properly delegates to the repository layer while maintaining the appropriate return type AnyPublisher<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 setPretendard method 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 DeleteCommentLikedUseCase class 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 execute method properly delegates to the repository while maintaining the appropriate return type AnyPublisher<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 DeleteContentLikedUseCase class 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 execute method properly delegates to the repository's deleteContentLiked method while maintaining an appropriate return type with AnyPublisher<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 API

The removal of the redundant userID parameter from updateUserSession is a good change since the UserSession object 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 pattern

The 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 method

The 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 pattern

The 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 support

Adding 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 case

The 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 implementation

The 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 logic

The 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 naming

The changes improve the code in two ways:

  1. Renamed useCase to fetchUserAuthUseCase for better clarity about its purpose
  2. Added updateUserSessionUseCase parameter to handle user session updates, which enhances separation of concerns

This 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 architecture

The 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 safety

The change from using profileURL?.absoluteString ?? "" to directly using profileURL suggests the UserSession struct now accepts an optional URL rather than a String. This is a good improvement as it:

  1. Maintains the URL type throughout the system
  2. Avoids unnecessary string conversions
  3. Preserves the nullability of the field

48-49: Simplified user session update

The removal of the second parameter (forUserID) simplifies the API, suggesting the UserSession object 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 parameter

The 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 options

The 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 isAlarmAllowed and isPushAlarmAllowed parameters, 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 injection

The 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 parameter

The 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 5

Length of output: 60


Action Required: The updated method signature in CreateUserProfileUseCase.swift now fully aligns with the repository interface and correctly uses default values for the optional parameters. However, our automated search for call sites invoking execute( (and specifically passing the isAlarmAllowed parameter) 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 isAlarmAllowed parameter.
    • 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 encapsulation

The refactoring of updateUserSession to accept only the UserSession parameter 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 support

Adding multipart form data support to the BaseTargetType protocol enhances its flexibility for handling file uploads and other complex data structures.


38-40: Correct task handling for multipart form data

This 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 requests

The 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 pattern

The addition of the lckYear and lckTeam properties 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 controllers

The implementation correctly passes the lckYear and lckTeam data to the ProfileRegisterViewController, maintaining data flow through the onboarding process.

Also applies to: 83-89


104-105: Team selection handling is properly implemented

The code correctly updates the lckTeam property 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 requestBody

Since 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, and defaultProfileType) 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 .none for other cases is valid.
The fallback behavior of returning nil for multipartFormData in non-updateUserProfile cases is logically consistent and helps avoid unintended uploads. No further changes needed.


45-54:

Details

✅ Verification successful

Properly handle image data.
Appending file data 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.swift properly appends multipart form data when request.file is 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 with nickname, lckTeam, lckYear, profileImage, and defaultImage ensures 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.
Introducing shouldShowLoadingScreen is clear and maintains custom initialization logic for the tab bar.


19-25: Lazy initialization of HomeViewController with loading screen property.
Passing shouldShowLoadingScreen explicitly 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.
Defining init(shouldShowLoadingScreen: Bool = true) is a neat pattern—no concerns here.


71-71: Delegate setup call added to viewDidLoad.
Invoking setupDelegate() ensures the tab bar controller is its own delegate—straightforward.


102-104: Explicit delegate assignment.
setupDelegate() sets delegate = self to 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 for configureView is 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 of PassthroughRelay and CancelBag is clear. This structure nicely keeps track of subscriptions.


23-74: UI components initialization.
Defining scroll view, text views, buttons, and image view with .then is concise. Ensure placeholders and styling remain consistent across different screen sizes.


76-86: Custom initializer for WritePostViewController.
Passing WritePostViewModel injects 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 in shouldChangeTextIn is 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 likeButtonTapHandler closure 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 = true allows 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 configureCell method 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 updateUserSessionUseCase property 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 lckYear and lckTeam ensures proper initialization and data passing in the onboarding flow.


36-38: Standard init?(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 that rootView.defaultImageList is 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 AgreementViewController with all required values is clear. No issues found with the data passing logic.


186-186: Simple image selection toggle.

Nullifying defaultImage when 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 CommentType with ripple and reply clarifies comment categorization.


41-42: Closure-based handlers.

Exposing tap handlers for like/reply encourages separation of concerns and flexible usage.


57-58: Lazy initialization for likeButton and replyButton.

This approach is fine, ensuring buttons are ready after the cell is instantiated.


88-88: Adding replyButton to subviews.

No concerns; it extends the cell functionality properly.


115-119: Reply button constraint setup looks correct.

Ensuring it aligns horizontally with likeButton is consistent with the UI layout.


155-157: Reply button tap handling is straightforward.

Invoking the replyButtonTapHandler closure 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: Extended configureCell signature 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 configuring likeButton with .comment type 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 DataSource and Snapshot clarifies diffable data source usage.


34-45: ViewModel and subject setup is logical.

All required Combine subjects and the dataSource property are conveniently declared.


48-55: Fetching user info.

Instantiating FetchUserInformationUseCase with 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 HomeDetailViewModel and CancelBag is a clarified approach for dependency injection.


103-111: View lifecycle entry is consistent.

viewDidLoad triggers 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(_:) and updateComments(_:) handle snapshot changes in a clean, modern way.

Comment on lines +30 to +65
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()
)
}
Copy link

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.

Suggested change
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)

Comment on lines 178 to 179
// TODO: 온보딩 기능 개발 후 다시 되돌려야 함
sessionInfo.isNewUser ? owner.navigateToOnboarding() : owner.navigateToOnboarding()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
// 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)

Comment on lines +20 to +43
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)
}
Copy link

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.

Suggested change
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
}
}
}

Comment on lines +140 to +211
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)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling and reduce complexity in the Combine chain.

  1. If profileUseCase.execute fails, there's no fallback or UI update for the user. Consider a dedicated failure handling path.
  2. 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.

Suggested change
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)

Comment on lines +177 to +186
@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)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider using 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)
}

Comment on lines +49 to 60
.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()
}
Copy link

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.

Suggested change
.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)

Comment on lines +66 to +83
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)

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

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.

Suggested change
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)

Comment on lines +51 to +67
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)
Copy link

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.

Suggested change
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)

Comment on lines 148 to 161
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)

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add 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.

Suggested change
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)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (5)

94-94: Check the impact of always scrolling to top in viewWillAppear.

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, willDisplayLastItemSubject might emit repeatedly. Consider using a guard or an isLoading state to ensure consecutive triggers don’t overlap or cause repeated fetch calls.


248-253: Redundant reloadData() 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6beee59 and e016148.

📒 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 default true for shouldShowLoadingScreen.

Displaying the loading screen on every viewWillAppear may 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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🔭 Outside diff range comments (1)
Wable-iOS/Presentation/WableComponent/Button/CommentButton.swift (1)

49-49: 🛠️ Refactor suggestion

Update method documentation to match implementation.

The method signature configureButton(commentCount: Int = 0) doesn't include a type parameter, but the documentation and examples on lines 18-20 show usage with a type parameter. 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 .content case have been added directly into the configuration method, while similar constraint logic for the .comment case 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 configureButton method.

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 use replaceError(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.
didCommentHeartTappedSubject is 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 your cancelBag or 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 to didCreateTappedSubject might 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 unused textViewState subscription 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0e68501 and 4cead09.

📒 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 configureButton is 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 the index is 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)

Comment on lines +88 to +103
.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()
Copy link

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.

Comment on lines +139 to +148
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)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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)

Comment on lines +48 to +49
// TODO: ViewModel로 옮겨야 할 로직

Copy link

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?

Copy link
Collaborator

@JinUng41 JinUng41 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다~

많은 내용에 보다가 숨이 넘어갈 뻔 했어요!
하지만 결국 다 봤으니 다행입니다.

사실 구현하기 쉬운 내용은 아니라고 생각이 드는데, 시간도 없다보니 다소 코드가 복잡해진 것 같습니다.
유진님도 이 점을 잘 아시고, PR에 적어주신 것 같아요.

코멘트는 남겨두었으나, 시간 관계상 넘어가고 추후에 반영해도 괜찮습니다.
화이팅하세요~

Comment on lines +33 to +36
func updateUserSession(session: UserSession) -> AnyPublisher<Void, Never> {
return Just(repository.updateUserSession(session))
.eraseToAnyPublisher()
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

타입 메서드 .just()도 가능하답니다~

Comment on lines 111 to 125
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

willAppearSubject.send()
}

override func viewDidLoad() {
super.viewDidLoad()

setupView()
setupConstraint()
setupDataSource()
setupAction()
setupBinding()
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

순서만 변경부탁드려요~

Comment on lines +181 to +183
let commentCellRegistration = UICollectionView.CellRegistration<CommentCollectionViewCell, ContentComment> { cell, indexPath, item in

self.userInformationUseCase.fetchActiveUserID()
Copy link
Collaborator

Choose a reason for hiding this comment

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

지금은 순환 참조의 여지가 보이네요.
약한 참조 혹은 미소유 참조를 하는 것이 좋아 보여요.

Comment on lines 185 to 195
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)
})
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

셀을 바인딩 하는 과정에서 유즈케이스를 실행 시켜야 하는 것이 신기하네요.
이유가 있을까요?

Comment on lines +219 to +220
createCommentButton.addAction(UIAction(handler: { _ in
guard let text = self.commentTextView.text else { return }
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분도 순환 참조의 여지가 보여요!

Comment on lines +17 to +21
var defaultImageList = [
DefaultProfileType.blue,
DefaultProfileType.green,
DefaultProfileType.purple
]
Copy link
Collaborator

Choose a reason for hiding this comment

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

고정적인 내용인 것으로 알고 있는데, 배열이 변경될 여지가 있는지 궁금합니다.
그리고 DefaultProfileTypeCaseIterable을 따르게 한다면, DefaultProfileType.allCases를 사용할 수 있어 더욱 간편할 것 같네요.

Comment on lines +142 to +209
.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)
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분은 확실히 수정의 여지가 있어보이네요.

sink 안에 sink 안에 sink라니,,

Comment on lines +118 to +129
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)
Copy link
Collaborator

@JinUng41 JinUng41 Apr 11, 2025

Choose a reason for hiding this comment

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

버튼이 눌렸을 때 유즈케이스를 실행하는 점은 알겠으나, 버튼이 눌릴 때마다 새로운 구독 관계가 형성되어 cancelBag에 저장되기에 메모리 누수가 발생할 것 같네요.

completion으로 UI를 반영하는 것이 맞는지도 의문입니다.

Comment on lines +129 to +136
if let navigationController = viewController as? UINavigationController {
if navigationController.viewControllers.first is HomeViewController {
if tabBarController.selectedIndex == 0 {
homeViewController.scrollToTop()
}
}
}
}
Copy link
Collaborator

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()

Comment on lines +12 to +33
/// 주어진 `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)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

속성을 계속 유지한다는 관점에서는, 편의 생성자로 구현하는 것도 괜찮아 보이네요.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 nil or 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4cead09 and 178927c.

📒 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 Constant enum 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 LottieAnimationView and UITextView are well-initialized. Great job ensuring corner radius, padding, and style.


101-110: Initializer is standard MVVM.

Injecting the viewModel and cancelBag is consistent with standard dependency injection.


112-127: Lifecycle methods are used appropriately.

Using viewWillAppear to send signals and viewDidLoad for 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.

Comment on lines +48 to +58
// TODO: ViewModel로 옮겨야 할 로직

private let userInformationUseCase = FetchUserInformationUseCase(
repository: UserSessionRepositoryImpl(
userDefaults: UserDefaultsStorage(
jsonEncoder: JSONEncoder(),
jsonDecoder: JSONDecoder()
)
)
)

Copy link

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)

@youz2me youz2me merged commit 83271fb into develop Apr 11, 2025
1 check passed
@youz2me youz2me deleted the feat/#139-home-ui branch April 11, 2025 14:01
youz2me added a commit that referenced this pull request Oct 26, 2025
[Feat] 온보딩 기능 구현 및 메인 홈, 글 작성 구현
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat 기능 또는 객체 구현 🦉 유진 🛌🛌🛌🛌🛌🛌🛌🛌🛌🛌

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 메인 홈 피드 화면 UI 구현

3 participants