Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Wable-iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@
DD8326962D95836F0014B62D /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8326952D95836B0014B62D /* HomeViewModel.swift */; };
DD8B42A42DA15DB0007DC1CA /* UITextView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8B42A32DA15DA9007DC1CA /* UITextView+.swift */; };
DD8B42A82DA16B1F007DC1CA /* HomeDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8B42A72DA16B1A007DC1CA /* HomeDetailViewModel.swift */; };
DD90EEA82DC40D1800338EB2 /* FetchGhostUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD90EEA72DC40D1700338EB2 /* FetchGhostUseCase.swift */; };
DD90EEAB2DC5F66900338EB2 /* CreateBannedUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD90EEAA2DC5F65100338EB2 /* CreateBannedUseCase.swift */; };
DD90EEAD2DC5F6CC00338EB2 /* CreateReportUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD90EEAC2DC5F6BB00338EB2 /* CreateReportUseCase.swift */; };
DD9980DB2D834CA900EBBFBC /* CommentInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9980DA2D834CA900EBBFBC /* CommentInfo.swift */; };
DD9980DF2D834F4E00EBBFBC /* CommentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9980DC2D834F4E00EBBFBC /* CommentCollectionViewCell.swift */; };
DD9980E02D834F4E00EBBFBC /* PostUserInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9980DE2D834F4E00EBBFBC /* PostUserInfoView.swift */; };
Expand Down Expand Up @@ -453,6 +456,9 @@
DD8CEF472D6A007900DBE580 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
DD8CEF482D6A007900DBE580 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
DD8CEFF82D6A00BB00DBE580 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = "Base.lproj/LaunchScreen 2.storyboard"; sourceTree = "<group>"; };
DD90EEA72DC40D1700338EB2 /* FetchGhostUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchGhostUseCase.swift; sourceTree = "<group>"; };
DD90EEAA2DC5F65100338EB2 /* CreateBannedUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateBannedUseCase.swift; sourceTree = "<group>"; };
DD90EEAC2DC5F6BB00338EB2 /* CreateReportUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateReportUseCase.swift; sourceTree = "<group>"; };
DD9980DA2D834CA900EBBFBC /* CommentInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentInfo.swift; sourceTree = "<group>"; };
DD9980DC2D834F4E00EBBFBC /* CommentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentCollectionViewCell.swift; sourceTree = "<group>"; };
DD9980DD2D834F4E00EBBFBC /* ContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentCollectionViewCell.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1161,6 +1167,9 @@
DD9EAE202D99C58600803A1A /* Home */ = {
isa = PBXGroup;
children = (
DD90EEAC2DC5F6BB00338EB2 /* CreateReportUseCase.swift */,
DD90EEAA2DC5F65100338EB2 /* CreateBannedUseCase.swift */,
DD90EEA72DC40D1700338EB2 /* FetchGhostUseCase.swift */,
DD5098192DA519C600F666DE /* FetchUserInformationUseCase.swift */,
DD5098132DA2C35C00F666DE /* DeleteCommentLikedUseCase.swift */,
DD5098112DA2C34900F666DE /* CreateCommentLikedUseCase.swift */,
Expand Down Expand Up @@ -2079,6 +2088,7 @@
DD2968322D6DAD1700143851 /* ContentRepositoryImpl.swift in Sources */,
DDCD66EC2D7DE5E000EF4C28 /* ConstraintMaker+.swift in Sources */,
DEAFE0512D8C44810097E91C /* GameScheduleListViewController.swift in Sources */,
DD90EEAB2DC5F66900338EB2 /* CreateBannedUseCase.swift in Sources */,
DE7C530C2D761DCA00076E5D /* ViewModelType.swift in Sources */,
DDCD66EA2D7DD72F00EF4C28 /* LottieType.swift in Sources */,
DD29683B2D6DAD2F00143851 /* CreateAccountResponse.swift in Sources */,
Expand Down Expand Up @@ -2121,6 +2131,7 @@
DD2968462D6DAD2F00143851 /* FetchAccountInfo.swift in Sources */,
DD2968472D6DAD2F00143851 /* FetchContent.swift in Sources */,
DD2968482D6DAD2F00143851 /* FetchNotificationNumber.swift in Sources */,
DD90EEA82DC40D1800338EB2 /* FetchGhostUseCase.swift in Sources */,
DD2968492D6DAD2F00143851 /* FetchContents.swift in Sources */,
DD5098102DA2C1C500F666DE /* DeleteCommentUseCase.swift in Sources */,
DD29684A2D6DAD2F00143851 /* FetchLCKTeamRankings.swift in Sources */,
Expand Down Expand Up @@ -2198,6 +2209,7 @@
DE3390352D8EF40B00638BB4 /* AnnouncementDetailViewController.swift in Sources */,
DDCD66FD2D7DF6D900EF4C28 /* ProfileViewController.swift in Sources */,
DEE5D6132D9126D8009E5A25 /* WableBadgeSegmentedControl.swift in Sources */,
DD90EEAD2DC5F6CC00338EB2 /* CreateReportUseCase.swift in Sources */,
DD005BAB2D80071E00B1661F /* CommentButton.swift in Sources */,
DEAFE05B2D8C4E930097E91C /* GameScheduleHeaderView.swift in Sources */,
DE7C53182D761F3A00076E5D /* ReuseIdentifiable.swift in Sources */,
Expand Down
8 changes: 8 additions & 0 deletions Wable-iOS/Domain/Entity/Opacity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,12 @@ struct Opacity: Hashable {
value -= 1
}
}

func reduced() -> Opacity {
var newValue = value
if newValue > Self.minValue {
newValue -= 1
}
return Opacity(value: newValue)
}
}
26 changes: 26 additions & 0 deletions Wable-iOS/Domain/UseCase/Home/CreateBannedUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// CreateBannedUseCase.swift
// Wable-iOS
//
// Created by YOUJIM on 5/3/25.
//


import Combine
import Foundation

final class CreateBannedUseCase {
private let repository: ReportRepository

init(repository: ReportRepository) {
self.repository = repository
}
}

// MARK: - Extension

extension CreateBannedUseCase {
func execute(memberID: Int, triggerType: TriggerType.Ban, triggerID: Int) -> AnyPublisher<Void, WableError> {
return repository.createBan(memberID: memberID, triggerType: triggerType, triggerID: triggerID)
}
}
26 changes: 26 additions & 0 deletions Wable-iOS/Domain/UseCase/Home/CreateReportUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// CreateReportUseCase.swift
// Wable-iOS
//
// Created by YOUJIM on 5/3/25.
//


import Combine
import Foundation

final class CreateReportUseCase {
private let repository: ReportRepository

init(repository: ReportRepository) {
self.repository = repository
}
}

// MARK: - Extension

extension CreateReportUseCase {
func execute(nickname: String, text: String) -> AnyPublisher<Void, WableError> {
return repository.createReport(nickname: nickname, text: text)
}
}
31 changes: 31 additions & 0 deletions Wable-iOS/Domain/UseCase/Home/FetchGhostUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// FetchGhostUseCase.swift
// Wable-iOS
//
// Created by YOUJIM on 5/2/25.
//


import Combine
import Foundation

final class FetchGhostUseCase {
private let repository: GhostRepository

init(repository: GhostRepository) {
self.repository = repository
}
}

// MARK: - Extension

extension FetchGhostUseCase {
func execute(type: PostType, targetID: Int, userID: Int) -> AnyPublisher<Void, WableError> {
return repository.postGhostReduction(
alarmTriggerType: type == .comment ? "commentGhost" : "contentGhost",
alarmTriggerID: targetID,
targetMemberID: userID,
reason: ""
)
}
}
14 changes: 12 additions & 2 deletions Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,18 @@ private extension HomeDetailViewController {
postType: .mine,
cellType: .detail,
likeButtonTapHandler: {
self.didContentHeartTappedSubject.send(cell.likeButton.isLiked)
})
self.didContentHeartTappedSubject.send(cell.likeButton.isLiked)
},
settingButtonTapHandler: {

},
profileImageViewTapHandler: {

},
ghostButtonTapHandler: {

}
Comment on lines +192 to +200
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에서는 반영하지 않았습니다 ~!!

)

cell.commentButton.addAction(UIAction(handler: { _ in
self.didCommentTappedSubject.send()
Expand Down
159 changes: 151 additions & 8 deletions Wable-iOS/Presentation/Home/View/HomeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,23 @@ final class HomeViewController: NavigationViewController {

// MARK: - Property

private var dataSource: DataSource?
var shouldShowLoadingScreen: Bool = false

private let viewModel: HomeViewModel
private let willAppearSubject = PassthroughSubject<Void, Never>()
private let didRefreshSubject = PassthroughSubject<Void, Never>()
private let didSelectedSubject = PassthroughSubject<Int, Never>()
private let didHeartTappedSubject = PassthroughSubject<(Int, Bool), Never>()
private let didGhostTappedSubject = PassthroughSubject<(Int, Int), Never>()
private let didDeleteTappedSubject = PassthroughSubject<Int, Never>()
private let didBannedTappedSubject = PassthroughSubject<(Int, Int), Never>()
private let didReportTappedSubject = PassthroughSubject<(String, String), Never>()
private let willDisplayLastItemSubject = PassthroughSubject<Void, Never>()
private let cancelBag: CancelBag
var shouldShowLoadingScreen: Bool = false

private var activeUserID: Int?
private var isActiveUserAdmin: Bool?
private var dataSource: DataSource?

// MARK: - UIComponent

Expand Down Expand Up @@ -78,7 +86,7 @@ final class HomeViewController: NavigationViewController {

shouldShowLoadingScreen ? showLoadingScreen() : nil

setupView( )
setupView()
setupConstraint()
setupDataSource()
setupAction()
Expand Down Expand Up @@ -128,7 +136,6 @@ private extension HomeViewController {
func setupView() {
navigationController?.navigationBar.isHidden = true


view.addSubviews(
collectionView,
plusButton,
Expand Down Expand Up @@ -158,10 +165,116 @@ private extension HomeViewController {
}

func setupDataSource() {
let homeCellRegistration = CellRegistration<ContentCollectionViewCell, Content> { cell, indexPath, itemID in
cell.configureCell(info: itemID.content.contentInfo, postType: .mine, likeButtonTapHandler: {
self.didHeartTappedSubject.send((itemID.content.id, cell.likeButton.isLiked))
})
let homeCellRegistration = CellRegistration<ContentCollectionViewCell, Content> { [weak self] cell, indexPath, itemID in
guard let self = self else { return }

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),
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),
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),
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: 프로필 구현되는 대로 추가적인 설정 필요
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)

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)
}
Comment on lines +260 to +262
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.

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

),
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)
}
)
}

dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, item in
Expand Down Expand Up @@ -199,11 +312,29 @@ private extension HomeViewController {
viewDidRefresh: didRefreshSubject.eraseToAnyPublisher(),
didSelectedItem: didSelectedSubject.eraseToAnyPublisher(),
didHeartTappedItem: didHeartTappedSubject.eraseToAnyPublisher(),
didGhostTappedItem: didGhostTappedSubject.eraseToAnyPublisher(),
didDeleteTappedItem: didDeleteTappedSubject.eraseToAnyPublisher(),
didBannedTappedItem: didBannedTappedSubject.eraseToAnyPublisher(),
didReportTappedItem: didReportTappedSubject.eraseToAnyPublisher(),
willDisplayLastItem: willDisplayLastItemSubject.eraseToAnyPublisher()
)

let output = viewModel.transform(input: input, cancelBag: cancelBag)

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)
Comment on lines +324 to +336
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를 추가하고 값을 받아온 이후에 이벤트를 발행해서 뷰를 다시 그릴까 하는 생각도 했는데 그러면 지금 구조를 유지해도 괜찮지 않나? 하는 생각이 들더라구요. 뭔가 놓치고 있는 것 같기도 합니다...

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


output.contents
.receive(on: DispatchQueue.main)
.sink { [weak self] contents in
Expand Down Expand Up @@ -250,6 +381,18 @@ private extension HomeViewController {
isLoadingMore ? self?.loadingIndicator.startAnimating() : self?.loadingIndicator.stopAnimating()
}
.store(in: cancelBag)

output.isReportSucceed
.receive(on: DispatchQueue.main)
.sink { isSucceed in
let toast = ToastView(
status: .complete,
message: "신고 접수가 완료되었어요.\n24시간 이내에 조치할 예정이예요."
)

isSucceed ? toast.show() : nil
}
.store(in: cancelBag)
}
}

Expand Down
Loading