Skip to content

Conversation

@youz2me
Copy link
Member

@youz2me youz2me commented May 3, 2025

👻 PULL REQUEST

📄 작업 내용

  • 홈 화면에서 구현되지 않았던 기능들을 구현했습니다.
    • 게시물 작성자 구분 기능을 구현했습니다.
    • 내리기 기능을 구현했습니다.
    • 신고하기, 삭제하기, 밴하기 기능을 구현했습니다.
  • 제가 테스트플라이트에 QA 때 반영했던 내용을 올리면서 따로 디벨롭 브랜치에 머지하지 않은 내용이 있는 것 같은데... 해당 사유 때문에 현재 날아간 기능들이 몇 가지 있습니다.
    • 현재 하트 클릭 시 업데이트 안되는 이슈 발견해서 수정했습니다.
    • 로딩 뷰 계속 뜨는 이슈도 발견했는데 해당 이슈는 세부 화면 PR에 반영 예정입니다.

💻 주요 코드 설명

Opacity 구조체 reduced() 메서드 추가 구현

func reduced() -> Opacity {
        var newValue = value
        if newValue > Self.minValue {
            newValue -= 1
        }
        return Opacity(value: newValue)
    }
  • 내리기 로직을 구현하기 위해서는 서버 통신 없이 해당 유저의 모든 게시물 투명도를 낮춰야 합니다.
    • 해당 로직을 구현하기 위해서는 원본 게시물 배열을 복사해 투명도를 낮추고, 복사된 배열을 output으로 전송하는 과정이 필요합니다.
    • 해당 과정을 조금 더 간편하게 처리하기 위해 Opacity 구조체 내에 Opacity 구조체를 반환하는 reduced() 메서드를 구현했습니다.
해당 코드가 있는 파일명
// 여기에 코드를 적어주세요!

📚 참고자료

👀 기타 더 이야기해볼 점

  • 실행 영상 대신 QA 전에 같이 한번 점검하면 좋을 것 같아서 따로 올리지 않았습니다. 자체 테스트했을 때는 기능 모두 잘 작동하는 것 확인했습니다 ~!
  • 현재 반복되는 로직이 많고 바인딩 코드가 너무 길어지는 문제가 있어 해당 문제 QA 이후 리팩토링 예정입니다.

🔗 연결된 이슈

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added support for reporting, banning, deleting, and ghosting content directly from the home screen, including new confirmation sheets and admin-specific actions.
    • Introduced new user interaction points on content cells, such as profile image, settings, and ghost button taps.
    • Added toast notifications to confirm successful report submissions.
  • Enhancements

    • Improved content moderation by updating content status and opacity in response to user actions.
    • Updated cell configuration and view model to support expanded interaction and moderation flows.
  • Bug Fixes

    • Adjusted ghost button visibility to better reflect post ownership and status.
  • Style

    • Updated sheet title font style for a more consistent appearance.
  • Chores

    • Integrated new use case files and dependencies for reporting, banning, ghosting, and user info retrieval into the project.

@youz2me youz2me added ✨ feat 기능 또는 객체 구현 🦉 유진 🛌🛌🛌🛌🛌🛌🛌🛌🛌🛌 labels May 3, 2025
@youz2me youz2me requested a review from JinUng41 May 3, 2025 18:25
@youz2me youz2me self-assigned this May 3, 2025
@coderabbitai
Copy link

coderabbitai bot commented May 3, 2025

Walkthrough

This update introduces new use cases and expands the Home screen's interactive capabilities. Three new use case files—FetchGhostUseCase, CreateBannedUseCase, and CreateReportUseCase—are added and integrated into the Xcode project. The HomeViewModel and HomeViewController are extended to support ghosting, reporting, banning, and deleting content, with corresponding UI handlers and reactive bindings. The ContentCollectionViewCell is updated to expose new tap handler closures and refines ghost button visibility. Additional dependency injections are made in the tab bar controller. Minor UI and method signature adjustments are also included.

Changes

File(s) Change Summary
Wable-iOS.xcodeproj/project.pbxproj Added FetchGhostUseCase.swift, CreateBannedUseCase.swift, and CreateReportUseCase.swift to the project, grouped under Home, and included in build sources.
Wable-iOS/Domain/Entity/Opacity.swift Added non-mutating method reduced() to Opacity struct for functional opacity decrementing.
Wable-iOS/Domain/UseCase/Home/CreateBannedUseCase.swift
Wable-iOS/Domain/UseCase/Home/CreateReportUseCase.swift
Wable-iOS/Domain/UseCase/Home/FetchGhostUseCase.swift
Introduced three new use case classes: CreateBannedUseCase, CreateReportUseCase, and FetchGhostUseCase. Each encapsulates repository logic for banning users, reporting content, and handling ghost operations, respectively.
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift Expanded content cell configuration to include empty tap handler closures for settings, profile image, and ghost button.
Wable-iOS/Presentation/Home/View/HomeViewController.swift Added new PassthroughSubjects for ghost, delete, ban, and report actions; extended cell configuration to handle new tap events; updated bottom sheet logic for admin/user actions; added toast for report success; updated data source and property order.
Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift Added new use case dependencies and reactive inputs/outputs for ghost, delete, ban, and report actions; updated transformation logic to handle these actions and update content state accordingly.
Wable-iOS/Presentation/TabBar/TabBarController.swift Injected new use cases into HomeViewModel initialization, expanding its functional scope.
Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift Added closure properties for profile image, settings, and ghost button tap handlers; updated selector methods to use these handlers; revised ghost button visibility logic; updated cell configuration method signature.
Wable-iOS/Presentation/WableComponent/ViewController/WableSheetViewController.swift Changed the font style for the sheet title label from .head1 to .head2.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant HomeViewController
    participant HomeViewModel
    participant UseCases
    participant Repository

    User->>HomeViewController: Tap on content cell (ghost/ban/report/delete)
    HomeViewController->>HomeViewModel: Send event via subject
    HomeViewModel->>UseCases: Call appropriate use case (ghost/report/ban/delete)
    UseCases->>Repository: Perform repository operation
    Repository-->>UseCases: Return result (success/failure)
    UseCases-->>HomeViewModel: Complete publisher
    HomeViewModel-->>HomeViewController: Publish updated state/output
    HomeViewController-->>User: Update UI (sheet, toast, cell state)
Loading

Assessment against linked issues

Objective Addressed Explanation
프로필 화면 전환 기능 구현 (#178) No explicit implementation for profile navigation is shown in the provided changes.
게시물 작성자 구분 기능 구현 (#178) Author identification and admin/user logic are handled in cell configuration and bottom sheet presentation.
내리기 기능 구현 (#178) Ghosting ("내리기") is implemented via new use case, UI handlers, and state updates.
삭제/신고/밴 기능 구현 (#178) Delete, report, and ban functionalities are implemented with new use cases, UI handlers, and reactive flows.

Possibly related PRs

Poem

A rabbit taps on ghost and ban,
With new use cases, the magic began!
Cells now listen for every press,
Report and toast away the mess.
Admins and users, roles are clear—
Home’s more lively, let’s all cheer!
🐰✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 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.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

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

CodeRabbit Commands (Invoked using PR comments)

  • @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 generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @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: 5

🔭 Outside diff range comments (1)
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (1)

185-201: 💡 Verification agent

🧩 Analysis chain

Added new tap handlers for content cell configuration.

You've added empty handler closures for setting button, profile image view, and ghost button taps. This enhancement aligns with the broader implementation of content moderation features. Consider implementing these handlers to mirror the functionality in HomeViewController for consistency across screens.


🏁 Script executed:

#!/bin/bash
# Check if HomeViewController implements these handlers
rg -A 5 "settingButtonTapHandler|profileImageViewTapHandler|ghostButtonTapHandler" --type swift

Length of output: 7171


Implement missing tap handlers in HomeDetailViewController

The settingButtonTapHandler, profileImageViewTapHandler, and ghostButtonTapHandler closures in HomeDetailViewController.swift (around lines 185–201) are currently empty. To keep the experience consistent with HomeViewController, please implement them using the same logic:

• settingButtonTapHandler: present WableBottomSheetController and add delete/report actions for the post author
• profileImageViewTapHandler: push ProfileViewController
• ghostButtonTapHandler: present WableSheetViewController with the moderation prompt and actions

Locations to update:

  • Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift, inside the cell.configureCell(…) call (lines 185–201)
🧹 Nitpick comments (6)
Wable-iOS/Domain/UseCase/Home/CreateReportUseCase.swift (1)

22-26: Clean implementation of the execute method

The method properly delegates to the repository layer, maintaining separation of concerns and following the established pattern in your codebase.

Consider adding basic validation for the input parameters (e.g., checking if text is empty) if this validation isn't already handled at the UI or repository level:

func execute(nickname: String, text: String) -> AnyPublisher<Void, WableError> {
+   guard !text.isEmpty else {
+       return Fail(error: WableError.invalidInput).eraseToAnyPublisher()
+   }
    return repository.createReport(nickname: nickname, text: text)
}
Wable-iOS/Domain/UseCase/Home/FetchGhostUseCase.swift (1)

23-30: Consider more descriptive method naming and string mapping

The implementation works correctly but has some opportunities for improvement.

Consider these refinements:

  1. Rename the method to better reflect its action (e.g., reduceGhost or markAsGhost instead of the generic execute)
  2. Move the string mapping logic to a dedicated mapper or to the repository layer:
-func execute(type: PostType, targetID: Int, userID: Int) -> AnyPublisher<Void, WableError> {
+func reduceGhost(type: PostType, targetID: Int, userID: Int) -> AnyPublisher<Void, WableError> {
    return repository.postGhostReduction(
-       alarmTriggerType: type == .comment ? "commentGhost" : "contentGhost",
+       alarmTriggerType: type.ghostTriggerType,
        alarmTriggerID: targetID,
        targetMemberID: userID,
        reason: ""
    )
}

With an extension on PostType:

extension PostType {
    var ghostTriggerType: String {
        return self == .comment ? "commentGhost" : "contentGhost"
    }
}

Why is the reason parameter always an empty string? If it's not used, consider removing it or documenting why it's empty.

Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (1)

252-261: Consider consistent parameter ordering in configureCell.

The order of parameters in the method signature is different from the order in which they're assigned to properties in the method body. For better maintainability, consider keeping them in the same order.

func configureCell(
    info: ContentInfo,
    postType: AuthorType,
    cellType: CellType = .list,
    likeButtonTapHandler: (() -> Void)?,
+   profileImageViewTapHandler: (() -> Void)?,
    settingButtonTapHandler: (() -> Void)?,
-   profileImageViewTapHandler: (() -> Void)?,
    ghostButtonTapHandler: (() -> Void)?
) {
    self.cellType = cellType
    self.likeButtonTapHandler = likeButtonTapHandler
+   self.profileImageViewTapHandler = profileImageViewTapHandler
    self.ghostButtonTapHandler = ghostButtonTapHandler
-   self.profileImageViewTapHandler = profileImageViewTapHandler
    self.settingButtonTapHandler = settingButtonTapHandler
    
    // ...
Wable-iOS/Presentation/Home/View/HomeViewController.swift (2)

168-169: Fix the unused parameter warning.

SwiftLint identified an unused parameter in the closure.

-   let homeCellRegistration = CellRegistration<ContentCollectionViewCell, Content> { [weak self] cell, indexPath, itemID in
+   let homeCellRegistration = CellRegistration<ContentCollectionViewCell, Content> { [weak self] cell, _, itemID in
🧰 Tools
🪛 SwiftLint (0.57.0)

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

(unused_closure_parameter)


403-413: Fix toast message typo and simplify conditional logic.

There's a typo in the toast message and the conditional logic could be simplified.

output.isReportSucceed
    .receive(on: DispatchQueue.main)
    .sink { isSucceed in
        let toast = ToastView(
            status: .complete,
-           message: "신고 접수가 완료되었어요.\n24시간 이내에 조치할 예정이예요."
+           message: "신고 접수가 완료되었어요.\n24시간 이내에 조치할 예정이에요."
        )
        
-       isSucceed ? toast.show() : nil
+       if isSucceed {
+           toast.show()
+       }
    }
    .store(in: cancelBag)
Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (1)

16-40: Constructor growing to ≥10 dependencies – consider grouping use cases

The initializer now requires ten separate use-case injections. This is a red flag for the Interface Segregation & Single-Responsibility principles:

• The view-model is directly aware of many domain concepts (ghosting, banning, reporting, …).
• Tests and call-sites become increasingly verbose.

Consider introducing a small facade/struct (e.g. HomeActionProvider) that bundles moderation-related actions. That keeps the view-model signature stable while you evolve the feature set.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 482ff3c and ec3ceaa.

📒 Files selected for processing (11)
  • Wable-iOS.xcodeproj/project.pbxproj (6 hunks)
  • Wable-iOS/Domain/Entity/Opacity.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Home/CreateBannedUseCase.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Home/CreateReportUseCase.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Home/FetchGhostUseCase.swift (1 hunks)
  • Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (1 hunks)
  • Wable-iOS/Presentation/Home/View/HomeViewController.swift (4 hunks)
  • Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (3 hunks)
  • Wable-iOS/Presentation/TabBar/TabBarController.swift (1 hunks)
  • Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (4 hunks)
  • Wable-iOS/Presentation/WableComponent/ViewController/WableSheetViewController.swift (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
Wable-iOS/Domain/UseCase/Home/CreateReportUseCase.swift (2)
Wable-iOS/Domain/UseCase/Home/CreateBannedUseCase.swift (1)
  • execute (23-25)
Wable-iOS/Data/RepositoryImpl/ReportRepositoryImpl.swift (1)
  • createReport (20-32)
Wable-iOS/Domain/UseCase/Home/FetchGhostUseCase.swift (1)
Wable-iOS/Data/RepositoryImpl/GhostRepositoryImpl.swift (1)
  • postGhostReduction (18-37)
Wable-iOS/Domain/UseCase/Home/CreateBannedUseCase.swift (2)
Wable-iOS/Domain/UseCase/Home/CreateReportUseCase.swift (1)
  • execute (23-25)
Wable-iOS/Data/RepositoryImpl/ReportRepositoryImpl.swift (1)
  • createBan (34-47)
Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (4)
Wable-iOS/Presentation/WableComponent/View/PostUserInfoView.swift (1)
  • settingButtonDidTap (133-135)
Wable-iOS/Presentation/WableComponent/Button/CommentButton.swift (1)
  • configureButton (49-82)
Wable-iOS/Presentation/WableComponent/Button/LikeButton.swift (1)
  • configureButton (72-94)
Wable-iOS/Presentation/WableComponent/Button/GhostButton.swift (1)
  • configureButton (72-102)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (4)
Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (1)
  • configureCell (248-310)
Wable-iOS/Presentation/WableComponent/ViewController/WableSheetViewController.swift (1)
  • addActions (122-124)
Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (1)
  • transform (66-316)
Wable-iOS/Presentation/WableComponent/View/ToastView.swift (1)
  • show (144-160)
🪛 SwiftLint (0.57.0)
Wable-iOS/Presentation/Home/View/HomeViewController.swift

[Warning] 267-267: TODOs should be resolved (프로필 구현되는 대로 추가적인 설정 필요)

(todo)


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

(unused_closure_parameter)

🔇 Additional comments (14)
Wable-iOS/Presentation/WableComponent/ViewController/WableSheetViewController.swift (1)

65-65: Font style change from .head1 to .head2 looks good.

The change from .head1 to .head2 for the title label's attributed text style is consistent with UI changes described in the summary. The modification reduces the title's emphasis, which is appropriate for bottom sheets used in content moderation flows.

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

72-78: Good addition of non-mutating reduced() method.

This non-mutating version of the reduce() method is a nice addition that follows functional programming principles. It allows getting a new Opacity instance with reduced value without modifying the original, which is useful for previewing opacity changes or working with immutable state patterns.

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

161-163: Files properly added to the project structure

The three new use case files (FetchGhostUseCase, CreateBannedUseCase, and CreateReportUseCase) are correctly added to the project's build sources.


459-461: File references properly configured

File references for the new use cases are correctly set up in the project file.

Wable-iOS/Domain/UseCase/Home/CreateReportUseCase.swift (1)

12-18: Well-structured use case with proper dependency injection

The class follows clean architecture principles by injecting the repository dependency, making it testable and maintainable.

Wable-iOS/Domain/UseCase/Home/FetchGhostUseCase.swift (1)

12-18: Well-structured use case with proper dependency injection

Good use of dependency injection and final class, following the clean architecture pattern in the project.

Wable-iOS/Domain/UseCase/Home/CreateBannedUseCase.swift (1)

1-27: Clean implementation of the CreateBannedUseCase.

The implementation is well-structured following the clean architecture pattern. The use case properly delegates to the repository and handles asynchronous operations using Combine. Good job on the separation of concerns and dependency injection.

Wable-iOS/Presentation/TabBar/TabBarController.swift (1)

24-36: Well-structured dependency injection for HomeViewModel.

The addition of new use cases to HomeViewModel is properly implemented with appropriate repository instantiations. The code follows the project's architecture pattern for dependency injection.

Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (3)

36-38: Good addition of tap handler properties for expanded interactivity.

The new optional closure properties are well-designed for flexible event handling.


201-210: Handler implementations correctly use the new tap handlers.

The methods have been properly updated to invoke the handler closures instead of containing debug messages or TODO comments.


289-290: Improved ghost button visibility logic.

The change to hide the ghost button for own content or already ghosted content is a meaningful improvement to the user experience.

Wable-iOS/Presentation/Home/View/HomeViewController.swift (3)

35-44: Good addition of subjects for new content moderation actions.

The new PassthroughSubjects for ghosting, deletion, banning, and reporting are well-designed for reactive handling of user interactions.


333-337: Good integration of new reactive inputs.

The additional inputs for the new content moderation features are properly integrated with the view model transform method.


342-355: Well-implemented user state management.

The bindings for admin status and active user ID follow a consistent pattern and properly update the controller's state.

self.present(viewController, animated: true)
},
profileImageViewTapHandler: {
// TODO: 프로필 구현되는 대로 추가적인 설정 필요
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Resolve the TODO comment for profile implementation.

There's a TODO comment that should be addressed when the profile functionality is available.


🏁 Script executed:

#!/bin/bash
# Check if profile functionality is implemented elsewhere in the codebase

# Look for ProfileViewController implementation
echo "Checking ProfileViewController implementation:"
rg -A 3 "class ProfileViewController" --type swift

Length of output: 393


Implement Profile Integration and Remove TODO

ProfileViewController is already implemented (Wable-iOS/Presentation/Profile/ProfileViewController.swift), so please remove the TODO at line 267 in HomeViewController.swift and replace it with the actual code to present or push the profile screen.

• File: Wable-iOS/Presentation/Home/View/HomeViewController.swift
Line: ~267 – replace

// TODO: 프로필 구현되는 대로 추가적인 설정 필요

with something like:

let profileVC = ProfileViewController()
navigationController?.pushViewController(profileVC, animated: true)
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 267-267: TODOs should be resolved (프로필 구현되는 대로 추가적인 설정 필요)

(todo)

Comment on lines 178 to 295
settingButtonTapHandler: {
let viewController = WableBottomSheetController()

if self.activeUserID == itemID.content.contentInfo.author.id {
viewController.addActions(WableBottomSheetAction(title: "삭제하기", handler: {
viewController.dismiss(animated: true, completion: {
let viewController = WableSheetViewController(title: "게시글을 삭제하시겠어요?", message: "게시글이 영구히 삭제됩니다.")
viewController.addActions(
WableSheetAction(
title: "취소",
style: .gray,
handler: {
viewController.dismiss(animated: true)
}
),
WableSheetAction(
title: "삭제하기",
style: .primary,
handler: {
viewController.dismiss(animated: true, completion: {
self.didDeleteTappedSubject.send(itemID.content.id)
})
}
)
)

self.present(viewController, animated: true)
})
}))
} else if self.isActiveUserAdmin ?? false {
viewController.addActions(WableBottomSheetAction(title: "신고하기", handler: {
viewController.dismiss(animated: true, completion: {
let viewController = WableSheetViewController(title: "신고하시겠어요?")
viewController.addActions(
WableSheetAction(
title: "취소",
style: .gray,
handler: {
viewController.dismiss(animated: true)
}
),
WableSheetAction(
title: "신고하기",
style: .primary,
handler: {
viewController.dismiss(animated: true, completion: {
self.didReportTappedSubject.send((itemID.content.contentInfo.title, itemID.content.contentInfo.text))
})
}
)
)

self.present(viewController, animated: true)
})
}), WableBottomSheetAction(title: "밴하기", handler: {
self.didBannedTappedSubject.send((itemID.content.contentInfo.author.id, itemID.content.id))
})
)
} else {
viewController.addActions(WableBottomSheetAction(title: "신고하기", handler: {
viewController.dismiss(animated: true, completion: {
let viewController = WableSheetViewController(title: "신고하시겠어요?")
viewController.addActions(
WableSheetAction(
title: "취소",
style: .gray,
handler: {
viewController.dismiss(animated: true)
}
),
WableSheetAction(
title: "신고하기",
style: .primary,
handler: {
viewController.dismiss(animated: true, completion: {
self.didReportTappedSubject.send((itemID.content.contentInfo.title, itemID.content.contentInfo.text))
})
}
)
)

self.present(viewController, animated: true)
})
}))
}

self.present(viewController, animated: true)
},
profileImageViewTapHandler: {
// TODO: 프로필 구현되는 대로 추가적인 설정 필요
let viewController = ProfileViewController()

self.navigationController?.pushViewController(viewController, animated: true)
},
ghostButtonTapHandler: {
let viewController = WableSheetViewController(title: "와블의 온화한 문화를 해치는\n누군가를 발견하신 건가요?")
viewController.addActions(
WableSheetAction(
title: "고민할게요",
style: .gray,
handler: {
viewController.dismiss(animated: true)
}
),
WableSheetAction(
title: "네 맞아요",
style: .primary,
handler: {
viewController.dismiss(animated: true, completion: {
self.didGhostTappedSubject.send((itemID.content.id, itemID.content.contentInfo.author.id))
})
}
)
)

self.present(viewController, animated: true)
}
)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Extract bottom sheet presentation logic into helper methods.

The cell configuration closure contains complex nested logic for presenting different bottom sheets based on user roles. This makes the code difficult to read and maintain.

Consider extracting this logic into separate helper methods to improve readability and maintainability:

// Add these helper methods
+ private func presentDeleteConfirmationSheet(for contentID: Int) {
+     let viewController = WableSheetViewController(title: "게시글을 삭제하시겠어요?", message: "게시글이 영구히 삭제됩니다.")
+     viewController.addActions(
+         WableSheetAction(
+             title: "취소",
+             style: .gray,
+             handler: { viewController.dismiss(animated: true) }
+         ),
+         WableSheetAction(
+             title: "삭제하기",
+             style: .primary,
+             handler: {
+                 viewController.dismiss(animated: true) {
+                     self.didDeleteTappedSubject.send(contentID)
+                 }
+             }
+         )
+     )
+     self.present(viewController, animated: true)
+ }
+ 
+ private func presentReportConfirmationSheet(title: String, text: String) {
+     let viewController = WableSheetViewController(title: "신고하시겠어요?")
+     viewController.addActions(
+         WableSheetAction(
+             title: "취소",
+             style: .gray,
+             handler: { viewController.dismiss(animated: true) }
+         ),
+         WableSheetAction(
+             title: "신고하기",
+             style: .primary,
+             handler: {
+                 viewController.dismiss(animated: true) {
+                     self.didReportTappedSubject.send((title, text))
+                 }
+             }
+         )
+     )
+     self.present(viewController, animated: true)
+ }
+ 
+ private func presentGhostConfirmationSheet(contentID: Int, authorID: Int) {
+     let viewController = WableSheetViewController(title: "와블의 온화한 문화를 해치는\n누군가를 발견하신 건가요?")
+     viewController.addActions(
+         WableSheetAction(
+             title: "고민할게요",
+             style: .gray,
+             handler: { viewController.dismiss(animated: true) }
+         ),
+         WableSheetAction(
+             title: "네 맞아요",
+             style: .primary,
+             handler: {
+                 viewController.dismiss(animated: true) {
+                     self.didGhostTappedSubject.send((contentID, authorID))
+                 }
+             }
+         )
+     )
+     self.present(viewController, animated: true)
+ }

Then simplify the cell configuration:

settingButtonTapHandler: {
    let viewController = WableBottomSheetController()
    
    if self.activeUserID == itemID.content.contentInfo.author.id {
        viewController.addActions(WableBottomSheetAction(title: "삭제하기", handler: {
            viewController.dismiss(animated: true) {
-               let viewController = WableSheetViewController(title: "게시글을 삭제하시겠어요?", message: "게시글이 영구히 삭제됩니다.")
-               viewController.addActions(
-                   WableSheetAction(
-                       title: "취소",
-                       style: .gray,
-                       handler: {
-                           viewController.dismiss(animated: true)
-                       }
-                   ),
-                   WableSheetAction(
-                       title: "삭제하기",
-                       style: .primary,
-                       handler: {
-                           viewController.dismiss(animated: true, completion: {
-                               self.didDeleteTappedSubject.send(itemID.content.id)
-                           })
-                       }
-                   )
-               )
-               
-               self.present(viewController, animated: true)
+               self.presentDeleteConfirmationSheet(for: itemID.content.id)
            }
        }))
    } else if self.isActiveUserAdmin ?? false {
        viewController.addActions(WableBottomSheetAction(title: "신고하기", handler: {
            viewController.dismiss(animated: true) {
-               let viewController = WableSheetViewController(title: "신고하시겠어요?")
-               viewController.addActions(
-                   WableSheetAction(
-                       title: "취소",
-                       style: .gray,
-                       handler: {
-                           viewController.dismiss(animated: true)
-                       }
-                   ),
-                   WableSheetAction(
-                       title: "신고하기",
-                       style: .primary,
-                       handler: {
-                           viewController.dismiss(animated: true, completion: {
-                               self.didReportTappedSubject.send((itemID.content.contentInfo.title, itemID.content.contentInfo.text))
-                           })
-                       }
-                   )
-               )
-               
-               self.present(viewController, animated: true)
+               self.presentReportConfirmationSheet(title: itemID.content.contentInfo.title, text: itemID.content.contentInfo.text)
            }
        }), /* rest of the code */
    }
}),
// ...
ghostButtonTapHandler: {
-   let viewController = WableSheetViewController(title: "와블의 온화한 문화를 해치는\n누군가를 발견하신 건가요?")
-   viewController.addActions(
-       WableSheetAction(
-           title: "고민할게요",
-           style: .gray,
-           handler: {
-               viewController.dismiss(animated: true)
-           }
-       ),
-       WableSheetAction(
-           title: "네 맞아요",
-           style: .primary,
-           handler: {
-               viewController.dismiss(animated: true, completion: {
-                   self.didGhostTappedSubject.send((itemID.content.id, itemID.content.contentInfo.author.id))
-               })
-           }
-       )
-   )
-   
-   self.present(viewController, animated: true)
+   self.presentGhostConfirmationSheet(contentID: itemID.content.id, authorID: itemID.content.contentInfo.author.id)
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
settingButtonTapHandler: {
let viewController = WableBottomSheetController()
if self.activeUserID == itemID.content.contentInfo.author.id {
viewController.addActions(WableBottomSheetAction(title: "삭제하기", handler: {
viewController.dismiss(animated: true, completion: {
let viewController = WableSheetViewController(title: "게시글을 삭제하시겠어요?", message: "게시글이 영구히 삭제됩니다.")
viewController.addActions(
WableSheetAction(
title: "취소",
style: .gray,
handler: {
viewController.dismiss(animated: true)
}
),
WableSheetAction(
title: "삭제하기",
style: .primary,
handler: {
viewController.dismiss(animated: true, completion: {
self.didDeleteTappedSubject.send(itemID.content.id)
})
}
)
)
self.present(viewController, animated: true)
})
}))
} else if self.isActiveUserAdmin ?? false {
viewController.addActions(WableBottomSheetAction(title: "신고하기", handler: {
viewController.dismiss(animated: true, completion: {
let viewController = WableSheetViewController(title: "신고하시겠어요?")
viewController.addActions(
WableSheetAction(
title: "취소",
style: .gray,
handler: {
viewController.dismiss(animated: true)
}
),
WableSheetAction(
title: "신고하기",
style: .primary,
handler: {
viewController.dismiss(animated: true, completion: {
self.didReportTappedSubject.send((itemID.content.contentInfo.title, itemID.content.contentInfo.text))
})
}
)
)
self.present(viewController, animated: true)
})
}), WableBottomSheetAction(title: "밴하기", handler: {
self.didBannedTappedSubject.send((itemID.content.contentInfo.author.id, itemID.content.id))
})
)
} else {
viewController.addActions(WableBottomSheetAction(title: "신고하기", handler: {
viewController.dismiss(animated: true, completion: {
let viewController = WableSheetViewController(title: "신고하시겠어요?")
viewController.addActions(
WableSheetAction(
title: "취소",
style: .gray,
handler: {
viewController.dismiss(animated: true)
}
),
WableSheetAction(
title: "신고하기",
style: .primary,
handler: {
viewController.dismiss(animated: true, completion: {
self.didReportTappedSubject.send((itemID.content.contentInfo.title, itemID.content.contentInfo.text))
})
}
)
)
self.present(viewController, animated: true)
})
}))
}
self.present(viewController, animated: true)
},
profileImageViewTapHandler: {
// TODO: 프로필 구현되는 대로 추가적인 설정 필요
let viewController = ProfileViewController()
self.navigationController?.pushViewController(viewController, animated: true)
},
ghostButtonTapHandler: {
let viewController = WableSheetViewController(title: "와블의 온화한 문화를 해치는\n누군가를 발견하신 건가요?")
viewController.addActions(
WableSheetAction(
title: "고민할게요",
style: .gray,
handler: {
viewController.dismiss(animated: true)
}
),
WableSheetAction(
title: "네 맞아요",
style: .primary,
handler: {
viewController.dismiss(animated: true, completion: {
self.didGhostTappedSubject.send((itemID.content.id, itemID.content.contentInfo.author.id))
})
}
)
)
self.present(viewController, animated: true)
}
)
// ────────────────────────────────────────────────────────────────────────────────
// Add these helper methods inside HomeViewController
// ────────────────────────────────────────────────────────────────────────────────
private func presentDeleteConfirmationSheet(for contentID: Int) {
let viewController = WableSheetViewController(
title: "게시글을 삭제하시겠어요?",
message: "게시글이 영구히 삭제됩니다."
)
viewController.addActions(
WableSheetAction(
title: "취소",
style: .gray,
handler: { viewController.dismiss(animated: true) }
),
WableSheetAction(
title: "삭제하기",
style: .primary,
handler: {
viewController.dismiss(animated: true) {
self.didDeleteTappedSubject.send(contentID)
}
}
)
)
self.present(viewController, animated: true)
}
private func presentReportConfirmationSheet(title: String, text: String) {
let viewController = WableSheetViewController(title: "신고하시겠어요?")
viewController.addActions(
WableSheetAction(
title: "취소",
style: .gray,
handler: { viewController.dismiss(animated: true) }
),
WableSheetAction(
title: "신고하기",
style: .primary,
handler: {
viewController.dismiss(animated: true) {
self.didReportTappedSubject.send((title, text))
}
}
)
)
self.present(viewController, animated: true)
}
private func presentGhostConfirmationSheet(contentID: Int, authorID: Int) {
let viewController = WableSheetViewController(
title: "와블의 온화한 문화를 해치는\n누군가를 발견하신 건가요?"
)
viewController.addActions(
WableSheetAction(
title: "고민할게요",
style: .gray,
handler: { viewController.dismiss(animated: true) }
),
WableSheetAction(
title: "네 맞아요",
style: .primary,
handler: {
viewController.dismiss(animated: true) {
self.didGhostTappedSubject.send((contentID, authorID))
}
}
)
)
self.present(viewController, animated: true)
}
// ────────────────────────────────────────────────────────────────────────────────
// Then, inside your cell configuration closure:
// ────────────────────────────────────────────────────────────────────────────────
settingButtonTapHandler: {
let viewController = WableBottomSheetController()
if self.activeUserID == itemID.content.contentInfo.author.id {
viewController.addActions(
WableBottomSheetAction(title: "삭제하기", handler: {
viewController.dismiss(animated: true) {
self.presentDeleteConfirmationSheet(for: itemID.content.id)
}
})
)
} else if self.isActiveUserAdmin ?? false {
viewController.addActions(
WableBottomSheetAction(title: "신고하기", handler: {
viewController.dismiss(animated: true) {
self.presentReportConfirmationSheet(
title: itemID.content.contentInfo.title,
text: itemID.content.contentInfo.text
)
}
}),
WableBottomSheetAction(title: "밴하기", handler: {
self.didBannedTappedSubject.send(
(itemID.content.contentInfo.author.id, itemID.content.id)
)
})
)
} else {
viewController.addActions(
WableBottomSheetAction(title: "신고하기", handler: {
viewController.dismiss(animated: true) {
self.presentReportConfirmationSheet(
title: itemID.content.contentInfo.title,
text: itemID.content.contentInfo.text
)
}
})
)
}
self.present(viewController, animated: true)
},
ghostButtonTapHandler: {
self.presentGhostConfirmationSheet(
contentID: itemID.content.id,
authorID: itemID.content.contentInfo.author.id
)
}
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 267-267: TODOs should be resolved (프로필 구현되는 대로 추가적인 설정 필요)

(todo)

Comment on lines +201 to +231
.sink(receiveValue: { userID in
var updatedContents = contentsSubject.value

guard let index = updatedContents.firstIndex(where: { $0.content.contentInfo.author.id == userID }) else { return }

let content = updatedContents[index]
let contentInfo = content.content.contentInfo
let userContent = content.content
let opacity = contentInfo.opacity.reduced()

let updatedContent = Content(
content: UserContent(
id: userContent.id,
contentInfo: ContentInfo(
author: contentInfo.author,
createdDate: contentInfo.createdDate,
title: contentInfo.title,
imageURL: contentInfo.imageURL,
text: contentInfo.text,
status: .ghost,
like: contentInfo.like,
opacity: opacity,
commentCount: contentInfo.commentCount
)
),
isDeleted: content.isDeleted
)

updatedContents[index] = updatedContent
contentsSubject.send(updatedContents)
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

didGhostTappedItem updates only the first matching post – update all authored posts

Currently the logic stops after modifying the first content written by the target user:

guard let index = updatedContents.firstIndex(where: { $0.content.contentInfo.author.id == userID }) else { return }
...
updatedContents[index] = updatedContent

If the user has authored multiple posts in the timeline the remaining ones keep their original status, so they are still visible even though the backend has marked them as ghosted.

Replace the single‐index update with a map (or indices.filter) so every post from that author is updated.

-guard let index = updatedContents.firstIndex(where: { $0.content.contentInfo.author.id == userID }) else { return }
-
-let content = updatedContents[index]
-...
-updatedContents[index] = updatedContent
+updatedContents = updatedContents.map { content in
+    guard content.content.contentInfo.author.id == userID else { return content }
+
+    let info     = content.content.contentInfo
+    let opacity  = info.opacity.reduced()
+
+    return Content(
+        content: UserContent(
+            id: content.content.id,
+            contentInfo: ContentInfo(
+                author: info.author,
+                createdDate: info.createdDate,
+                title: info.title,
+                imageURL: info.imageURL,
+                text: info.text,
+                status: .ghost,
+                like: info.like,
+                opacity: opacity,
+                commentCount: info.commentCount
+            )
+        ),
+        isDeleted: content.isDeleted
+    )
+}

This guarantees UI consistency with backend state for all affected cells.

Comment on lines +269 to +299
.sink(receiveValue: { userID in
var updatedContents = contentsSubject.value

guard let index = updatedContents.firstIndex(where: { $0.content.contentInfo.author.id == userID }) else { return }

let content = updatedContents[index]
let contentInfo = content.content.contentInfo
let userContent = content.content
let opacity = contentInfo.opacity.reduced()

let updatedContent = Content(
content: UserContent(
id: userContent.id,
contentInfo: ContentInfo(
author: contentInfo.author,
createdDate: contentInfo.createdDate,
title: contentInfo.title,
imageURL: contentInfo.imageURL,
text: contentInfo.text,
status: .blind,
like: contentInfo.like,
opacity: opacity,
commentCount: contentInfo.commentCount
)
),
isDeleted: content.isDeleted
)

updatedContents[index] = updatedContent
contentsSubject.send(updatedContents)
})
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Same multi-post issue & code duplication in didBannedTappedItem

didBannedTappedItem suffers from the identical “first match only” bug and duplicates 40+ lines that were just written for ghosting.

Refactor the updating logic into a small helper that takes (authorID, newStatus) and returns an updated [Content]. That eliminates duplication and prevents the two branches from diverging in future maintenance.

Example sketch:

private func apply(status: ContentStatus,
                   toAuthor authorID: Int,
                   on contents: [Content]) -> [Content] {
    return contents.map { content in
        guard content.content.contentInfo.author.id == authorID else { return content }
        let info = content.content.contentInfo
        return content.replacing(info: info.copy(status: status,
                                                opacity: info.opacity.reduced()))
    }
}

Then both sinks become a terse:

updatedContents = apply(status: .ghost,  toAuthor: userID, on: updatedContents)

Comment on lines +71 to 74
let activeUserIDSubject = CurrentValueSubject<Int?, Never>(nil)
let isAdminSubject = CurrentValueSubject<Bool?, Never>(false)
let isReportSucceedSubject = CurrentValueSubject<Bool, Never>(false)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use PassthroughSubject for one-shot “report succeeded” event

isReportSucceedSubject is declared as a CurrentValueSubject, meaning it always keeps the latest boolean in memory and immediately re-emits it to every new subscriber.
For a one-off toast / banner trigger this is usually undesirable because:

  1. A later subscriber (e.g. when the view re-appears) will receive a stale true and think a new report just succeeded.
  2. You have to remember to manually reset the subject back to false, otherwise the side effect can fire repeatedly.

A PassthroughSubject conveys the event only when it actually happens and keeps no state, which is exactly the semantic we need.

-let isReportSucceedSubject = CurrentValueSubject<Bool, Never>(false)
+let isReportSucceedSubject = PassthroughSubject<Bool, Never>()

No other change is needed because you already call isReportSucceedSubject.send(true); the stream type remains AnyPublisher<Bool, Never>.

Also applies to: 251-261

@youz2me youz2me changed the title [Feat] 홈화면 기능 구현 [Feat] 홈 화면 기능 구현 May 3, 2025
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.

고생하셨습니다.

워낙 기능이 많고, 방대한 양이라 구현하시는데에 절 한번 올리겠습니다. 🙇🙇🏻
끝으로 뷰모델에서 튜플을 바로 사용하던데 content.0와 같은 경우, 정확하게 어떠한 것을 리턴하는지는 코드를 보고는 바로 알 수 없기 때문에, 이러한 경우는 지양해 주시면 감사하겠습니다.

Comment on lines +192 to +200
settingButtonTapHandler: {

},
profileImageViewTapHandler: {

},
ghostButtonTapHandler: {

}
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
Member Author

Choose a reason for hiding this comment

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

이 부분은 세부 화면 쪽에서 구현된 부분이라 해당 PR에서는 반영하지 않았습니다 ~!!

Comment on lines 171 to 265
cell.configureCell(
info: itemID.content.contentInfo,
postType: itemID.content.contentInfo.author.id == self.activeUserID ? .mine : .others,
cellType: .list,
likeButtonTapHandler: {
self.didHeartTappedSubject.send((itemID.content.id, cell.likeButton.isLiked))
},
settingButtonTapHandler: {
let viewController = WableBottomSheetController()

if self.activeUserID == itemID.content.contentInfo.author.id {
viewController.addActions(WableBottomSheetAction(title: "삭제하기", handler: {
viewController.dismiss(animated: true, completion: {
let viewController = WableSheetViewController(title: "게시글을 삭제하시겠어요?", message: "게시글이 영구히 삭제됩니다.")
viewController.addActions(
WableSheetAction(
title: "취소",
style: .gray,
handler: {
viewController.dismiss(animated: true)
}
),
WableSheetAction(
title: "삭제하기",
style: .primary,
handler: {
viewController.dismiss(animated: true, completion: {
self.didDeleteTappedSubject.send(itemID.content.id)
})
}
)
)

self.present(viewController, animated: true)
})
}))
} else if self.isActiveUserAdmin ?? false {
viewController.addActions(WableBottomSheetAction(title: "신고하기", handler: {
viewController.dismiss(animated: true, completion: {
let viewController = WableSheetViewController(title: "신고하시겠어요?")
viewController.addActions(
WableSheetAction(
title: "취소",
style: .gray,
handler: {
viewController.dismiss(animated: true)
}
),
WableSheetAction(
title: "신고하기",
style: .primary,
handler: {
viewController.dismiss(animated: true, completion: {
self.didReportTappedSubject.send((itemID.content.contentInfo.title, itemID.content.contentInfo.text))
})
}
)
)

self.present(viewController, animated: true)
})
}), WableBottomSheetAction(title: "밴하기", handler: {
self.didBannedTappedSubject.send((itemID.content.contentInfo.author.id, itemID.content.id))
})
)
} else {
viewController.addActions(WableBottomSheetAction(title: "신고하기", handler: {
viewController.dismiss(animated: true, completion: {
let viewController = WableSheetViewController(title: "신고하시겠어요?")
viewController.addActions(
WableSheetAction(
title: "취소",
style: .gray,
handler: {
viewController.dismiss(animated: true)
}
),
WableSheetAction(
title: "신고하기",
style: .primary,
handler: {
viewController.dismiss(animated: true, completion: {
self.didReportTappedSubject.send((itemID.content.contentInfo.title, itemID.content.contentInfo.text))
})
}
)
)

self.present(viewController, animated: true)
})
}))
}

self.present(viewController, animated: true)
},
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
Member Author

Choose a reason for hiding this comment

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

ㅎㅎ 세부 화면과도 겹치는 로직이 너무 많아서... 이후에 함께 관리할 수 있는 방법을 고민해봐야 할 것 같습니다. 좋은 방법이 있으시다면 공유 부탁드립니닷 🥹

Comment on lines +278 to +280
handler: {
viewController.dismiss(animated: true)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

dismiss는 하지 않아도 내부적으로 하게끔 작성해 두었습니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

헉 🫢 제가 확인을 제대로 못했네요 반영해두었습니다 ~!

Comment on lines +342 to +354
output.isAdmin
.receive(on: DispatchQueue.main)
.sink { [weak self] isAdmin in
self?.isActiveUserAdmin = isAdmin
}
.store(in: cancelBag)

output.activeUserID
.receive(on: DispatchQueue.main)
.sink { [weak self] id in
self?.activeUserID = id
}
.store(in: cancelBag)
Copy link
Collaborator

Choose a reason for hiding this comment

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

반응형으로 동작해야 하지 않는 작업의 경우는, 단순하게 뷰모델의 프로퍼티로 관리해도 좋을 것 같아요.
불필요한 Input- Output을 타야 하는 건 번거로울 것 같습니다.

Copy link
Member Author

@youz2me youz2me May 4, 2025

Choose a reason for hiding this comment

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

저도 이 부분 고민을 좀 했었는데요...! isAdmin과 activeUserID가 초기 로딩만 하고 거의 변하지 않는 값이라는 점에서 저도 굳이 이렇게 관리해야 하나? 라는 의문이 있었습니다. 그런데 모든 프로퍼티를 Input-Output으로 관리해야 하는줄 알고 있어서 ㅋㅋ ㅜㅜ 이렇게 구현했는데 프로퍼티로 빼도 되는 거였군요 ,,,

다만 리팩토링을 시도해봤는데 뷰모델에서 유저의 어드민 여부와 아이디를 받아온 후 해당 값을 뷰컨에서 받아 이후에 뷰들을 그려주는 과정이 필요한데 프로퍼티로 구현하게 되면 유저의 어드민 여부와 아이디를 받아오기 전에 뷰컨이 업데이트되면서 뷰가 제대로 그려지지 않는 오류가 있었습니다.
그래서 output에 isReportSucceed를 추가하고 값을 받아온 이후에 이벤트를 발행해서 뷰를 다시 그릴까 하는 생각도 했는데 그러면 지금 구조를 유지해도 괜찮지 않나? 하는 생각이 들더라구요. 뭔가 놓치고 있는 것 같기도 합니다...

이 부분은 일단 머지해둘테니 몸 나아지시면 다음에 한번 같이 봐주시면 좋을 것 ,,, 같습니닷 ,,, ㅜㅜ

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

♻️ Duplicate comments (3)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (3)

254-276: 🛠️ Refactor suggestion

Extract ghost confirmation sheet logic to a helper method.

The ghost confirmation sheet logic should also be extracted to improve readability and maintainability.

+ private func presentGhostConfirmationSheet(contentID: Int, authorID: Int) {
+     let viewController = WableSheetViewController(title: "와블의 온화한 문화를 해치는\n누군가를 발견하신 건가요?")
+     viewController.addActions(
+         WableSheetAction(
+             title: "고민할게요",
+             style: .gray
+         ),
+         WableSheetAction(
+             title: "네 맞아요",
+             style: .primary,
+             handler: {
+                 viewController.dismiss(animated: true) {
+                     self.didGhostTappedSubject.send((contentID, authorID))
+                 }
+             }
+         )
+     )
+     self.present(viewController, animated: true)
+ }

Then you can simplify the call:

-                    let viewController = WableSheetViewController(title: "와블의 온화한 문화를 해치는\n누군가를 발견하신 건가요?")
-                    viewController.addActions(
-                        WableSheetAction(
-                            title: "고민할게요",
-                            style: .gray,
-                            handler: {
-                                viewController.dismiss(animated: true)
-                            }
-                        ),
-                        WableSheetAction(
-                            title: "네 맞아요",
-                            style: .primary,
-                            handler: {
-                                viewController.dismiss(animated: true, completion: {
-                                    self.didGhostTappedSubject.send((itemID.content.id, itemID.content.contentInfo.author.id))
-                                })
-                            }
-                        )
-                    )
-                    
-                    self.present(viewController, animated: true)
+                    self.presentGhostConfirmationSheet(contentID: itemID.content.id, authorID: itemID.content.contentInfo.author.id)

202-243: 🛠️ Refactor suggestion

Extract report confirmation sheet logic to a helper method.

Similar to the deletion confirmation, the reporting flow is duplicated twice in this method. Extract it to reduce duplication.

+ private func presentReportConfirmationSheet(title: String, text: String) {
+     let viewController = WableSheetViewController(title: "신고하시겠어요?")
+     viewController.addActions(
+         WableSheetAction(title: "취소", style: .gray),
+         WableSheetAction(
+             title: "신고하기",
+             style: .primary,
+             handler: {
+                 viewController.dismiss(animated: true) {
+                     self.didReportTappedSubject.send((title, text))
+                 }
+             }
+         )
+     )
+     self.present(viewController, animated: true)
+ }

Then you can simplify both report action implementations.


182-200: 🛠️ Refactor suggestion

Extract deletion confirmation sheet logic to a helper method.

The deletion confirmation sheet presentation contains complex nested logic that makes the code difficult to read and maintain.

Consider extracting this into a dedicated helper method:

+ private func presentDeleteConfirmationSheet(for contentID: Int) {
+     let viewController = WableSheetViewController(title: "게시글을 삭제하시겠어요?", message: "게시글이 영구히 삭제됩니다.")
+     viewController.addActions(
+         WableSheetAction(title: "취소", style: .gray),
+         WableSheetAction(
+             title: "삭제하기",
+             style: .primary,
+             handler: {
+                 viewController.dismiss(animated: true) {
+                     self.didDeleteTappedSubject.send(contentID)
+                 }
+             }
+         )
+     )
+     self.present(viewController, animated: true)
+ }

Then you can simplify the call:

-                            viewController.dismiss(animated: true, completion: {
-                                let viewController = WableSheetViewController(title: "게시글을 삭제하시겠어요?", message: "게시글이 영구히 삭제됩니다.")
-                                viewController.addActions(
-                                    WableSheetAction(title: "취소", style: .gray),
-                                    WableSheetAction(
-                                        title: "삭제하기",
-                                        style: .primary,
-                                        handler: {
-                                            viewController.dismiss(animated: true, completion: {
-                                                self.didDeleteTappedSubject.send(itemID.content.id)
-                                            })
-                                        }
-                                    )
-                                )
-                                
-                                self.present(viewController, animated: true)
-                            })
+                            viewController.dismiss(animated: true) {
+                                self.presentDeleteConfirmationSheet(for: itemID.content.id)
+                            }
🧹 Nitpick comments (4)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (4)

249-252: Remove TODO comment as the implementation is already provided.

The TODO comment mentions waiting for profile implementation, but the implementation is already there in the following lines.

-                    // TODO: 프로필 구현되는 대로 추가적인 설정 필요
                     let viewController = ProfileViewController()
                    
                     self.navigationController?.pushViewController(viewController, animated: true)
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 249-249: TODOs should be resolved (프로필 구현되는 대로 추가적인 설정 필요)

(todo)


168-168: Fix unused parameter warning in closure.

The cell parameter is not used in the closure, which triggers a SwiftLint warning.

-        let homeCellRegistration = CellRegistration<ContentCollectionViewCell, Content> { [weak self] cell, indexPath, itemID in
+        let homeCellRegistration = CellRegistration<ContentCollectionViewCell, Content> { [weak self] cell, _, itemID in
🧰 Tools
🪛 SwiftLint (0.57.0)

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

(unused_closure_parameter)


260-262: Remove redundant dismiss call in WableSheetAction.

From previous review comments, it was noted that the dismiss functionality is already handled internally.

                            title: "고민할게요",
                            style: .gray,
-                            handler: {
-                                viewController.dismiss(animated: true)
-                            }
+                            handler: nil
                        ),

385-396: Fix typo in toast message.

There's a small typo in the success toast message.

-                    message: "신고 접수가 완료되었어요.\n24시간 이내에 조치할 예정이예요."
+                    message: "신고 접수가 완료되었어요.\n24시간 이내에 조치할 예정이에요."
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between ec3ceaa and 57fb957.

📒 Files selected for processing (1)
  • Wable-iOS/Presentation/Home/View/HomeViewController.swift (5 hunks)
🧰 Additional context used
🪛 SwiftLint (0.57.0)
Wable-iOS/Presentation/Home/View/HomeViewController.swift

[Warning] 249-249: TODOs should be resolved (프로필 구현되는 대로 추가적인 설정 필요)

(todo)


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

(unused_closure_parameter)

🔇 Additional comments (1)
Wable-iOS/Presentation/Home/View/HomeViewController.swift (1)

324-336: Consider alternative approaches for managing relatively static values.

As mentioned in previous review comments, consider whether reactive binding is necessary for values like isAdmin and activeUserID that rarely change. However, also consider the timing issues that were highlighted in past review discussions.

If these values are only needed at initial load and don't change during the lifecycle of the view controller, a simpler approach using properties might be possible.

@youz2me youz2me merged commit 0088e44 into develop May 7, 2025
1 check passed
@youz2me youz2me deleted the feat/#178-home branch May 7, 2025 15:18
youz2me added a commit that referenced this pull request Oct 26, 2025
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] 홈 화면 기능 구현

3 participants