diff --git a/SOPT-iOS/Projects/Data/Sources/Repository/RankingRepository.swift b/SOPT-iOS/Projects/Data/Sources/Repository/RankingRepository.swift index c5894517a..aa1261536 100644 --- a/SOPT-iOS/Projects/Data/Sources/Repository/RankingRepository.swift +++ b/SOPT-iOS/Projects/Data/Sources/Repository/RankingRepository.swift @@ -13,22 +13,39 @@ import Domain import Networks public class RankingRepository { - - private let rankService: RankService - private let cancelBag = CancelBag() - - public init(service: RankService) { - self.rankService = service - } + + private let rankService: RankService + private let cancelBag = CancelBag() + + public init(service: RankService) { + self.rankService = service + } } extension RankingRepository: RankingRepositoryInterface { - public func fetchRankingListModel(isCurrentGeneration: Bool) -> AnyPublisher<[Domain.RankingModel], Error> { - return self.rankService - .fetchRankingList(isCurrentGeneration: isCurrentGeneration) - .map({ entity in - entity.map { $0.toDomain() } - }) - .eraseToAnyPublisher() - } + public func fetchRankingListModel(isCurrentGeneration: Bool) -> AnyPublisher<[Domain.RankingModel], Error> { + return self.rankService + .fetchRankingList(isCurrentGeneration: isCurrentGeneration) + .map({ entity in + entity.map { $0.toDomain() } + }) + .eraseToAnyPublisher() + } + + public func fetchPartRanking() -> AnyPublisher<[Domain.PartRankingModel], Error> { + return self.rankService + .fetchPartRanking() + .map { + $0.map { $0.toDomain() } + }.eraseToAnyPublisher() + } + + public func fetchRankingListInPart(part: String) -> AnyPublisher<[RankingModel], Error> { + return self.rankService + .fetchRankingListInPart(part: part) + .map { + $0.map { $0.toDomain() } + } + .eraseToAnyPublisher() + } } diff --git a/SOPT-iOS/Projects/Data/Sources/Transform/PartRankingTransform.swift b/SOPT-iOS/Projects/Data/Sources/Transform/PartRankingTransform.swift new file mode 100644 index 000000000..c9eb91bd9 --- /dev/null +++ b/SOPT-iOS/Projects/Data/Sources/Transform/PartRankingTransform.swift @@ -0,0 +1,20 @@ +// +// PartRankingTransform.swift +// Data +// +// Created by Aiden.lee on 2024/04/05. +// Copyright © 2024 SOPT-iOS. All rights reserved. +// + +import Foundation + +import Core +import Domain +import Networks + +extension PartRankingEntity { + public func toDomain() -> PartRankingModel { + return .init(part: part, rank: rank, points: points) + } +} + diff --git a/SOPT-iOS/Projects/Domain/Sources/Model/PartRankingModel.swift b/SOPT-iOS/Projects/Domain/Sources/Model/PartRankingModel.swift new file mode 100644 index 000000000..ecea9322d --- /dev/null +++ b/SOPT-iOS/Projects/Domain/Sources/Model/PartRankingModel.swift @@ -0,0 +1,29 @@ +// +// PartRankingModel.swift +// Domain +// +// Created by Aiden.lee on 2024/04/05. +// Copyright © 2024 SOPT-iOS. All rights reserved. +// + +import Foundation + +public struct PartRankingModel: Hashable { + public let part: String + public let rank: Int + public let points: Int + + public init(part: String, rank: Int, points: Int) { + self.part = part + self.rank = rank + self.points = points + } +} + +public struct PartRankingChartModel: Hashable { + public let ranking: [PartRankingModel] + + public init(ranking: [PartRankingModel]) { + self.ranking = ranking + } +} diff --git a/SOPT-iOS/Projects/Domain/Sources/RepositoryInterface/RankingRepositoryInterface.swift b/SOPT-iOS/Projects/Domain/Sources/RepositoryInterface/RankingRepositoryInterface.swift index 105334ee3..18e849ffd 100644 --- a/SOPT-iOS/Projects/Domain/Sources/RepositoryInterface/RankingRepositoryInterface.swift +++ b/SOPT-iOS/Projects/Domain/Sources/RepositoryInterface/RankingRepositoryInterface.swift @@ -9,5 +9,7 @@ import Combine public protocol RankingRepositoryInterface { - func fetchRankingListModel(isCurrentGeneration: Bool) -> AnyPublisher<[RankingModel], Error> + func fetchRankingListModel(isCurrentGeneration: Bool) -> AnyPublisher<[RankingModel], Error> + func fetchPartRanking() -> AnyPublisher<[Domain.PartRankingModel], Error> + func fetchRankingListInPart(part: String) -> AnyPublisher<[RankingModel], Error> } diff --git a/SOPT-iOS/Projects/Domain/Sources/UseCase/RankingUseCase.swift b/SOPT-iOS/Projects/Domain/Sources/UseCase/RankingUseCase.swift index 6713cb7eb..c8d3b7ec5 100644 --- a/SOPT-iOS/Projects/Domain/Sources/UseCase/RankingUseCase.swift +++ b/SOPT-iOS/Projects/Domain/Sources/UseCase/RankingUseCase.swift @@ -11,57 +11,82 @@ import Combine import Core public protocol RankingUseCase { - func fetchRankingList(isCurrentGeneration: Bool) - func findMyRanking() - var rankingListModelFetched: CurrentValueSubject<[RankingModel], Error> { get } - var myRanking: PassthroughSubject<(section: Int, item: Int), Error> { get set } + func fetchRankingList(isCurrentGeneration: Bool) + func fetchRankingList(part: String) + func findMyRanking() + func fetchPartRanking() + + var rankingListModelFetched: CurrentValueSubject<[RankingModel], Error> { get } + var myRanking: PassthroughSubject<(section: Int, item: Int), Error> { get set } + var partRanking: PassthroughSubject<[PartRankingModel], Error> { get } } public class DefaultRankingUseCase { - - private let repository: RankingRepositoryInterface - private var cancelBag = CancelBag() - public var rankingListModelFetched = CurrentValueSubject<[RankingModel], Error>([]) - public var myRanking = PassthroughSubject<(section: Int, item: Int), Error>() - - public init(repository: RankingRepositoryInterface) { - self.repository = repository - } + + private let repository: RankingRepositoryInterface + private var cancelBag = CancelBag() + public var rankingListModelFetched = CurrentValueSubject<[RankingModel], Error>([]) + public var myRanking = PassthroughSubject<(section: Int, item: Int), Error>() + public let partRanking = PassthroughSubject<[PartRankingModel], Error>() + + public init(repository: RankingRepositoryInterface) { + self.repository = repository + } } extension DefaultRankingUseCase: RankingUseCase { - public func fetchRankingList(isCurrentGeneration: Bool) { - self.repository - .fetchRankingListModel(isCurrentGeneration: isCurrentGeneration) - .map { model in - var newModel = model - let myRankingIndex = self.findMyRankingIndex(model: model) - newModel[myRankingIndex].setMyRanking(true) - return newModel - } - .withUnretained(self) - .sink { completion in - print(completion) - } receiveValue: { owner, model in - owner.rankingListModelFetched.send(model) - }.store(in: self.cancelBag) - } - - public func findMyRanking() { - let myRankingIndex = self.findMyRankingIndex(model: rankingListModelFetched.value) - - if myRankingIndex > 2 { - myRanking.send((1, myRankingIndex - 3)) - } else { - myRanking.send((0, 0)) - } - } - - private func findMyRankingIndex(model: [RankingModel]) -> Int { - let myUserName = UserDefaultKeyList.User.soptampName - let index = model.firstIndex { model in - model.username == myUserName - } ?? 0 - return index + public func fetchRankingList(isCurrentGeneration: Bool) { + self.repository + .fetchRankingListModel(isCurrentGeneration: isCurrentGeneration) + .map { model in + var newModel = model + let myRankingIndex = self.findMyRankingIndex(model: model) + newModel[myRankingIndex].setMyRanking(true) + return newModel + } + .withUnretained(self) + .sink { completion in + print(completion) + } receiveValue: { owner, model in + owner.rankingListModelFetched.send(model) + }.store(in: self.cancelBag) + } + + public func fetchPartRanking() { + self.repository + .fetchPartRanking() + .sink { completion in + print(completion) + } receiveValue: { [weak self] rankingModels in + self?.partRanking.send(rankingModels) + }.store(in: cancelBag) + } + + public func fetchRankingList(part: String) { + self.repository + .fetchRankingListInPart(part: part) + .sink { completion in + print(completion) + } receiveValue: { [weak self] rankingModels in + self?.rankingListModelFetched.send(rankingModels) + }.store(in: cancelBag) + } + + public func findMyRanking() { + let myRankingIndex = self.findMyRankingIndex(model: rankingListModelFetched.value) + + if myRankingIndex > 2 { + myRanking.send((1, myRankingIndex - 3)) + } else { + myRanking.send((0, 0)) } + } + + private func findMyRankingIndex(model: [RankingModel]) -> Int { + let myUserName = UserDefaultKeyList.User.soptampName + let index = model.firstIndex { model in + model.username == myUserName + } ?? 0 + return index + } } diff --git a/SOPT-iOS/Projects/Features/StampFeature/Interface/Sources/Part.swift b/SOPT-iOS/Projects/Features/StampFeature/Interface/Sources/Part.swift new file mode 100644 index 000000000..56a9a4143 --- /dev/null +++ b/SOPT-iOS/Projects/Features/StampFeature/Interface/Sources/Part.swift @@ -0,0 +1,29 @@ +// +// Part.swift +// StampFeatureInterface +// +// Created by Aiden.lee on 2024/04/06. +// Copyright © 2024 SOPT-iOS. All rights reserved. +// + +import Foundation + +public enum Part: String, CaseIterable { + case plan = "기획" + case design = "디자인" + case web = "웹" + case ios = "아요" + case android = "안드" + case server = "서버" + + public func uppercasedName() -> String { + switch self { + case .plan: return "PLAN" + case .design: return "DESIGN" + case .web: return "WEB" + case .ios: return "IOS" + case .android: return "ANDROID" + case .server: return "SERVER" + } + } +} diff --git a/SOPT-iOS/Projects/Features/StampFeature/Interface/Sources/StampFeatureBuildable.swift b/SOPT-iOS/Projects/Features/StampFeature/Interface/Sources/StampFeatureBuildable.swift index 73639469a..8848820d1 100644 --- a/SOPT-iOS/Projects/Features/StampFeature/Interface/Sources/StampFeatureBuildable.swift +++ b/SOPT-iOS/Projects/Features/StampFeature/Interface/Sources/StampFeatureBuildable.swift @@ -13,6 +13,8 @@ import BaseFeatureDependency public enum RankingViewType { case all case currentGeneration(info: UsersActiveGenerationStatusViewResponse) + case partRanking + case individualRankingInPart(part: Part) } public protocol StampFeatureViewBuildable { @@ -29,5 +31,6 @@ public protocol StampFeatureViewBuildable { completionHandler: (() -> Void)? ) -> MissionCompletedViewControllable func makeRankingVC(rankingViewType: RankingViewType) -> RankingViewControllable + func makePartRankingVC(rankingViewType: RankingViewType) -> PartRankingViewControllable func makeStampGuideVC() -> StampGuideViewControllable } diff --git a/SOPT-iOS/Projects/Features/StampFeature/Interface/Sources/StampFeatureViewControllable.swift b/SOPT-iOS/Projects/Features/StampFeature/Interface/Sources/StampFeatureViewControllable.swift index b08602429..d652bffd9 100644 --- a/SOPT-iOS/Projects/Features/StampFeature/Interface/Sources/StampFeatureViewControllable.swift +++ b/SOPT-iOS/Projects/Features/StampFeature/Interface/Sources/StampFeatureViewControllable.swift @@ -12,22 +12,27 @@ import Domain public protocol MissionListViewControllable: ViewControllable & MissionListCoordinatable { } public protocol MissionListCoordinatable { - var onSwiped: (() -> Void)? { get set } - var onNaviBackTap: (() -> Void)? { get set } - var onRankingButtonTap: ((RankingViewType) -> Void)? { get set } - var onCurrentGenerationRankingButtonTap: ((RankingViewType) -> Void)? { get set } - var onGuideTap: (() -> Void)? { get set } - var onCellTap: ((MissionListModel, _ username: String?) -> Void)? { get set } + var onSwiped: (() -> Void)? { get set } + var onNaviBackTap: (() -> Void)? { get set } + var onPartRankingButtonTap: ((RankingViewType) -> Void)? { get set } + var onCurrentGenerationRankingButtonTap: ((RankingViewType) -> Void)? { get set } + var onGuideTap: (() -> Void)? { get set } + var onCellTap: ((MissionListModel, _ username: String?) -> Void)? { get set } } public protocol ListDetailViewControllable: ViewControllable & ListDetailCoordinatable { } public protocol ListDetailCoordinatable { - var onComplete: ((StarViewLevel, (() -> Void)?) -> Void)? { get set } + var onComplete: ((StarViewLevel, (() -> Void)?) -> Void)? { get set } } public protocol MissionCompletedViewControllable: ViewControllable { } public protocol RankingViewControllable: ViewControllable & RankingCoordinatable { } public protocol RankingCoordinatable { - var onCellTap: ((_ username: String, _ sentence: String) -> Void)? { get set } - var onSwiped: (() -> Void)? { get set } - var onNaviBackTap: (() -> Void)? { get set } + var onCellTap: ((_ username: String, _ sentence: String) -> Void)? { get set } + var onNaviBackTap: (() -> Void)? { get set } } + +public protocol PartRankingCoordinatable { + var onCellTap: ((_ part: Part) -> Void)? { get set } + var onNaviBackTap: (() -> Void)? { get set } +} +public protocol PartRankingViewControllable: ViewControllable & PartRankingCoordinatable { } public protocol StampGuideViewControllable: ViewControllable { } diff --git a/SOPT-iOS/Projects/Features/StampFeature/Sources/Components/STPartChartRectangleView.swift b/SOPT-iOS/Projects/Features/StampFeature/Sources/Components/STPartChartRectangleView.swift new file mode 100644 index 000000000..fe01ac667 --- /dev/null +++ b/SOPT-iOS/Projects/Features/StampFeature/Sources/Components/STPartChartRectangleView.swift @@ -0,0 +1,152 @@ +// +// STPartChartRectangleView.swift +// StampFeature +// +// Created by Aiden.lee on 2024/04/01. +// Copyright © 2024 SOPT-iOS. All rights reserved. +// + +import UIKit + +import Core +import DSKit + +import SnapKit + +public class STPartChartRectangleView: UIView { + + // MARK: - Properties + + public var rank: Int = 6 + public var partName: String = "파트" + + // MARK: - UI Components + + private let starRankView: UIImageView = { + let iv = UIImageView() + iv.contentMode = .scaleAspectFit + iv.image = DSKitAsset.Assets.icStar.image.withRenderingMode(.alwaysTemplate) + iv.tintColor = DSKitAsset.Colors.soptampPurple100.color + return iv + }() + + private let rankLabel: UILabel = { + let label = UILabel() + label.font = DSKitFontFamily.Montserrat.bold.font(size: 30) + return label + }() + + private let rectangleView: UIView = { + let view = UIView() + view.layer.cornerRadius = 8 + return view + }() + + private let partNameLabel: UILabel = { + let label = UILabel() + label.setTypoStyle(.MDS.body3) + label.textColor = .black + label.lineBreakMode = .byTruncatingTail + label.setCharacterSpacing(0) + return label + }() + + // MARK: View Life Cycle + + public init(rank: Int) { + self.init() + self.rank = rank + setUI() + setLayout() + } + + private override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - UI & Layouts + +extension STPartChartRectangleView { + + private func setUI() { + rankLabel.text = "\(rank)" + partNameLabel.text = partName + starRankView.isHidden = (rank > 3) + + if rank == 1 { + rankLabel.textColor = DSKitAsset.Colors.soptampPurple300.color + rectangleView.backgroundColor = DSKitAsset.Colors.soptampPurple200.color + starRankView.image = DSKitAsset.Assets.icStar.image.withRenderingMode(.alwaysTemplate) + } else if rank == 2 { + rankLabel.textColor = DSKitAsset.Colors.soptampPink300.color + rectangleView.backgroundColor = DSKitAsset.Colors.soptampPink200.color + starRankView.image = nil + } else if rank == 3 { + rankLabel.textColor = DSKitAsset.Colors.soptampMint300.color + rectangleView.backgroundColor = DSKitAsset.Colors.soptampMint200.color + starRankView.image = nil + } else { + rankLabel.text = "" + rectangleView.backgroundColor = DSKitAsset.Colors.soptampGray300.color + } + } + + private func setLayout() { + self.addSubviews(starRankView, rectangleView, partNameLabel) + starRankView.addSubview(rankLabel) + + starRankView.snp.makeConstraints { make in + make.bottom.equalTo(rectangleView.snp.top).offset(-13.adjusted) + make.centerX.equalToSuperview() + make.size.equalTo(50.adjusted) + } + + rankLabel.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.centerY.equalToSuperview().inset(3) + } + + rectangleView.snp.makeConstraints { make in + make.bottom.equalTo(partNameLabel.snp.top).offset(-10.adjustedH) + make.leading.trailing.equalToSuperview() + make.height.equalTo(self.calculateRectangleViewHeight()) + } + + partNameLabel.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.bottom.equalToSuperview() + make.width.lessThanOrEqualToSuperview() + } + } + + private func updateLayout() { + rectangleView.snp.updateConstraints { make in + make.height.equalTo(self.calculateRectangleViewHeight()) + } + } + + private func calculateRectangleViewHeight() -> CGFloat { + return 27.f * (7 - rank).f + } +} + +extension STPartChartRectangleView { + public func setData(rank: Int, partName: String) { + self.rank = rank + self.partName = partName + self.setUI() + self.updateLayout() + } +} + +extension STPartChartRectangleView { + static func == (left: STPartChartRectangleView, right: STPartChartRectangleView) -> Bool { + return left.rank == right.rank + } +} + diff --git a/SOPT-iOS/Projects/Features/StampFeature/Sources/Coordinator/RankingCoordinator.swift b/SOPT-iOS/Projects/Features/StampFeature/Sources/Coordinator/RankingCoordinator.swift index 241723c71..a5f4e26c6 100644 --- a/SOPT-iOS/Projects/Features/StampFeature/Sources/Coordinator/RankingCoordinator.swift +++ b/SOPT-iOS/Projects/Features/StampFeature/Sources/Coordinator/RankingCoordinator.swift @@ -31,24 +31,39 @@ final class RankingCoordinator: DefaultCoordinator { } public override func start() { - showRanking() + switch rankingViewType { + case .all, .currentGeneration, .individualRankingInPart: + showRanking(rankingViewType: rankingViewType) + case .partRanking: + showPartRanking() + } } - private func showRanking() { - var ranking = factory.makeRankingVC(rankingViewType: self.rankingViewType) - ranking.onSwiped = { [weak self] in - self?.router.popModule() - } + private func showRanking(rankingViewType: RankingViewType) { + var ranking = factory.makeRankingVC(rankingViewType: rankingViewType) + ranking.onCellTap = { [weak self] (username, sentence) in self?.showOtherMissionList(username, sentence) } ranking.onNaviBackTap = { [weak self] in self?.router.popModule() - self?.finishFlow?() } router.push(ranking) } - + + private func showPartRanking() { + var ranking = factory.makePartRankingVC(rankingViewType: self.rankingViewType) + + ranking.onCellTap = { [weak self] part in + self?.showRanking(rankingViewType: .individualRankingInPart(part: part)) + } + ranking.onNaviBackTap = { [weak self] in + self?.router.popModule() + self?.finishFlow?() + } + router.push(ranking) + } + private func showOtherMissionList(_ username: String, _ sentence: String) { var otherMissionList = factory.makeMissionListVC( sceneType: .ranking(userName: username, sentence: sentence) diff --git a/SOPT-iOS/Projects/Features/StampFeature/Sources/Coordinator/StampBuilder.swift b/SOPT-iOS/Projects/Features/StampFeature/Sources/Coordinator/StampBuilder.swift index 25d5e4643..85d072991 100644 --- a/SOPT-iOS/Projects/Features/StampFeature/Sources/Coordinator/StampBuilder.swift +++ b/SOPT-iOS/Projects/Features/StampFeature/Sources/Coordinator/StampBuilder.swift @@ -76,4 +76,11 @@ extension StampBuilder: StampFeatureViewBuildable { let stampGuideVC = StampGuideVC() return stampGuideVC } + + public func makePartRankingVC(rankingViewType: RankingViewType) -> PartRankingViewControllable { + let vc = PartRankingVC(rankingViewType: rankingViewType) + let useCase = DefaultRankingUseCase(repository: rankingRepository) + vc.viewModel = PartRankingViewModel(rankingViewType: rankingViewType, useCase: useCase) + return vc + } } diff --git a/SOPT-iOS/Projects/Features/StampFeature/Sources/Coordinator/StampCoordinator.swift b/SOPT-iOS/Projects/Features/StampFeature/Sources/Coordinator/StampCoordinator.swift index 91549afbc..ce2da2fee 100644 --- a/SOPT-iOS/Projects/Features/StampFeature/Sources/Coordinator/StampCoordinator.swift +++ b/SOPT-iOS/Projects/Features/StampFeature/Sources/Coordinator/StampCoordinator.swift @@ -41,7 +41,7 @@ final class StampCoordinator: DefaultCoordinator { missionList.onGuideTap = { [weak self] in self?.showGuide() } - missionList.onRankingButtonTap = { [weak self] rankingViewType in + missionList.onPartRankingButtonTap = { [weak self] rankingViewType in self?.runRankingFlow(rankingViewType: rankingViewType) } missionList.onCurrentGenerationRankingButtonTap = { [weak self] rankingViewType in @@ -74,7 +74,7 @@ final class StampCoordinator: DefaultCoordinator { addDependency(rankingCoordinator) rankingCoordinator.start() } - + private func runMissionDetailFlow(_ model: MissionListModel, _ username: String?) { let missionDetailCoordinator = MissionDetailCoordinator( router: Router(rootController: rootController!), diff --git a/SOPT-iOS/Projects/Features/StampFeature/Sources/MissionListScene/VC/MissionListVC.swift b/SOPT-iOS/Projects/Features/StampFeature/Sources/MissionListScene/VC/MissionListVC.swift index d2b38f868..d0ec0ad9c 100644 --- a/SOPT-iOS/Projects/Features/StampFeature/Sources/MissionListScene/VC/MissionListVC.swift +++ b/SOPT-iOS/Projects/Features/StampFeature/Sources/MissionListScene/VC/MissionListVC.swift @@ -20,422 +20,417 @@ import StampFeatureInterface import BaseFeatureDependency public class MissionListVC: UIViewController, MissionListViewControllable { - - // MARK: - Properties - - public var viewModel: MissionListViewModel! - public var sceneType: MissionListSceneType { - return self.viewModel.missionListsceneType - } - private var cancelBag = CancelBag() - - private var missionTypeMenuSelected = CurrentValueSubject(.all) - private var viewWillAppear = PassthroughSubject() - private let swipeHandler = PassthroughSubject() - - lazy var dataSource: UICollectionViewDiffableDataSource! = nil - - // MARK: - MissionListCoordinatable - - public var onSwiped: (() -> Void)? - public var onNaviBackTap: (() -> Void)? - public var onRankingButtonTap: ((RankingViewType) -> Void)? - public var onCurrentGenerationRankingButtonTap: ((RankingViewType) -> Void)? - public var onGuideTap: (() -> Void)? - public var onCellTap: ((MissionListModel, String?) -> Void)? - - private var usersActiveGenerationStatus: UsersActiveGenerationStatusViewResponse? - - // MARK: - UI Components - - lazy var naviBar: STNavigationBar = { - switch sceneType { - case .default: - return STNavigationBar(self, type: .title) - .setTitle("전체 미션") - .setTitleTypoStyle(.SoptampFont.h2) - .setTitleButtonMenu(menuItems: self.menuItems) - .addLeftButtonToTitleMenu() - case .ranking(let username, _): - return STNavigationBar(self, type: .titleWithLeftButton) - .setTitle(username) - .setRightButton(.none) - .setTitleTypoStyle(.SoptampFont.h2) - } - }() - - private lazy var menuItems: [UIAction] = { - var menuItems: [UIAction] = [] - [("전체 미션", MissionListFetchType.all), - ("완료 미션", MissionListFetchType.complete), - ("미완료 미션", MissionListFetchType.incomplete)].forEach { menuTitle, fetchType in - menuItems.append(UIAction(title: menuTitle, - handler: { _ in - self.missionTypeMenuSelected.send(fetchType) - self.naviBar.setTitle(menuTitle) - })) - } - return menuItems - }() - - private lazy var sentenceLabel: SentencePaddingLabel = { - let lb = SentencePaddingLabel() - if case let .ranking(_, sentence) = sceneType { - lb.text = sentence - } - lb.setTypoStyle(.SoptampFont.subtitle1) - lb.textColor = DSKitAsset.Colors.soptampGray900.color - lb.numberOfLines = 2 - lb.textAlignment = .center - lb.backgroundColor = DSKitAsset.Colors.soptampPurple100.color - lb.layer.cornerRadius = 9.adjustedH - lb.clipsToBounds = true - lb.setCharacterSpacing(0) - return lb - }() - - private lazy var missionListCollectionView: UICollectionView = { - let cv = UICollectionView(frame: .zero, collectionViewLayout: self.createLayout()) - cv.showsVerticalScrollIndicator = true - cv.backgroundColor = .white - cv.bounces = false - return cv - }() - - private let missionListEmptyView = MissionListEmptyView() - - private lazy var floatingButtonStackView = UIStackView(frame: self.view.frame).then { - $0.axis = .horizontal - $0.spacing = 0.f - $0.addArrangedSubviews(self.rankingFloatingButton) + + // MARK: - Properties + + public var viewModel: MissionListViewModel! + public var sceneType: MissionListSceneType { + return self.viewModel.missionListsceneType + } + private var cancelBag = CancelBag() + + private var missionTypeMenuSelected = CurrentValueSubject(.all) + private var viewWillAppear = PassthroughSubject() + private let swipeHandler = PassthroughSubject() + + lazy var dataSource: UICollectionViewDiffableDataSource! = nil + + // MARK: - MissionListCoordinatable + + public var onSwiped: (() -> Void)? + public var onNaviBackTap: (() -> Void)? + public var onPartRankingButtonTap: ((RankingViewType) -> Void)? + public var onCurrentGenerationRankingButtonTap: ((RankingViewType) -> Void)? + public var onGuideTap: (() -> Void)? + public var onCellTap: ((MissionListModel, String?) -> Void)? + + private var usersActiveGenerationStatus: UsersActiveGenerationStatusViewResponse? + + // MARK: - UI Components + + lazy var naviBar: STNavigationBar = { + switch sceneType { + case .default: + return STNavigationBar(self, type: .title) + .setTitle("전체 미션") + .setTitleTypoStyle(.SoptampFont.h2) + .setTitleButtonMenu(menuItems: self.menuItems) + .addLeftButtonToTitleMenu() + case .ranking(let username, _): + return STNavigationBar(self, type: .titleWithLeftButton) + .setTitle(username) + .setRightButton(.none) + .setTitleTypoStyle(.SoptampFont.h2) } - - private lazy var rankingFloatingButton: UIButton = { - let bt = UIButton() - bt.layer.cornerRadius = 27.adjustedH - bt.setBackgroundColor(DSKitAsset.Colors.soptampPurple300.color, for: .normal) - bt.setBackgroundColor(DSKitAsset.Colors.soptampPurple300.color.withAlphaComponent(0.2), for: .selected) - bt.setImage(DSKitAsset.Assets.icTrophy.image.withRenderingMode(.alwaysTemplate), for: .normal) - bt.setImage(DSKitAsset.Assets.icTrophy.image.withRenderingMode(.alwaysTemplate), for: .highlighted) - bt.tintColor = .white - bt.titleLabel?.setTypoStyle(.SoptampFont.h2) - let attributedStr = NSMutableAttributedString(string: "전체 랭킹") - let style = NSMutableParagraphStyle() - attributedStr.addAttribute(NSAttributedString.Key.kern, value: 0, range: NSMakeRange(0, attributedStr.length)) - attributedStr.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.white, range: NSMakeRange(0, attributedStr.length)) - bt.setAttributedTitle(attributedStr, for: .normal) - bt.contentEdgeInsets = UIEdgeInsets(top: 0, left: -15, bottom: 0, right: 0) - bt.titleEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0) - return bt - }() - - private lazy var currentGenerationRankFloatingButton: UIButton = { - let bt = UIButton() - bt.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner] - bt.layer.cornerRadius = 27.adjustedH - bt.setBackgroundColor(DSKitAsset.Colors.soptampPink300.color, for: .normal) - bt.setBackgroundColor(DSKitAsset.Colors.soptampPink300.color.withAlphaComponent(0.2), for: .selected) - bt.setImage(DSKitAsset.Assets.icTrophy.image.withRenderingMode(.alwaysTemplate), for: .normal) - bt.setImage(DSKitAsset.Assets.icTrophy.image.withRenderingMode(.alwaysTemplate), for: .highlighted) - bt.tintColor = .white - bt.titleLabel?.setTypoStyle(.SoptampFont.h2) - bt.contentEdgeInsets = UIEdgeInsets(top: 0, left: -15, bottom: 0, right: 0) - bt.titleEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0) - return bt - }() - - // MARK: - View Life Cycle - - public override func viewDidLoad() { - super.viewDidLoad() - self.setUI() - self.setLayout() - self.setDelegate() - self.setGesture() - self.registerCells() - self.setDataSource() - self.bindViews() - self.bindViewModels() + }() + + private lazy var menuItems: [UIAction] = { + var menuItems: [UIAction] = [] + [("전체 미션", MissionListFetchType.all), + ("완료 미션", MissionListFetchType.complete), + ("미완료 미션", MissionListFetchType.incomplete)].forEach { menuTitle, fetchType in + menuItems.append(UIAction(title: menuTitle, + handler: { _ in + self.missionTypeMenuSelected.send(fetchType) + self.naviBar.setTitle(menuTitle) + })) } - - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - self.viewWillAppear.send(()) - self.navigationController?.interactivePopGestureRecognizer?.delegate = self + return menuItems + }() + + private lazy var sentenceLabel: SentencePaddingLabel = { + let lb = SentencePaddingLabel() + if case let .ranking(_, sentence) = sceneType { + lb.text = sentence } + lb.setTypoStyle(.SoptampFont.subtitle1) + lb.textColor = DSKitAsset.Colors.soptampGray900.color + lb.numberOfLines = 2 + lb.textAlignment = .center + lb.backgroundColor = DSKitAsset.Colors.soptampPurple100.color + lb.layer.cornerRadius = 9.adjustedH + lb.clipsToBounds = true + lb.setCharacterSpacing(0) + return lb + }() + + private lazy var missionListCollectionView: UICollectionView = { + let cv = UICollectionView(frame: .zero, collectionViewLayout: self.createLayout()) + cv.showsVerticalScrollIndicator = true + cv.backgroundColor = .white + cv.bounces = false + return cv + }() + + private let missionListEmptyView = MissionListEmptyView() + + private lazy var floatingButtonStackView = UIStackView(frame: self.view.frame).then { + $0.axis = .horizontal + $0.spacing = 0.f + } + + private lazy var currentGenerationRankFloatingButton: UIButton = { + let bt = UIButton() + bt.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] + bt.layer.cornerRadius = 27.adjustedH + bt.setBackgroundColor(DSKitAsset.Colors.soptampPurple300.color, for: .normal) + bt.setBackgroundColor(DSKitAsset.Colors.soptampPurple300.color.withAlphaComponent(0.2), for: .selected) + bt.setImage(DSKitAsset.Assets.icTrophy.image.withRenderingMode(.alwaysTemplate), for: .normal) + bt.setImage(DSKitAsset.Assets.icTrophy.image.withRenderingMode(.alwaysTemplate), for: .highlighted) + bt.tintColor = .white + bt.titleLabel?.setTypoStyle(.SoptampFont.h2) + bt.contentEdgeInsets = UIEdgeInsets(top: 0, left: -15, bottom: 0, right: 0) + bt.titleEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0) + return bt + }() + + private lazy var partRankingFloatingButton: UIButton = { + let bt = UIButton() + bt.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner] + bt.layer.cornerRadius = 27.adjustedH + bt.setBackgroundColor(DSKitAsset.Colors.soptampPink300.color, for: .normal) + bt.setBackgroundColor(DSKitAsset.Colors.soptampPink300.color.withAlphaComponent(0.2), for: .selected) + bt.setImage(DSKitAsset.Assets.icTrophy.image.withRenderingMode(.alwaysTemplate), for: .normal) + bt.setImage(DSKitAsset.Assets.icTrophy.image.withRenderingMode(.alwaysTemplate), for: .highlighted) + bt.tintColor = .white + bt.titleLabel?.setTypoStyle(.SoptampFont.h2) + let attributedStr = NSMutableAttributedString(string: "파트별 랭킹") + let style = NSMutableParagraphStyle() + attributedStr.addAttribute(NSAttributedString.Key.kern, value: 0, range: NSMakeRange(0, attributedStr.length)) + attributedStr.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.white, range: NSMakeRange(0, attributedStr.length)) + bt.setAttributedTitle(attributedStr, for: .normal) + bt.contentEdgeInsets = UIEdgeInsets(top: 0, left: -15, bottom: 0, right: 0) + bt.titleEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0) + return bt + }() + + // MARK: - View Life Cycle + + public override func viewDidLoad() { + super.viewDidLoad() + self.setUI() + self.setLayout() + self.setDelegate() + self.setGesture() + self.registerCells() + self.setDataSource() + self.bindViews() + self.bindViewModels() + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.viewWillAppear.send(()) + self.navigationController?.interactivePopGestureRecognizer?.delegate = self + } } // MARK: - UI & Layouts extension MissionListVC { - - private func setUI() { - self.view.backgroundColor = .white - self.navigationController?.isNavigationBarHidden = true + + private func setUI() { + self.view.backgroundColor = .white + self.navigationController?.isNavigationBarHidden = true + } + + private func setLayout() { + self.view.addSubviews(naviBar, missionListCollectionView) + + naviBar.snp.makeConstraints { make in + make.leading.top.trailing.equalTo(view.safeAreaLayoutGuide) } - - private func setLayout() { - self.view.addSubviews(naviBar, missionListCollectionView) - - naviBar.snp.makeConstraints { make in - make.leading.top.trailing.equalTo(view.safeAreaLayoutGuide) - } - - missionListCollectionView.snp.makeConstraints { make in - make.top.equalTo(naviBar.snp.bottom).offset(20.adjustedH) - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - } - - switch sceneType { - case .default: - self.view.addSubview(self.floatingButtonStackView) - self.floatingButtonStackView.addArrangedSubview(self.rankingFloatingButton) - - self.floatingButtonStackView.snp.makeConstraints { make in - make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-18.adjustedH) - make.centerX.equalToSuperview() - } - - self.rankingFloatingButton.snp.makeConstraints { - $0.width.equalTo(143.adjusted) - $0.height.equalTo(54.adjustedH) - } - - self.currentGenerationRankFloatingButton.snp.makeConstraints { - $0.width.equalTo(143.adjusted) - $0.height.equalTo(54.adjustedH) - } - - case .ranking: - self.view.addSubview(sentenceLabel) - - sentenceLabel.snp.makeConstraints { make in - make.top.equalTo(naviBar.snp.bottom).offset(10.adjustedH) - make.leading.trailing.equalToSuperview().inset(20.adjusted) - make.height.equalTo(64.adjustedH) - } - - missionListCollectionView.snp.remakeConstraints { make in - make.top.equalTo(sentenceLabel.snp.bottom).offset(16.adjustedH) - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - } - } + + missionListCollectionView.snp.makeConstraints { make in + make.top.equalTo(naviBar.snp.bottom).offset(20.adjustedH) + make.leading.trailing.equalToSuperview() + make.bottom.equalToSuperview() } + + switch sceneType { + case .default: + self.view.addSubview(self.floatingButtonStackView) + + self.floatingButtonStackView.snp.makeConstraints { make in + make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-18.adjustedH) + make.centerX.equalToSuperview() + } + + self.partRankingFloatingButton.snp.makeConstraints { + $0.width.equalTo(143.adjusted) + $0.height.equalTo(54.adjustedH) + } + + self.currentGenerationRankFloatingButton.snp.makeConstraints { + $0.width.equalTo(143.adjusted) + $0.height.equalTo(54.adjustedH) + } + + case .ranking: + self.view.addSubview(sentenceLabel) + + sentenceLabel.snp.makeConstraints { make in + make.top.equalTo(naviBar.snp.bottom).offset(10.adjustedH) + make.leading.trailing.equalToSuperview().inset(20.adjusted) + make.height.equalTo(64.adjustedH) + } + + missionListCollectionView.snp.remakeConstraints { make in + make.top.equalTo(sentenceLabel.snp.bottom).offset(16.adjustedH) + make.leading.trailing.equalToSuperview() + make.bottom.equalToSuperview() + } + } + } } // MARK: - Methods extension MissionListVC { - private func bindViews() { - - naviBar.rightButtonTapped - .asDriver() - .withUnretained(self) - .sink { owner, _ in - owner.onGuideTap?() - }.store(in: self.cancelBag) - - if case .default = sceneType { - naviBar.leftButtonTapped - .withUnretained(self) - .sink { owner, _ in - owner.onNaviBackTap?() - }.store(in: self.cancelBag) - } - - rankingFloatingButton.publisher(for: .touchUpInside) - .withUnretained(self) - .sink { owner, _ in - owner.onRankingButtonTap?(.all) - }.store(in: self.cancelBag) - - currentGenerationRankFloatingButton.publisher(for: .touchUpInside) - .withUnretained(self) - .sink { owner, _ in - guard let usersActiveGenerationStatus = owner.usersActiveGenerationStatus else { return } - - owner.onCurrentGenerationRankingButtonTap?(.currentGeneration(info: usersActiveGenerationStatus)) - }.store(in: self.cancelBag) - - swipeHandler - .first() - .withUnretained(self) - .sink { owner, _ in - owner.onSwiped?() - }.store(in: self.cancelBag) - } - - private func bindViewModels() { - let input = MissionListViewModel.Input(viewWillAppear: viewWillAppear.asDriver(), - missionTypeSelected: missionTypeMenuSelected) - let output = self.viewModel.transform(from: input, cancelBag: self.cancelBag) - - output.$missionListModel - .compactMap { $0 } - .sink { [weak self] model in - self?.setCollectionView(model: model) - }.store(in: self.cancelBag) - - output.$usersActivateGenerationStatus - .compactMap { $0 } - .sink { [weak self] generationStatus in - guard generationStatus.status == .ACTIVE else { return } - - self?.usersActiveGenerationStatus = generationStatus - self?.remakeButtonConstraint() - self?.configureCurrentGenerationButton(with: String(describing: generationStatus.currentGeneration)) - }.store(in: self.cancelBag) + private func bindViews() { + + naviBar.rightButtonTapped + .asDriver() + .withUnretained(self) + .sink { owner, _ in + owner.onGuideTap?() + }.store(in: self.cancelBag) + + if case .default = sceneType { + naviBar.leftButtonTapped + .withUnretained(self) + .sink { owner, _ in + owner.onNaviBackTap?() + }.store(in: self.cancelBag) } + + partRankingFloatingButton.publisher(for: .touchUpInside) + .withUnretained(self) + .sink { owner, _ in + owner.onPartRankingButtonTap?(.partRanking) + }.store(in: self.cancelBag) + + currentGenerationRankFloatingButton.publisher(for: .touchUpInside) + .withUnretained(self) + .sink { owner, _ in + guard let usersActiveGenerationStatus = owner.usersActiveGenerationStatus else { return } + + owner.onCurrentGenerationRankingButtonTap?(.currentGeneration(info: usersActiveGenerationStatus)) + }.store(in: self.cancelBag) + + swipeHandler + .first() + .withUnretained(self) + .sink { owner, _ in + owner.onSwiped?() + }.store(in: self.cancelBag) + } + + private func bindViewModels() { + let input = MissionListViewModel.Input(viewWillAppear: viewWillAppear.asDriver(), + missionTypeSelected: missionTypeMenuSelected) + let output = self.viewModel.transform(from: input, cancelBag: self.cancelBag) + + output.$missionListModel + .compactMap { $0 } + .sink { [weak self] model in + self?.setCollectionView(model: model) + }.store(in: self.cancelBag) + + output.$usersActivateGenerationStatus + .compactMap { $0 } + .sink { [weak self] generationStatus in + guard generationStatus.status == .ACTIVE else { return } + + self?.usersActiveGenerationStatus = generationStatus + self?.remakeButtonConstraint() + self?.configureCurrentGenerationButton(with: String(describing: generationStatus.currentGeneration)) + }.store(in: self.cancelBag) + } } extension MissionListVC { - - private func setDelegate() { - missionListCollectionView.delegate = self - } - - private func setGesture() { - self.setGesture(to: missionListCollectionView) - self.setGesture(to: missionListEmptyView) - } - - private func setGesture(to view: UIView) { - let swipeGesture = UIPanGestureRecognizer(target: self, action: #selector(swipeBack(_:))) - swipeGesture.delegate = self - view.addGestureRecognizer(swipeGesture) - } - - @objc - private func swipeBack(_ sender: UIPanGestureRecognizer) { - let velocity = sender.velocity(in: self.view) - let velocityMinimum: CGFloat = 1000 - guard let navigation = self.navigationController else { return } - let isScrollY: Bool = abs(velocity.x) > abs(velocity.y) + 200 - let isNotRootView = navigation.viewControllers.count >= 2 - if velocity.x >= velocityMinimum - && isNotRootView - && isScrollY { - self.missionListCollectionView.isScrollEnabled = false - swipeHandler.send(()) - } - } - - private func registerCells() { - MissionListCVC.register(target: missionListCollectionView) - } - - private func setDataSource() { - dataSource = UICollectionViewDiffableDataSource(collectionView: missionListCollectionView, cellProvider: { collectionView, indexPath, itemIdentifier in - switch MissionListSection.type(indexPath.section) { - case .sentence: - guard let sentenceCell = collectionView.dequeueReusableCell(withReuseIdentifier: MissionListCVC.className, for: indexPath) as? MissionListCVC else { return UICollectionViewCell() } - return sentenceCell - - case .missionList: - guard let missionListCell = collectionView.dequeueReusableCell(withReuseIdentifier: MissionListCVC.className, for: indexPath) as? MissionListCVC else { return UICollectionViewCell() } - let missionListModel = itemIdentifier - missionListCell.initCellType = missionListModel.toCellType() - missionListCell.setData(model: missionListModel) - return missionListCell - } - }) - } - - func setCollectionView(model: [MissionListModel]) { - if model.isEmpty { - self.missionListCollectionView.isHidden = true - self.missionListEmptyView.isHidden = false - self.setEmptyView() - } else { - self.missionListCollectionView.isHidden = false - self.missionListEmptyView.isHidden = true - self.applySnapshot(model: model) - } - } - - private func setEmptyView() { - missionListEmptyView.snp.removeConstraints() - missionListEmptyView.removeFromSuperview() - self.view.addSubviews(missionListEmptyView) - missionListEmptyView.snp.makeConstraints { make in - make.top.equalTo(naviBar.snp.bottom).offset(145.adjustedH) - make.centerX.equalToSuperview() - make.width.equalToSuperview() - make.bottom.equalToSuperview().priority(.low) - } - bringRankingFloatingButtonToFront() - } - - private func bringRankingFloatingButtonToFront() { - self.view.subviews.forEach { view in - if view == self.rankingFloatingButton { - self.view.bringSubviewToFront(rankingFloatingButton) - } - } + + private func setDelegate() { + missionListCollectionView.delegate = self + } + + private func setGesture() { + self.setGesture(to: missionListCollectionView) + self.setGesture(to: missionListEmptyView) + } + + private func setGesture(to view: UIView) { + let swipeGesture = UIPanGestureRecognizer(target: self, action: #selector(swipeBack(_:))) + swipeGesture.delegate = self + view.addGestureRecognizer(swipeGesture) + } + + @objc + private func swipeBack(_ sender: UIPanGestureRecognizer) { + let velocity = sender.velocity(in: self.view) + let velocityMinimum: CGFloat = 1000 + guard let navigation = self.navigationController else { return } + let isScrollY: Bool = abs(velocity.x) > abs(velocity.y) + 200 + let isNotRootView = navigation.viewControllers.count >= 2 + if velocity.x >= velocityMinimum + && isNotRootView + && isScrollY { + self.missionListCollectionView.isScrollEnabled = false + swipeHandler.send(()) } - - private func applySnapshot(model: [MissionListModel]) { - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.sentence, .missionList]) - snapshot.appendItems(model, toSection: .missionList) - dataSource.apply(snapshot, animatingDifferences: false) - self.view.setNeedsLayout() + } + + private func registerCells() { + MissionListCVC.register(target: missionListCollectionView) + } + + private func setDataSource() { + dataSource = UICollectionViewDiffableDataSource(collectionView: missionListCollectionView, cellProvider: { collectionView, indexPath, itemIdentifier in + switch MissionListSection.type(indexPath.section) { + case .sentence: + guard let sentenceCell = collectionView.dequeueReusableCell(withReuseIdentifier: MissionListCVC.className, for: indexPath) as? MissionListCVC else { return UICollectionViewCell() } + return sentenceCell + + case .missionList: + guard let missionListCell = collectionView.dequeueReusableCell(withReuseIdentifier: MissionListCVC.className, for: indexPath) as? MissionListCVC else { return UICollectionViewCell() } + let missionListModel = itemIdentifier + missionListCell.initCellType = missionListModel.toCellType() + missionListCell.setData(model: missionListModel) + return missionListCell + } + }) + } + + func setCollectionView(model: [MissionListModel]) { + if model.isEmpty { + self.missionListCollectionView.isHidden = true + self.missionListEmptyView.isHidden = false + self.setEmptyView() + } else { + self.missionListCollectionView.isHidden = false + self.missionListEmptyView.isHidden = true + self.applySnapshot(model: model) } - - private func remakeButtonConstraint() { - self.rankingFloatingButton.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] - self.rankingFloatingButton.layer.cornerRadius = 27.f - self.rankingFloatingButton.layoutIfNeeded() - - self.floatingButtonStackView.addArrangedSubview(self.currentGenerationRankFloatingButton) + } + + private func setEmptyView() { + missionListEmptyView.snp.removeConstraints() + missionListEmptyView.removeFromSuperview() + self.view.addSubviews(missionListEmptyView) + missionListEmptyView.snp.makeConstraints { make in + make.top.equalTo(naviBar.snp.bottom).offset(145.adjustedH) + make.centerX.equalToSuperview() + make.width.equalToSuperview() + make.bottom.equalToSuperview().priority(.low) } - - private func configureCurrentGenerationButton(with generation: String) { - let attributedStr = NSMutableAttributedString(string: "\(generation)기 랭킹") - let style = NSMutableParagraphStyle() - attributedStr.addAttribute(NSAttributedString.Key.kern, value: 0, range: NSMakeRange(0, attributedStr.length)) - attributedStr.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.white, range: NSMakeRange(0, attributedStr.length)) - self.currentGenerationRankFloatingButton.setAttributedTitle(attributedStr, for: .normal) + bringRankingFloatingButtonToFront() + } + + private func bringRankingFloatingButtonToFront() { + self.view.subviews.forEach { view in + if view == self.partRankingFloatingButton { + self.view.bringSubviewToFront(partRankingFloatingButton) + } } + } + + private func applySnapshot(model: [MissionListModel]) { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.sentence, .missionList]) + snapshot.appendItems(model, toSection: .missionList) + dataSource.apply(snapshot, animatingDifferences: false) + self.view.setNeedsLayout() + } + + private func remakeButtonConstraint() { + self.floatingButtonStackView.addArrangedSubviews(self.currentGenerationRankFloatingButton, self.partRankingFloatingButton) + } + + private func configureCurrentGenerationButton(with generation: String) { + let attributedStr = NSMutableAttributedString(string: "\(generation)기 랭킹") + let style = NSMutableParagraphStyle() + attributedStr.addAttribute(NSAttributedString.Key.kern, value: 0, range: NSMakeRange(0, attributedStr.length)) + attributedStr.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.white, range: NSMakeRange(0, attributedStr.length)) + self.currentGenerationRankFloatingButton.setAttributedTitle(attributedStr, for: .normal) + } } // MARK: - UICollectionViewDelegate extension MissionListVC: UICollectionViewDelegate { - public func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { - switch indexPath.section { - case 0: - return false - case 1: - switch self.sceneType { - case .default: - return true - case .ranking: - return true - } - default: - return false - } + public func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { + switch indexPath.section { + case 0: + return false + case 1: + switch self.sceneType { + case .default: + return true + case .ranking: + return true + } + default: + return false } - - public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - switch indexPath.section { - case 0: - return - case 1: - guard let tappedCell = collectionView.cellForItem(at: indexPath) as? MissionListCVC, - let model = tappedCell.model else { return } - onCellTap?(model, sceneType.usrename) - default: - return - } + } + + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + switch indexPath.section { + case 0: + return + case 1: + guard let tappedCell = collectionView.cellForItem(at: indexPath) as? MissionListCVC, + let model = tappedCell.model else { return } + onCellTap?(model, sceneType.usrename) + default: + return } + } } extension MissionListVC: UIGestureRecognizerDelegate { - public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return true - } + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } } diff --git a/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/Cells/PartRankingChartCVC.swift b/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/Cells/PartRankingChartCVC.swift new file mode 100644 index 000000000..717224950 --- /dev/null +++ b/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/Cells/PartRankingChartCVC.swift @@ -0,0 +1,86 @@ +// +// PartRankingChartCVC.swift +// StampFeature +// +// Created by Aiden.lee on 2024/04/01. +// Copyright © 2024 SOPT-iOS. All rights reserved. +// + +import UIKit +import Combine + +import Core +import Domain +import DSKit + +import SnapKit + +final class PartRankingChartCVC: UICollectionViewCell, UICollectionViewRegisterable { + static var isFromNib: Bool = false + + // MARK: - Properties + + public var models: [PartRankingModel] = [] + + // MARK: - UI Components + + private let chartStackView: UIStackView = { + let st = UIStackView() + st.axis = .horizontal + st.spacing = 7 + st.distribution = .fillEqually + return st + }() + + // MARK: - View Life Cycles + + private override init(frame: CGRect) { + super.init(frame: frame) + self.setChartViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - UI & Layouts + +extension PartRankingChartCVC { + + private func setChartViews() { + self.addSubviews(chartStackView) + + (1...6).forEach { rank in + let rectangleView = STPartChartRectangleView(rank: rank) + rectangleView.snp.makeConstraints { make in + make.width.greaterThanOrEqualTo(40) + } + chartStackView.addArrangedSubview(rectangleView) + } + + chartStackView.snp.makeConstraints { make in + make.height.equalTo(250) + make.top.leading.trailing.bottom.equalToSuperview() + } + } +} + +// MARK: - Methods + +extension PartRankingChartCVC { + public func setData(model: PartRankingChartModel) { + let models = model.ranking + self.models = models + + self.setChartData(chartRectangleModel: models) + } + + private func setChartData(chartRectangleModel: [PartRankingModel]) { + for (index, rectangle) in chartStackView.arrangedSubviews.enumerated() { + guard let chartRectangle = rectangle as? STPartChartRectangleView else { return } + guard let model = chartRectangleModel[safe: index] else { return } + chartRectangle.setData(rank: model.rank, partName: model.part) + } + } +} diff --git a/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/Cells/PartRankingListCVC.swift b/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/Cells/PartRankingListCVC.swift new file mode 100644 index 000000000..38ac42d4a --- /dev/null +++ b/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/Cells/PartRankingListCVC.swift @@ -0,0 +1,116 @@ +// +// PartRankingListCVC.swift +// StampFeature +// +// Created by Aiden.lee on 2024/04/01. +// Copyright © 2024 SOPT-iOS. All rights reserved. +// + +import UIKit +import Combine + +import Core +import Domain +import DSKit + +import SnapKit + +final class PartRankingListCVC: UICollectionViewCell, UICollectionViewRegisterable { + + // MARK: - Properties + + static var isFromNib: Bool = false + var model: PartRankingModel? + + // MARK: - UI Components + + private let rankLabel: UILabel = { + let label = UILabel() + label.font = DSKitFontFamily.Montserrat.bold.font(size: 30.adjusted) + label.textColor = DSKitAsset.Colors.soptampGray500.color + label.textAlignment = .center + return label + }() + + private let partNameLabel: UILabel = { + let label = UILabel() + label.setTypoStyle(.SoptampFont.h3) + label.textColor = DSKitAsset.Colors.soptampGray800.color + label.lineBreakMode = .byTruncatingTail + label.setCharacterSpacing(0) + return label + }() + + private let scoreLabel: UILabel = { + let label = UILabel() + label.setTypoStyle(.SoptampFont.number2) + label.textColor = DSKitAsset.Colors.soptampGray400.color + return label + }() + + // MARK: - View Life Cycles + + private override init(frame: CGRect) { + super.init(frame: frame) + self.setUI() + self.setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - UI & Layouts + +extension PartRankingListCVC { + + public func setUI() { + self.backgroundColor = DSKitAsset.Colors.soptampGray50.color + self.layer.cornerRadius = 8 + } + + private func setLayout() { + self.addSubviews(rankLabel, partNameLabel, scoreLabel) + + rankLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.leading.equalToSuperview().inset(16.adjusted) + make.width.greaterThanOrEqualTo(53.adjusted) + } + + partNameLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.leading.equalTo(rankLabel.snp.trailing).offset(16.adjusted) + make.width.lessThanOrEqualTo(157.adjusted) + } + + scoreLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.trailing.equalToSuperview().inset(20.adjusted) + } + } +} + +// MARK: - Methods + +extension PartRankingListCVC { + + public func setData(model: PartRankingModel) { + self.model = model + rankLabel.text = String(model.rank) + partNameLabel.text = model.part + scoreLabel.text = "\(model.points)점" + scoreLabel.partFontChange(targetString: "점", + font: DSKitFontFamily.Pretendard.medium.font(size: 12)) + setDefaultRanking() + } + + private func setDefaultRanking() { + self.backgroundColor = DSKitAsset.Colors.soptampGray50.color + self.layer.borderColor = nil + self.layer.borderWidth = 0 + rankLabel.textColor = DSKitAsset.Colors.soptampGray500.color + scoreLabel.textColor = DSKitAsset.Colors.soptampGray400.color + } +} diff --git a/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/CollectionView/PartRankingCompositionalLayout.swift b/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/CollectionView/PartRankingCompositionalLayout.swift new file mode 100644 index 000000000..79f6cb6c1 --- /dev/null +++ b/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/CollectionView/PartRankingCompositionalLayout.swift @@ -0,0 +1,47 @@ +// +// PartRankingCompositionalLayout.swift +// StampFeature +// +// Created by Aiden.lee on 2024/03/31. +// Copyright © 2024 SOPT-iOS. All rights reserved. +// + +import UIKit + +extension PartRankingVC { + static let standardWidth = UIScreen.main.bounds.width - 40.adjusted + + func createLayout() -> UICollectionViewLayout { + return UICollectionViewCompositionalLayout { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in + switch RankingSection.type(sectionIndex) { + case .chart: return self.createChartSection() + case .list: return self.createListSection() + } + } + } + + private func createChartSection() -> NSCollectionLayoutSection { + let size = NSCollectionLayoutSize(widthDimension: .absolute(RankingVC.standardWidth), heightDimension: .estimated(250)) + let item = NSCollectionLayoutItem(layoutSize: size) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitem: item, count: 1) + + let section = NSCollectionLayoutSection(group: group) + section.contentInsets = .init(top: 0, leading: 20, bottom: 0, trailing: 20) + section.orthogonalScrollingBehavior = .none + return section + } + + private func createListSection() -> NSCollectionLayoutSection { + let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(60)) + let item = NSCollectionLayoutItem(layoutSize: size) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitem: item, count: 1) + + let section = NSCollectionLayoutSection(group: group) + section.contentInsets = .init(top: 28.adjustedH, leading: 20.adjusted, bottom: 60.adjustedH, trailing: 20.adjusted) + section.interGroupSpacing = .init(10.adjustedH) + section.orthogonalScrollingBehavior = .none + + return section + } +} + diff --git a/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/VC/PartRankingVC.swift b/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/VC/PartRankingVC.swift new file mode 100644 index 000000000..a9fa68108 --- /dev/null +++ b/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/VC/PartRankingVC.swift @@ -0,0 +1,194 @@ +// +// PartRankingVC.swift +// StampFeature +// +// Created by Aiden.lee on 2024/03/31. +// Copyright © 2024 SOPT-iOS. All rights reserved. +// + +import UIKit + +import Core +import Domain +import DSKit + +import Combine +import SnapKit +import Then + +import StampFeatureInterface +import BaseFeatureDependency + +public class PartRankingVC: UIViewController, PartRankingViewControllable { + + // MARK: - Properties + + public var viewModel: PartRankingViewModel! + private var cancelBag = CancelBag() + + lazy var dataSource: UICollectionViewDiffableDataSource! = nil + + // MARK: - RankingCoordinatable + + public var onCellTap: ((Part) -> Void)? + public var onNaviBackTap: (() -> Void)? + + // MARK: - UI Components + + lazy var naviBar = STNavigationBar(self, type: .titleWithLeftButton, ignorePopAction: true) + .setTitleTypoStyle(.SoptampFont.h2) + .setTitle("파트별 랭킹") + .setRightButton(.none) + + private lazy var rankingCollectionView: UICollectionView = { + let cv = UICollectionView(frame: .zero, collectionViewLayout: self.createLayout()) + cv.showsVerticalScrollIndicator = true + cv.backgroundColor = .white + cv.refreshControl = refresher + return cv + }() + + private let refresher: UIRefreshControl = { + let rf = UIRefreshControl() + return rf + }() + + // MARK: - View Life Cycle + private let rankingViewType: RankingViewType + + init(rankingViewType: RankingViewType) { + self.rankingViewType = rankingViewType + + super.init(nibName: nil, bundle: nil) + + guard case .currentGeneration(let info) = rankingViewType else { return } + + let navigationTitle = String(describing: info.currentGeneration) + "기 랭킹" + self.naviBar.setTitle(navigationTitle) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidLoad() { + super.viewDidLoad() + self.setUI() + self.setLayout() + self.setDataSource() + self.setDelegate() + self.registerCells() + self.bindViews() + self.bindViewModels() + } +} + +// MARK: - UI & Layouts + +extension PartRankingVC { + + private func setUI() { + self.view.backgroundColor = .white + self.navigationController?.isNavigationBarHidden = true + } + + private func setLayout() { + self.view.addSubviews(naviBar, rankingCollectionView) + + naviBar.snp.makeConstraints { make in + make.leading.top.trailing.equalTo(view.safeAreaLayoutGuide) + } + + rankingCollectionView.snp.makeConstraints { make in + make.top.equalTo(naviBar.snp.bottom).offset(4) + make.leading.trailing.equalToSuperview() + make.bottom.equalToSuperview() + } + } +} + +// MARK: - Methods + +extension PartRankingVC { + + private func bindViews() { + naviBar.leftButtonTapped + .withUnretained(self) + .sink { owner, _ in + owner.onNaviBackTap?() + }.store(in: cancelBag) + } + + private func bindViewModels() { + let refreshStarted = refresher.publisher(for: .valueChanged) + .mapVoid() + .asDriver() + + let input = PartRankingViewModel.Input( + viewDidLoad: Driver.just(()), + refreshStarted: refreshStarted + ) + + let output = self.viewModel.transform(from: input, cancelBag: self.cancelBag) + + output.partRanking + .receive(on: DispatchQueue.main) + .sink { [weak self] partRankingModels in + self?.applySnapshot(model: partRankingModels) + }.store(in: cancelBag) + } + + private func setDelegate() { + rankingCollectionView.delegate = self + } + + private func registerCells() { + PartRankingChartCVC.register(target: rankingCollectionView) + PartRankingListCVC.register(target: rankingCollectionView) + } + + private func setDataSource() { + dataSource = UICollectionViewDiffableDataSource(collectionView: rankingCollectionView, cellProvider: { collectionView, indexPath, itemIdentifier in + switch RankingSection.type(indexPath.section) { + case .chart: + guard let chartCell = collectionView.dequeueReusableCell(withReuseIdentifier: PartRankingChartCVC.className, for: indexPath) as? PartRankingChartCVC, + let chartCellModel = itemIdentifier as? PartRankingChartModel else { return UICollectionViewCell() } + chartCell.setData(model: chartCellModel) + return chartCell + + case .list: + guard let rankingListCell = collectionView.dequeueReusableCell(withReuseIdentifier: PartRankingListCVC.className, for: indexPath) as? PartRankingListCVC, + let model = itemIdentifier as? PartRankingModel else { return UICollectionViewCell() } + rankingListCell.setData(model: model) + + return rankingListCell + } + }) + } + + func applySnapshot(model: [PartRankingModel]) { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.chart, .list]) + let chartCellModel = PartRankingChartModel.init(ranking: model) + snapshot.appendItems([chartCellModel], toSection: .chart) + let listModels = model.sorted(by: { $0.rank < $1.rank }) + snapshot.appendItems(listModels, toSection: .list) + dataSource.apply(snapshot, animatingDifferences: false) + self.view.setNeedsLayout() + } + + private func endRefresh() { + self.refresher.endRefreshing() + } +} + +extension PartRankingVC: UICollectionViewDelegate { + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard indexPath.section >= 1 else { return } + + guard let tappedCell = collectionView.cellForItem(at: indexPath) as? PartRankingListCVC, + let model = tappedCell.model else { return } + guard let part = Part(rawValue: model.part) else { return } + self.onCellTap?(part) + } +} diff --git a/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/VC/RankingVC.swift b/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/VC/RankingVC.swift index a737d32f8..1581313fe 100644 --- a/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/VC/RankingVC.swift +++ b/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/VC/RankingVC.swift @@ -17,6 +17,7 @@ import SnapKit import Then import StampFeatureInterface +import BaseFeatureDependency public class RankingVC: UIViewController, RankingViewControllable { @@ -30,7 +31,6 @@ public class RankingVC: UIViewController, RankingViewControllable { // MARK: - RankingCoordinatable public var onCellTap: ((String, String) -> Void)? - public var onSwiped: (() -> Void)? public var onNaviBackTap: (() -> Void)? // MARK: - UI Components @@ -74,10 +74,15 @@ public class RankingVC: UIViewController, RankingViewControllable { super.init(nibName: nil, bundle: nil) - guard case .currentGeneration(let info) = rankingViewType else { return } - + if case .currentGeneration(let info) = rankingViewType { let navigationTitle = String(describing: info.currentGeneration) + "기 랭킹" self.naviBar.setTitle(navigationTitle) + } + + if case .individualRankingInPart(let part) = rankingViewType { + let navigationTitle = String(describing: part.rawValue) + " 랭킹" + self.naviBar.setTitle(navigationTitle) + } } required init?(coder: NSCoder) { @@ -89,7 +94,6 @@ public class RankingVC: UIViewController, RankingViewControllable { self.setUI() self.setLayout() self.setDelegate() - self.setGesture() self.registerCells() self.bindViews() self.bindViewModels() @@ -180,27 +184,6 @@ extension RankingVC { rankingCollectionView.delegate = self } - private func setGesture() { - let swipeGesture = UIPanGestureRecognizer(target: self, action: #selector(swipeBack(_:))) - swipeGesture.delegate = self - self.rankingCollectionView.addGestureRecognizer(swipeGesture) - } - - @objc - private func swipeBack(_ sender: UIPanGestureRecognizer) { - let velocity = sender.velocity(in: rankingCollectionView) - let velocityMinimum: CGFloat = 1000 - guard let navigation = self.navigationController else { return } - let isScrollY: Bool = abs(velocity.x) > abs(velocity.y) + 200 - let isNotRootView = navigation.viewControllers.count >= 2 - if velocity.x >= velocityMinimum - && isNotRootView - && isScrollY { - self.rankingCollectionView.isScrollEnabled = false - self.onSwiped?() - } - } - private func registerCells() { RankingChartCVC.register(target: rankingCollectionView) RankingListCVC.register(target: rankingCollectionView) diff --git a/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/ViewModel/PartRankingViewModel.swift b/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/ViewModel/PartRankingViewModel.swift new file mode 100644 index 000000000..496629f1b --- /dev/null +++ b/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/ViewModel/PartRankingViewModel.swift @@ -0,0 +1,66 @@ +// +// PartRankingViewModel.swift +// StampFeature +// +// Created by Aiden.lee on 2024/03/31. +// Copyright © 2024 SOPT-iOS. All rights reserved. +// + +import Combine + +import Core +import Domain +import StampFeatureInterface + +public class PartRankingViewModel: ViewModelType { + + private let useCase: RankingUseCase + private let rankingViewType: RankingViewType + private var cancelBag = CancelBag() + + // MARK: - Inputs + + public struct Input { + let viewDidLoad: Driver + let refreshStarted: Driver + } + + // MARK: - Outputs + + public class Output { + let partRanking = PassthroughSubject<[PartRankingModel], Never>() + } + + // MARK: - init + + public init( + rankingViewType: RankingViewType, + useCase: RankingUseCase + ) { + self.rankingViewType = rankingViewType + self.useCase = useCase + } +} + +extension PartRankingViewModel { + public func transform(from input: Input, cancelBag: Core.CancelBag) -> Output { + let output = Output() + self.bindOutput(output: output, cancelBag: cancelBag) + + input.viewDidLoad + .merge(with: input.refreshStarted) + .sink { [weak self] _ in + self?.useCase.fetchPartRanking() + }.store(in: cancelBag) + + return output + } + + private func bindOutput(output: Output, cancelBag: CancelBag) { + useCase.partRanking + .asDriver() + .sink { rankingModels in + output.partRanking.send(rankingModels) + }.store(in: cancelBag) + } +} diff --git a/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/ViewModel/RankingViewModel.swift b/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/ViewModel/RankingViewModel.swift index 22901c6db..3158c2519 100644 --- a/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/ViewModel/RankingViewModel.swift +++ b/SOPT-iOS/Projects/Features/StampFeature/Sources/RankingScene/ViewModel/RankingViewModel.swift @@ -55,8 +55,14 @@ extension RankingViewModel { .withUnretained(self) .sink { owner, _ in switch self.rankingViewType { - case .all: owner.useCase.fetchRankingList(isCurrentGeneration: false) - case .currentGeneration: owner.useCase.fetchRankingList(isCurrentGeneration: true) + case .all: + owner.useCase.fetchRankingList(isCurrentGeneration: false) + case .currentGeneration: + owner.useCase.fetchRankingList(isCurrentGeneration: true) + case .individualRankingInPart(let part): + owner.useCase.fetchRankingList(part: part.uppercasedName()) + default: + return } }.store(in: self.cancelBag) diff --git a/SOPT-iOS/Projects/Modules/Networks/Sources/API/RankAPI.swift b/SOPT-iOS/Projects/Modules/Networks/Sources/API/RankAPI.swift index 3c425713d..106442493 100644 --- a/SOPT-iOS/Projects/Modules/Networks/Sources/API/RankAPI.swift +++ b/SOPT-iOS/Projects/Modules/Networks/Sources/API/RankAPI.swift @@ -12,69 +12,75 @@ import Alamofire import Moya public enum RankAPI { - case rank - case currentRank - case rankDetail(userName: String) + case rank + case currentRank + case rankDetail(userName: String) + case rankPart + case currentRankInPart(partName: String) } extension RankAPI: BaseAPI { - - public static var apiType: APIType = .rank - - // MARK: - Header - public var headers: [String: String]? { - switch self { - case .rank, .rankDetail: - return HeaderType.jsonWithToken.value - default: return HeaderType.json.value - } + + public static var apiType: APIType = .rank + + // MARK: - Header + public var headers: [String: String]? { + switch self { + case .rank, .rankDetail: + return HeaderType.jsonWithToken.value + default: return HeaderType.json.value } - - // MARK: - Path - public var path: String { - switch self { - case .rank: - return "" - case .currentRank: - return "/current" - case .rankDetail: - return "/detail" - } + } + + // MARK: - Path + public var path: String { + switch self { + case .rank: + return "" + case .currentRank: + return "/current" + case .rankDetail: + return "/detail" + case .rankPart: + return "/part" + case .currentRankInPart(let partName): + return "/current/part/\(partName)" } - - // MARK: - Method - public var method: Moya.Method { - switch self { - default: return .get - } + } + + // MARK: - Method + public var method: Moya.Method { + switch self { + default: return .get } - - // MARK: - Parameters - private var bodyParameters: Parameters? { - var params: Parameters = [:] - switch self { - case .rankDetail(let userName): - params["nickname"] = userName - default: break - } - return params + } + + // MARK: - Parameters + private var bodyParameters: Parameters? { + var params: Parameters = [:] + switch self { + case .rankDetail(let userName): + params["nickname"] = userName + default: break } - - private var parameterEncoding: ParameterEncoding { - switch self { - case .rankDetail: - return URLEncoding.default - default: - return JSONEncoding.default - } + return params + } + + private var parameterEncoding: ParameterEncoding { + switch self { + case .rankDetail: + return URLEncoding.default + default: + return JSONEncoding.default } - - public var task: Task { - switch self { - case .rankDetail: - return .requestParameters(parameters: bodyParameters ?? [:], encoding: parameterEncoding) - default: - return .requestPlain - } + } + + public var task: Task { + switch self { + case .rankDetail: + return .requestParameters(parameters: bodyParameters ?? [:], encoding: parameterEncoding) + default: + return .requestPlain } + } } diff --git a/SOPT-iOS/Projects/Modules/Networks/Sources/Entity/PartRankingEntity.swift b/SOPT-iOS/Projects/Modules/Networks/Sources/Entity/PartRankingEntity.swift new file mode 100644 index 000000000..846f430fd --- /dev/null +++ b/SOPT-iOS/Projects/Modules/Networks/Sources/Entity/PartRankingEntity.swift @@ -0,0 +1,15 @@ +// +// PartRankingEntity.swift +// Networks +// +// Created by Aiden.lee on 2024/04/05. +// Copyright © 2024 SOPT-iOS. All rights reserved. +// + +import Foundation + +public struct PartRankingEntity: Decodable { + public let part: String + public let rank: Int + public let points: Int +} diff --git a/SOPT-iOS/Projects/Modules/Networks/Sources/Service/RankService.swift b/SOPT-iOS/Projects/Modules/Networks/Sources/Service/RankService.swift index 38c07fbed..fdb0b9e76 100644 --- a/SOPT-iOS/Projects/Modules/Networks/Sources/Service/RankService.swift +++ b/SOPT-iOS/Projects/Modules/Networks/Sources/Service/RankService.swift @@ -15,16 +15,26 @@ import Moya public typealias DefaultRankService = BaseService public protocol RankService { - func fetchRankingList(isCurrentGeneration: Bool) -> AnyPublisher<[RankingEntity], Error> - func fetchRankDetail(userName: String) -> AnyPublisher + func fetchRankingList(isCurrentGeneration: Bool) -> AnyPublisher<[RankingEntity], Error> + func fetchRankDetail(userName: String) -> AnyPublisher + func fetchPartRanking() -> AnyPublisher<[PartRankingEntity], Error> + func fetchRankingListInPart(part: String) -> AnyPublisher<[RankingEntity], Error> } extension DefaultRankService: RankService { - public func fetchRankingList(isCurrentGeneration: Bool) -> AnyPublisher<[RankingEntity], Error> { - requestObjectInCombine(isCurrentGeneration ? RankAPI.currentRank : RankAPI.rank) - } - - public func fetchRankDetail(userName: String) -> AnyPublisher { - requestObjectInCombine(RankAPI.rankDetail(userName: userName)) - } + public func fetchRankingList(isCurrentGeneration: Bool) -> AnyPublisher<[RankingEntity], Error> { + requestObjectInCombine(isCurrentGeneration ? RankAPI.currentRank : RankAPI.rank) + } + + public func fetchRankDetail(userName: String) -> AnyPublisher { + requestObjectInCombine(RankAPI.rankDetail(userName: userName)) + } + + public func fetchPartRanking() -> AnyPublisher<[PartRankingEntity], Error> { + requestObjectInCombine(.rankPart) + } + + public func fetchRankingListInPart(part: String) -> AnyPublisher<[RankingEntity], Error> { + requestObjectInCombine(.currentRankInPart(partName: part)) + } }