3개의 API 모두 viewModel로부터 분리
→ View에 보여줄 데이터를 가공한다는 의미에서는 ViewModel에 존재하는 게 맞지만, 여러 곳에서 필요한 로직이기도 하고, 이를 UseCase에서 진행하는 것이 역할 분리가 더 명확해질 것이라는 판단을 내려 UseCase로 이사햇음
extension VoteUseCase {
private func filterSelectedResult(voteInfoList: [VoteInfoModel]) -> (agree: [VoteInfoModel], disagree: [VoteInfoModel]) {
return (voteInfoList.filter { $0.isAgree }, voteInfoList.filter { !$0.isAgree })
}
// 투표 결과의 상위 2가지를 계산
private func getTopConsumerTypes(for votes: [VoteInfoModel]) -> [ConsumerType] {
return Dictionary(grouping: votes, by: { $0.consumerType })
.sorted { $0.value.count > $1.value.count }
.prefix(2)
.map { ConsumerType(rawValue: $0.key) }
.compactMap { $0 }
}
// 투표 비율을 계산
private func calculateRatio(for count: Int?, totalCount: Int?) -> Double {
guard let count = count, let totalCount = totalCount, totalCount > 0 else { return 0 }
return (Double(count) / Double(totalCount)) * 100
}
}
func vote(postId: Int, myChoice: Bool) -> AnyPublisher<(Double, Double), WoteError> {
voteRepository.vote(postId: postId, myChoice: myChoice)
.map{ voteCounts in
let total = voteCounts.agreeCount + voteCounts.disagreeCount
let agreeRatio = self.calculateRatio(for: voteCounts.agreeCount, totalCount: total)
let disagreeRatio = self.calculateRatio(for: voteCounts.disagreeCount, totalCount: total)
return (agreeRatio, disagreeRatio)
}
.eraseToAnyPublisher()
}
위와 같은 내용을 활용하여 ViewModel을 리팩토링했다.
import Combine
import SwiftUI
final class DetailViewModel: ObservableObject {
private var appLoginState: AppLoginState
private var cancellables: Set<AnyCancellable> = []
@Published var isMine = false
@Published var error: NetworkError?
@Published var postDetail: PostDetailModel?
@Published var agreeTopConsumerTypes = [ConsumerType]()
@Published var disagreeTopConsumerTypes = [ConsumerType]()
init(appLoginState: AppLoginState) {
self.appLoginState = appLoginState
}
func fetchPostDetail(postId: Int) {
appLoginState.serviceRoot.apimanager
.request(.postService(.getPostDetail(postId: postId)),
decodingType: PostDetailModel.self)
.compactMap(\.data)
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .finished:
break
case .failure(let failure):
if failure == .noMember || failure == .noPost {
self.error = failure
}
}
} receiveValue: { data in
self.postDetail = data
guard let isMine = data.post.isMine else { return }
self.isMine = isMine
self.setTopConsumerTypes()
}
.store(in: &cancellables)
}
func votePost(postId: Int,
choice: Bool,
index: Int?) {
appLoginState.serviceRoot.apimanager
.request(.postService(.votePost(postId: postId, choice: choice)),
decodingType: VoteCountsModel.self)
.compactMap(\.data)
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .finished:
break
case .failure(let failure):
if failure == .noMember || failure == .noPost {
self.error = failure
}
}
} receiveValue: { data in
if let postIndex = index {
self.updatePost(index: postIndex,
myChoice: choice,
voteCount: data)
}
self.fetchPostDetail(postId: postId)
}
.store(in: &cancellables)
}
func deletePost(postId: Int) {
appLoginState.serviceRoot.apimanager
.request(.postService(.deletePost(postId: postId)),
decodingType: NoData.self)
.sink { completion in
switch completion {
case .finished:
break
case .failure(let error):
print("error: \(error)")
}
} receiveValue: { _ in
// self.appLoginState.appData.postManager.deleteReviews(postId: postId)
// self.appLoginState.appData.postManager.removeCount += 1
NotificationCenter.default.post(name: NSNotification.voteStateUpdated, object: nil)
}
.store(in: &cancellables)
}
func closePost(postId: Int, index: (Int?, Int?)) {
appLoginState.serviceRoot.apimanager
.request(.postService(.closeVote(postId: postId)),
decodingType: NoData.self)
.sink { completion in
switch completion {
case .finished:
break
case .failure(let error):
print("error: \(error)")
}
} receiveValue: { _ in
if let postIndex = index.0 {
self.appLoginState.appData.postManager.posts[postIndex].postStatus
= PostStatus.closed.rawValue
}
if let myPostIndex = index.1 {
self.appLoginState.appData.postManager.myPosts[myPostIndex].postStatus
= PostStatus.closed.rawValue
}
self.fetchPostDetail(postId: postId)
}
.store(in: &cancellables)
}
func calculatVoteRatio(voteCounts: VoteCountsModel?) -> (agree: Double, disagree: Double) {
guard let voteCounts = voteCounts else { return (0.0, 0.0) }
let voteCount = voteCounts.agreeCount + voteCounts.disagreeCount
guard voteCount != 0 else { return (0, 0) }
let agreeRatio = Double(voteCounts.agreeCount) / Double(voteCount) * 100
return (agreeRatio, 100 - agreeRatio)
}
private func setTopConsumerTypes() {
guard let voteInfoList = postDetail?.post.voteInfoList else { return }
let (agreeVoteInfos, disagreeVoteInfos) = filterSelectedResult(voteInfoList: voteInfoList)
agreeTopConsumerTypes = getTopConsumerTypes(for: agreeVoteInfos)
disagreeTopConsumerTypes = getTopConsumerTypes(for: disagreeVoteInfos)
}
private func filterSelectedResult(voteInfoList: [VoteInfoModel]) -> (agree: [VoteInfoModel],
disagree: [VoteInfoModel]) {
return (voteInfoList.filter { $0.isAgree }, voteInfoList.filter { !$0.isAgree })
}
private func getTopConsumerTypes(for votes: [VoteInfoModel]) -> [ConsumerType] {
return Dictionary(grouping: votes, by: { $0.consumerType })
.sorted { $0.value.count > $1.value.count }
.prefix(2)
.map { ConsumerType(rawValue: $0.key) }
.compactMap { $0 }
}
}
final class DetailViewModel: ObservableObject {
enum Action {
case loadDetail
case vote(_ myChoice: Bool)
case deleteVote
case closeVote
}
@Published var agreeTopConsumerTypes: [ConsumerType]?
@Published var disagreeTopConsumerTypes: [ConsumerType]?
@Published var comments: CommentsModel?
@Published var voteDetail: VoteDetailModel?
@Published var isVoteResultShowed: Bool = false
@Published var isVoteConsumerTypeResultShowed: Bool = false
@Published var agreeRatio: Double?
@Published var disagreeRatio: Double?
private let postId: Int
private let voteUseCase: VoteUseCaseType
init(postId: Int, voteUseCase: VoteUseCaseType) {
self.postId = postId
self.voteUseCase = voteUseCase
}
private var cancellables: Set<AnyCancellable> = []
func send(action: Action) {
switch action {
case .loadDetail:
voteUseCase.loadVoteDetail(postId: postId)
.sink { completion in
} receiveValue: { [weak self] voteDetail in
self?.voteDetail = voteDetail
self?.agreeRatio = voteDetail.post.agreeRatio
self?.disagreeRatio = voteDetail.post.disagreeRatio
self?.agreeTopConsumerTypes = voteDetail.agreeTopConsumers
self?.disagreeTopConsumerTypes = voteDetail.disagreeTopConsumers
if voteDetail.post.postStatus == "CLOSED" || voteDetail.post.myChoice != nil {
self?.isVoteResultShowed = true
}
if voteDetail.post.voteCount ?? 0 > 0 { self?.isVoteConsumerTypeResultShowed = true }
}
.store(in: &cancellables)
case let .vote(myChoice):
voteUseCase.vote(postId: postId, myChoice: myChoice)
.sink { completion in
} receiveValue: { [weak self] _ in
self?.send(action: .loadDetail)
}
.store(in: &cancellables)
case .deleteVote:
return
case .closeVote:
return
}
}
}
(투표 삭제와 종료는 아직 구현되지 않았다.)
오늘의 일지 끗