diff --git a/SOPT-iOS/Projects/Data/Sources/Repository/PokeMainRepository.swift b/SOPT-iOS/Projects/Data/Sources/Repository/PokeMainRepository.swift new file mode 100644 index 00000000..7501b1ff --- /dev/null +++ b/SOPT-iOS/Projects/Data/Sources/Repository/PokeMainRepository.swift @@ -0,0 +1,43 @@ +// +// PokeMainRepository.swift +// Data +// +// Created by sejin on 12/19/23. +// Copyright © 2023 SOPT-iOS. All rights reserved. +// + +import Combine + +import Core +import Domain +import Networks + +public class PokeMainRepository { + + private let pokeService: PokeService + private let cancelBag = CancelBag() + + public init(service: PokeService) { + self.pokeService = service + } +} + +extension PokeMainRepository: PokeMainRepositoryInterface { + public func getWhoPokeToMe() -> AnyPublisher { + pokeService.getWhoPokedToMe() + .map { $0?.toDomain() } + .eraseToAnyPublisher() + } + + public func getFriend() -> AnyPublisher<[PokeUserModel], Error> { + pokeService.getFriend() + .map { $0.map { $0.toDomain() } } + .eraseToAnyPublisher() + } + + public func getFriendRandomUser() -> AnyPublisher<[PokeFriendRandomUserModel], Error> { + pokeService.getFriendRandomUser() + .map { $0.map { $0.toDomain() } } + .eraseToAnyPublisher() + } +} diff --git a/SOPT-iOS/Projects/Data/Sources/Transform/PokeFriendRandomUserTransform.swift b/SOPT-iOS/Projects/Data/Sources/Transform/PokeFriendRandomUserTransform.swift new file mode 100644 index 00000000..f4a9509f --- /dev/null +++ b/SOPT-iOS/Projects/Data/Sources/Transform/PokeFriendRandomUserTransform.swift @@ -0,0 +1,21 @@ +// +// PokeFriendRandomUserTransform.swift +// Data +// +// Created by sejin on 12/20/23. +// Copyright © 2023 SOPT-iOS. All rights reserved. +// + +import Foundation + +import Domain +import Networks + +extension PokeFriendRandomUserEntity { + public func toDomain() -> PokeFriendRandomUserModel { + return PokeFriendRandomUserModel(friendId: friendId, + friendName: friendName, + friendProfileImage: friendProfileImage, + friendList: friendList.map { $0.toDomain() }) + } +} diff --git a/SOPT-iOS/Projects/Data/Sources/Transform/PokeUserTransform.swift b/SOPT-iOS/Projects/Data/Sources/Transform/PokeUserTransform.swift new file mode 100644 index 00000000..f0a76dd9 --- /dev/null +++ b/SOPT-iOS/Projects/Data/Sources/Transform/PokeUserTransform.swift @@ -0,0 +1,29 @@ +// +// PokeUserTransform.swift +// Data +// +// Created by sejin on 12/19/23. +// Copyright © 2023 SOPT-iOS. All rights reserved. +// + +import Foundation + +import Domain +import Networks + +extension PokeUserEntity { + public func toDomain() -> PokeUserModel { + return PokeUserModel(userId: userId, + playgroundId: playgroundId, + profileImage: profileImage, + name: name, + generation: generation, + part: part, + pokeNum: pokeNum, + message: message, + relationName: relationName, + mutual: mutual, + isFirstMeet: isFirstMeet, + isAlreadyPoke: isAlreadyPoke) + } +} diff --git a/SOPT-iOS/Projects/Demo/Sources/Dependency/RegisterDependencies.swift b/SOPT-iOS/Projects/Demo/Sources/Dependency/RegisterDependencies.swift index b834f131..eb8de3fa 100644 --- a/SOPT-iOS/Projects/Demo/Sources/Dependency/RegisterDependencies.swift +++ b/SOPT-iOS/Projects/Demo/Sources/Dependency/RegisterDependencies.swift @@ -136,5 +136,12 @@ extension AppDelegate { ) } ) + container.register(interface: PokeMainRepositoryInterface.self, + implement: { + PokeMainRepository( + service: DefaultPokeService() + ) + } + ) } } diff --git a/SOPT-iOS/Projects/Domain/Sources/Model/PokeFriendRandomUserModel.swift b/SOPT-iOS/Projects/Domain/Sources/Model/PokeFriendRandomUserModel.swift new file mode 100644 index 00000000..2610cad2 --- /dev/null +++ b/SOPT-iOS/Projects/Domain/Sources/Model/PokeFriendRandomUserModel.swift @@ -0,0 +1,22 @@ +// +// PokeFriendRandomUserModel.swift +// Domain +// +// Created by sejin on 12/20/23. +// Copyright © 2023 SOPT-iOS. All rights reserved. +// + +import Foundation + +public struct PokeFriendRandomUserModel { + public let friendId: Int + public let friendName, friendProfileImage: String + public let friendList: [PokeUserModel] + + public init(friendId: Int, friendName: String, friendProfileImage: String, friendList: [PokeUserModel]) { + self.friendId = friendId + self.friendName = friendName + self.friendProfileImage = friendProfileImage + self.friendList = friendList + } +} diff --git a/SOPT-iOS/Projects/Domain/Sources/Model/PokeUserModel.swift b/SOPT-iOS/Projects/Domain/Sources/Model/PokeUserModel.swift new file mode 100644 index 00000000..8783eadd --- /dev/null +++ b/SOPT-iOS/Projects/Domain/Sources/Model/PokeUserModel.swift @@ -0,0 +1,38 @@ +// +// PokeUserModel.swift +// Domain +// +// Created by sejin on 12/19/23. +// Copyright © 2023 SOPT-iOS. All rights reserved. +// + +import Foundation + +// MARK: - Empty +public struct PokeUserModel: Codable { + public let userId: Int + public let playgroundId: Int + public let profileImage, name: String + public let generation: Int + public let part: String + public let pokeNum: Int + public let message: String + public let relationName: String + public let mutual: [String] + public let isFirstMeet, isAlreadyPoke: Bool + + public init(userId: Int, playgroundId: Int, profileImage: String, name: String, generation: Int, part: String, pokeNum: Int, message: String, relationName: String, mutual: [String], isFirstMeet: Bool, isAlreadyPoke: Bool) { + self.userId = userId + self.playgroundId = playgroundId + self.profileImage = profileImage + self.name = name + self.generation = generation + self.part = part + self.pokeNum = pokeNum + self.message = message + self.relationName = relationName + self.mutual = mutual + self.isFirstMeet = isFirstMeet + self.isAlreadyPoke = isAlreadyPoke + } +} diff --git a/SOPT-iOS/Projects/Domain/Sources/RepositoryInterface/PokeMainRepositoryInterface.swift b/SOPT-iOS/Projects/Domain/Sources/RepositoryInterface/PokeMainRepositoryInterface.swift new file mode 100644 index 00000000..b7706939 --- /dev/null +++ b/SOPT-iOS/Projects/Domain/Sources/RepositoryInterface/PokeMainRepositoryInterface.swift @@ -0,0 +1,15 @@ +// +// PokeMainRepositoryInterface.swift +// Domain +// +// Created by sejin on 12/19/23. +// Copyright © 2023 SOPT-iOS. All rights reserved. +// + +import Combine + +public protocol PokeMainRepositoryInterface { + func getWhoPokeToMe() -> AnyPublisher + func getFriend() -> AnyPublisher<[PokeUserModel], Error> + func getFriendRandomUser() -> AnyPublisher<[PokeFriendRandomUserModel], Error> +} diff --git a/SOPT-iOS/Projects/Domain/Sources/UseCase/PokeMainUseCase.swift b/SOPT-iOS/Projects/Domain/Sources/UseCase/PokeMainUseCase.swift new file mode 100644 index 00000000..59a1dd01 --- /dev/null +++ b/SOPT-iOS/Projects/Domain/Sources/UseCase/PokeMainUseCase.swift @@ -0,0 +1,67 @@ +// +// PokeMainUseCase.swift +// Domain +// +// Created by sejin on 12/19/23. +// Copyright © 2023 SOPT-iOS. All rights reserved. +// + +import Combine + +import Core + +public protocol PokeMainUseCase { + var pokedToMeUser: PassthroughSubject { get } + var myFriend: PassthroughSubject<[PokeUserModel], Never> { get } + var friendRandomUsers: PassthroughSubject<[PokeFriendRandomUserModel], Never> { get } + + func getWhoPokedToMe() + func getFriend() + func getFriendRandomUser() +} + +public class DefaultPokeMainUseCase { + public let repository: PokeMainRepositoryInterface + public let cancelBag = CancelBag() + + public let pokedToMeUser = PassthroughSubject() + public let myFriend = PassthroughSubject<[PokeUserModel], Never>() + public let friendRandomUsers = PassthroughSubject<[PokeFriendRandomUserModel], Never>() + + + public init(repository: PokeMainRepositoryInterface) { + self.repository = repository + } +} + +extension DefaultPokeMainUseCase: PokeMainUseCase { + public func getWhoPokedToMe() { + repository.getWhoPokeToMe() + .catch { _ in + Just(nil) + } + .sink { event in + print("GetPokedToMe State: \(event)") + } receiveValue: { [weak self] pokeUser in + self?.pokedToMeUser.send(pokeUser) + }.store(in: cancelBag) + } + + public func getFriend() { + repository.getFriend() + .sink { event in + print("GetFriend State: \(event)") + } receiveValue: { [weak self] friend in + self?.myFriend.send(friend) + }.store(in: cancelBag) + } + + public func getFriendRandomUser() { + repository.getFriendRandomUser() + .sink { event in + print("GetFriendRandomUser State: \(event)") + } receiveValue: { [weak self] randomUsers in + self?.friendRandomUsers.send(randomUsers) + }.store(in: cancelBag) + } +} diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeChipView.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeChipView.swift index 649f2bc0..2fc5fc19 100644 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeChipView.swift +++ b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeChipView.swift @@ -13,6 +13,8 @@ import DSKit final public class PokeChipView: UIView { public enum ChipType { + case newUser + case singleFriend(friendName: String) case withPokeCount(relation: String, pokeCount: String) case acquaintance(friendname: String, relationCount: String) } @@ -72,6 +74,10 @@ final public class PokeChipView: UIView { extension PokeChipView { public func configure(with pokechipType: ChipType) { switch pokechipType { + case .newUser: + self.titleLabel.text = "새로운 친구" + case let .singleFriend(friendName): + self.titleLabel.text = "\(friendName)의 친구" case let .withPokeCount(relation, pokeCount): self.titleLabel.text = relation + Constant.dotWithWhiteSpace + pokeCount + "콕" case let .acquaintance(friendname, relationCount): diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeKokButton.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeKokButton.swift index 2d9cc2e1..d0b16708 100644 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeKokButton.swift +++ b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeKokButton.swift @@ -60,6 +60,10 @@ public final class PokeKokButton: UIButton { self.backgroundColor = backgroundColor } + public func setIsFriend(with isFriend: Bool) { + self.isFriend = isFriend + } + private func setIcon() { let icon = self.isFriend ? DSKitAsset.Assets.iconKok.image : DSKitAsset.Assets.iconEyes.image self.setImage(icon.withTintColor(DSKitAsset.Colors.black.color), for: .normal) diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeNotificationListContentView.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeNotificationListContentView.swift index 968608b1..7187922e 100644 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeNotificationListContentView.swift +++ b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeNotificationListContentView.swift @@ -78,6 +78,8 @@ final public class PokeNotificationListContentView: UIView { // NOTE: NotifcationDetailView에서는 description의 numberOfLine Value가 2에요 private let isDetailView: Bool + private var userId: Int? + // MARK: - View Lifecycle public init( isDetailView: Bool = true, @@ -138,20 +140,22 @@ extension PokeNotificationListContentView { extension PokeNotificationListContentView { public func configure(with model: NotificationListContentModel) { + self.userId = model.userId self.profileImageView.setImage(with: model.avatarUrl, relation: model.pokeRelation) self.nameLabel.text = model.name self.partInfoLabel.text = model.partInfomation self.descriptionLabel.attributedText = model.description.applyMDSFont() self.pokeChipView.configure(with: model.chipInfo) self.pokeKokButton.isEnabled = !model.isPoked + self.pokeKokButton.setIsFriend(with: !model.isFirstMeet) } public func poked() { // TBD } - public func signalForPokeButtonClicked() -> Driver { - self.pokeKokButton.tap + public func signalForPokeButtonClicked() -> Driver { + self.pokeKokButton.tap.map { self.userId }.asDriver() } } diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeProfileCardView.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeProfileCardView.swift index 385036c4..ad19d557 100644 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeProfileCardView.swift +++ b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeProfileCardView.swift @@ -10,16 +10,17 @@ import UIKit import DSKit import Core +import Domain public final class PokeProfileCardView: UIView { // MARK: - Properties - typealias UserId = String + typealias UserId = Int lazy var kokButtonTap: Driver = kokButton.tap.map { self.userId }.asDriver() - var userId: String? + var userId: Int? // MARK: - UI Components @@ -102,12 +103,12 @@ public final class PokeProfileCardView: UIView { } // MARK: - Methods - - func setData(with model: ProfileCardContentModel) { + + func setData(with model: PokeUserModel) { self.userId = model.userId - self.profileImageView.setImage(with: model.avatarUrl) + self.profileImageView.setImage(with: model.profileImage) self.nameLabel.text = model.name - self.partLabel.text = model.partInfomation + self.partLabel.text = model.part } @discardableResult diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeProfileListView.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeProfileListView.swift index a0c36790..02c5dfb6 100644 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeProfileListView.swift +++ b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Components/PokeProfileListView.swift @@ -10,18 +10,19 @@ import UIKit import DSKit import Core +import Domain public final class PokeProfileListView: UIView { // MARK: - Properties - typealias UserId = String + typealias UserId = Int lazy var kokButtonTap: Driver = kokButton.tap.map { self.userId }.asDriver() var viewType: ProfileListType - var userId: String? + var userId: Int? // MARK: - UI Components @@ -171,12 +172,12 @@ public final class PokeProfileListView: UIView { // MARK: - Methods @discardableResult - func setData(with model: ProfileListContentModel) -> Self { + func setData(with model: PokeUserModel) -> Self { self.userId = model.userId - self.profileImageView.setImage(with: model.avatarUrl, relation: model.relation) + self.profileImageView.setImage(with: model.profileImage, relation: model.pokeRelation) self.nameLabel.text = model.name - self.partLabel.text = model.partInfomation - self.kokCountLabel.text = "\(model.pokeCount)콕" + self.partLabel.text = model.part + self.kokCountLabel.text = "\(model.pokeNum)콕" return self } diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Coordinator/PokeBuilder.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Coordinator/PokeBuilder.swift index 45b8910e..3b4c3e1d 100644 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Coordinator/PokeBuilder.swift +++ b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Coordinator/PokeBuilder.swift @@ -11,12 +11,15 @@ import Domain @_exported import PokeFeatureInterface public final class PokeBuilder { + @Injected public var pokeMainRepository: PokeMainRepositoryInterface + public init() {} } extension PokeBuilder: PokeFeatureBuildable { public func makePokeMain() -> PokeFeatureInterface.PokeMainPresentable { - let viewModel = PokeMainViewModel() + let useCase = DefaultPokeMainUseCase(repository: pokeMainRepository) + let viewModel = PokeMainViewModel(useCase: useCase) let pokeMainVC = PokeMainVC(viewModel: viewModel) return (pokeMainVC, viewModel) } diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Enum/PokeRelation.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Enum/PokeRelation.swift index 278fe60c..c89f8301 100644 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Enum/PokeRelation.swift +++ b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Enum/PokeRelation.swift @@ -8,8 +8,10 @@ import UIKit import DSKit +import Domain public enum PokeRelation: String { + case nonFriend case newFriend = "친한친구" case bestFriend = "단짝친구" case soulmate = "천생연분" @@ -18,7 +20,7 @@ public enum PokeRelation: String { extension PokeRelation { var color: UIColor { switch self { - case .newFriend: + case .nonFriend, .newFriend: return DSKitAsset.Colors.success.color case .bestFriend: return DSKitAsset.Colors.information.color @@ -27,3 +29,9 @@ extension PokeRelation { } } } + +extension PokeUserModel { + var pokeRelation: PokeRelation { + return PokeRelation(rawValue: self.relationName) ?? .nonFriend + } +} diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Models/NotificationListContentModel.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Models/NotificationListContentModel.swift index 6b171b8e..ac0e09fd 100644 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Models/NotificationListContentModel.swift +++ b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Models/NotificationListContentModel.swift @@ -7,6 +7,7 @@ // public struct NotificationListContentModel { + let userId: Int let avatarUrl: String let pokeRelation: PokeRelation let name: String @@ -14,4 +15,5 @@ public struct NotificationListContentModel { let description: String let chipInfo: PokeChipView.ChipType let isPoked: Bool + let isFirstMeet: Bool } diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Models/ProfileCardContentModel.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Models/ProfileCardContentModel.swift deleted file mode 100644 index 958cc695..00000000 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Models/ProfileCardContentModel.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// ProfileCardContentModel.swift -// PokeFeature -// -// Created by sejin on 12/8/23. -// Copyright © 2023 SOPT-iOS. All rights reserved. -// - -import Foundation - -public struct ProfileCardContentModel { - let userId: String - let avatarUrl: String - let name: String - let partInfomation: String -} diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Models/ProfileListContentModel.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/Models/ProfileListContentModel.swift deleted file mode 100644 index c524d3cc..00000000 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/Models/ProfileListContentModel.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// ProfileListContentModel.swift -// PokeFeature -// -// Created by sejin on 12/8/23. -// Copyright © 2023 SOPT-iOS. All rights reserved. -// - -import Foundation - -public struct ProfileListContentModel { - let userId: String - let avatarUrl: String - let name: String - let partInfomation: String - let pokeCount: Int - let relation: PokeRelation -} diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMainScene/VC/PokeMainVC.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMainScene/VC/PokeMainVC.swift index e93fc3f9..8af5261b 100644 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMainScene/VC/PokeMainVC.swift +++ b/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMainScene/VC/PokeMainVC.swift @@ -57,7 +57,9 @@ public final class PokeMainVC: UIViewController, PokeMainViewControllable { // 누가 나를 찔렀어요 부분 private let pokedSectionHeaderView = PokeMainSectionHeaderView(title: I18N.Poke.someonePokedMe) private let pokedUserContentView = PokeNotificationListContentView(frame: .zero) - private lazy var pokedSectionGroupView = self.makeSectionGroupView(header: pokedSectionHeaderView, content: pokedUserContentView) + private lazy var pokedSectionGroupView = self.makeSectionGroupView(header: pokedSectionHeaderView, content: pokedUserContentView).then { + $0.isHidden = true + } // 내 친구를 찔러보세요 부분 private let friendSectionHeaderView = PokeMainSectionHeaderView(title: I18N.Poke.pokeMyFriends) @@ -179,6 +181,18 @@ extension PokeMainVC { make.bottom.equalToSuperview().inset(20) } } + + private func setFriendRandomUsers(with randomUsers: [PokeFriendRandomUserModel]) { + let profileCardGroupViews = [firstProfileCardGroupView, secondProfileCardGroupView] + + for (i, profileCardGroupView) in profileCardGroupViews.enumerated() { + let randomUser = randomUsers[safe: i] + profileCardGroupView.isHidden = (randomUser == nil) + if let randomUser { + profileCardGroupView.setData(with: randomUser) + } + } + } } // MARK: - Methods @@ -187,6 +201,7 @@ extension PokeMainVC { private func bindViewModel() { let input = PokeMainViewModel .Input( + viewDidLoad: Just(()).asDriver(), naviBackButtonTap: self.backButton .publisher(for: .touchUpInside) .mapVoid().asDriver(), @@ -194,8 +209,8 @@ extension PokeMainVC { .rightButtonTap, friendSectionHeaderButtonTap: friendSectionHeaderView .rightButtonTap, - pokedSectionKokButtonTap: PassthroughSubject() - .asDriver(), + pokedSectionKokButtonTap: pokedUserContentView + .signalForPokeButtonClicked(), friendSectionKokButtonTap: friendSectionContentView .kokButtonTap, nearbyFriendsSectionKokButtonTap: firstProfileCardGroupView @@ -207,14 +222,36 @@ extension PokeMainVC { let output = viewModel.transform(from: input, cancelBag: cancelBag) - // 테스트를 위해 더미 데이터를 넣도록 임시 세팅 - pokedSectionHeaderView.rightButtonTap - .sink { _ in - self.friendSectionContentView.setData(with: .init(userId: "1234aa", avatarUrl: "sdafasdf", name: "test1", partInfomation: "ios", pokeCount: 3, relation: .bestFriend)) - - self.firstProfileCardGroupView.setProfileCard(with: [.init(userId: "777", avatarUrl: "sdafds", name: "test2", partInfomation: "server"), .init(userId: "999", avatarUrl: "", name: "test3", partInfomation: "pm")], friendName: "someone") - - self.secondProfileCardGroupView.setProfileCard(with: [.init(userId: "001", avatarUrl: "sdafds", name: "test4", partInfomation: "server")], friendName: "aaa") + output.pokedToMeUser + .withUnretained(self) + .sink { owner, model in + owner.pokedUserContentView.configure(with: model) + }.store(in: cancelBag) + + output.pokedUserSectionWillBeHidden + .assign(to: \.isHidden, onWeak: pokedSectionGroupView) + .store(in: cancelBag) + + output.myFriend + .withUnretained(self) + .sink { owner, model in + owner.friendSectionContentView.setData(with: model) + }.store(in: cancelBag) + + output.friendsSectionWillBeHidden + .assign(to: \.isHidden, onWeak: friendSectionGroupView) + .store(in: cancelBag) + + output.friendRandomUsers + .withUnretained(self) + .sink { owner, randomUsers in + owner.setFriendRandomUsers(with: randomUsers) + }.store(in: cancelBag) + + output.endRefreshLoading + .withUnretained(self) + .sink { owner, _ in + owner.refreshControl.endRefreshing() }.store(in: cancelBag) } } diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMainScene/ViewModel/PokeMainViewModel.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMainScene/ViewModel/PokeMainViewModel.swift index 60820ff1..dce6aaa0 100644 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMainScene/ViewModel/PokeMainViewModel.swift +++ b/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMainScene/ViewModel/PokeMainViewModel.swift @@ -16,19 +16,21 @@ import PokeFeatureInterface public class PokeMainViewModel: PokeMainViewModelType { - - typealias UserId = String + + typealias UserId = Int public var onNaviBackTap: (() -> Void)? public var onMyFriendsTap: (() -> Void)? // MARK: - Properties + private let useCase: PokeMainUseCase private var cancelBag = CancelBag() // MARK: - Inputs public struct Input { + let viewDidLoad: Driver let naviBackButtonTap: Driver let pokedSectionHeaderButtonTap: Driver let friendSectionHeaderButtonTap: Driver @@ -41,20 +43,33 @@ public class PokeMainViewModel: // MARK: - Outputs public struct Output { + let pokedToMeUser = PassthroughSubject() + let pokedUserSectionWillBeHidden = PassthroughSubject() + let myFriend = PassthroughSubject() + let friendsSectionWillBeHidden = PassthroughSubject() + let friendRandomUsers = PassthroughSubject<[PokeFriendRandomUserModel], Never>() + let endRefreshLoading = PassthroughSubject() } // MARK: - initialization - public init() { - + public init(useCase: PokeMainUseCase) { + self.useCase = useCase } } - + extension PokeMainViewModel { public func transform(from input: Input, cancelBag: Core.CancelBag) -> Output { let output = Output() self.bindOutput(output: output, cancelBag: cancelBag) + Publishers.Merge(input.viewDidLoad, input.refreshRequest) + .sink { [weak self] _ in + self?.useCase.getWhoPokedToMe() + self?.useCase.getFriend() + self?.useCase.getFriendRandomUser() + }.store(in: cancelBag) + input.naviBackButtonTap .sink { [weak self] _ in self?.onNaviBackTap?() @@ -75,7 +90,7 @@ extension PokeMainViewModel { .sink { userId in print("찌르기 - \(userId)") }.store(in: cancelBag) - + input.friendSectionKokButtonTap .compactMap { $0 } .sink { userId in @@ -88,14 +103,73 @@ extension PokeMainViewModel { print("찌르기 - \(userId)") }.store(in: cancelBag) - input.refreshRequest - .sink { _ in - print("리프레시 요청") - }.store(in: cancelBag) - return output } private func bindOutput(output: Output, cancelBag: CancelBag) { + useCase.pokedToMeUser + .compactMap { $0 } + .withUnretained(self) + .map { owner, pokeUserModel in + owner.makeNotificationListContentModel(with: pokeUserModel) + } + .subscribe(output.pokedToMeUser) + .store(in: cancelBag) + + useCase.pokedToMeUser + .map { $0 == nil } + .subscribe(output.pokedUserSectionWillBeHidden) + .store(in: cancelBag) + + useCase.myFriend + .compactMap { $0.first } + .subscribe(output.myFriend) + .store(in: cancelBag) + + useCase.myFriend + .map { $0.isEmpty } + .subscribe(output.friendsSectionWillBeHidden) + .store(in: cancelBag) + + useCase.friendRandomUsers + .prefix(2) + .subscribe(output.friendRandomUsers) + .store(in: cancelBag) + + Publishers.Zip3(useCase.pokedToMeUser, useCase.myFriend, useCase.friendRandomUsers) + .map { _ in Void() } + .subscribe(output.endRefreshLoading) + .store(in: cancelBag) + } +} + +// MARK: - Methods + +extension PokeMainViewModel { + private func makeNotificationListContentModel(with model: PokeUserModel) -> NotificationListContentModel { + return NotificationListContentModel(userId: model.userId, + avatarUrl: model.profileImage, + pokeRelation: PokeRelation(rawValue: model.relationName) ?? .newFriend, + name: model.name, + partInfomation: model.part, + description: model.message, + chipInfo: self.makeChipInfo(with: model), + isPoked: model.isAlreadyPoke, + isFirstMeet: model.isFirstMeet) + } + + private func makeChipInfo(with model: PokeUserModel) -> PokeChipView.ChipType { + if model.isFirstMeet { // 친구가 아닌 경우 + switch model.mutual.count { + case 0: + return .newUser + case 1: + return .singleFriend(friendName: model.mutual.first ?? "") + default: + return .acquaintance(friendname: model.mutual.first ?? "", relationCount: "\(model.mutual.count-1)명") + } + } + + return .withPokeCount(relation: model.relationName, pokeCount: String(model.pokeNum)) } } diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMainScene/Views/ProfileCardGroupView.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMainScene/Views/ProfileCardGroupView.swift index d0f14579..df436a65 100644 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMainScene/Views/ProfileCardGroupView.swift +++ b/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMainScene/Views/ProfileCardGroupView.swift @@ -11,12 +11,13 @@ import Combine import Core import DSKit +import Domain public final class ProfileCardGroupView: UIView { // MARK: - Properties - typealias UserId = String + typealias UserId = Int lazy var kokButtonTap: Driver = leftProfileCardView.kokButtonTap .merge(with: rightProfileCardView.kokButtonTap) @@ -109,14 +110,15 @@ extension ProfileCardGroupView { // MARK: - Methods extension ProfileCardGroupView { - func setProfileCard(with models: [ProfileCardContentModel], friendName: String) { - self.friendNameLabel.text = friendName - let models = models.prefix(2) + + func setData(with model: PokeFriendRandomUserModel) { + self.friendNameLabel.text = model.friendName + let randomUsers = model.friendList.prefix(2) - handleProfileCardCount(count: models.count) + handleProfileCardCount(count: randomUsers.count) - for (index, model) in models.enumerated() { - index == 0 ? leftProfileCardView.setData(with: model) : rightProfileCardView.setData(with: model) + for (index, user) in randomUsers.enumerated() { + index == 0 ? leftProfileCardView.setData(with: user) : rightProfileCardView.setData(with: user) } } diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMyFriendsScene/VC/PokeMyFriendsVC.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMyFriendsScene/VC/PokeMyFriendsVC.swift index 6b2158ca..26339807 100644 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMyFriendsScene/VC/PokeMyFriendsVC.swift +++ b/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMyFriendsScene/VC/PokeMyFriendsVC.swift @@ -124,10 +124,6 @@ extension PokeMyFriendsVC { let output = viewModel.transform(from: input, cancelBag: cancelBag) // TODO: 임시로 데이터를 넣기 위한 코드 -> 서버 연결 후 제거 예정 - self.newFriendsSectionView.setData(friendsCount: 84, models: [.init(userId: "", avatarUrl: "", name: "가나다", partInfomation: "33기 안드로이드", pokeCount: 4, relation: .newFriend), .init(userId: "", avatarUrl: "", name: "가나다", partInfomation: "33기 안드로이드", pokeCount: 4, relation: .newFriend)]) - - self.bestFriendsSectionView.setData(friendsCount: 100, models: [.init(userId: "", avatarUrl: "", name: "가나다", partInfomation: "33기 안드로이드", pokeCount: 4, relation: .newFriend), .init(userId: "", avatarUrl: "", name: "가나다", partInfomation: "33기 서버", pokeCount: 4, relation: .newFriend)]) - self.soulmateSectionView.setData(friendsCount: 2, models: []) } } diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMyFriendsScene/Views/PokeFriendsSectionGroupView.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMyFriendsScene/Views/PokeFriendsSectionGroupView.swift index 761c4e3f..e298b372 100644 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMyFriendsScene/Views/PokeFriendsSectionGroupView.swift +++ b/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeMyFriendsScene/Views/PokeFriendsSectionGroupView.swift @@ -11,12 +11,13 @@ import Combine import DSKit import Core +import Domain public final class PokeFriendsSectionGroupView: UIView { // MARK: - Properties - typealias UserId = String + typealias UserId = Int lazy var headerRightButtonTap: Driver = headerView.rightButtonTap.map { self.relation }.asDriver() let kokButtonTap = PassthroughSubject() @@ -102,7 +103,7 @@ extension PokeFriendsSectionGroupView { return self } - public func setData(friendsCount: Int, models: [ProfileListContentModel]) { + public func setData(friendsCount: Int, models: [PokeUserModel]) { self.headerView.setFriendsCount(friendsCount) let models = Array(models.prefix(maxContentsCount)) diff --git a/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeTemp.swift b/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeTemp.swift deleted file mode 100644 index a088369e..00000000 --- a/SOPT-iOS/Projects/Features/PokeFeature/Sources/PokeTemp.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// PokeTemp.swift -// ProjectDescriptionHelpers -// -// Created by sejin on 12/3/23. -// - -import Foundation diff --git a/SOPT-iOS/Projects/Modules/Networks/Sources/API/PokeAPI.swift b/SOPT-iOS/Projects/Modules/Networks/Sources/API/PokeAPI.swift new file mode 100644 index 00000000..13caee65 --- /dev/null +++ b/SOPT-iOS/Projects/Modules/Networks/Sources/API/PokeAPI.swift @@ -0,0 +1,48 @@ +// +// PokeAPI.swift +// Networks +// +// Created by sejin on 12/19/23. +// Copyright © 2023 SOPT-iOS. All rights reserved. +// + +import Foundation + +import Alamofire +import Moya +import Core + +public enum PokeAPI { + case getWhoPokedToMe + case getFriend + case getFriendRandomUser +} + +extension PokeAPI: BaseAPI { + public static var apiType: APIType = .poke + + public var path: String { + switch self { + case .getWhoPokedToMe: + return "/to/me" + case .getFriend: + return "/friend" + case .getFriendRandomUser: + return "/friend/random-user" + } + } + + public var method: Moya.Method { + switch self { + case .getWhoPokedToMe, .getFriend, .getFriendRandomUser: + return .get + } + } + + public var task: Moya.Task { + switch self { + default: + return .requestPlain + } + } +} diff --git a/SOPT-iOS/Projects/Modules/Networks/Sources/Entity/PokeFriendRandomUserEntity.swift b/SOPT-iOS/Projects/Modules/Networks/Sources/Entity/PokeFriendRandomUserEntity.swift new file mode 100644 index 00000000..1270d350 --- /dev/null +++ b/SOPT-iOS/Projects/Modules/Networks/Sources/Entity/PokeFriendRandomUserEntity.swift @@ -0,0 +1,15 @@ +// +// PokeFriendRandomUserEntity.swift +// Networks +// +// Created by sejin on 12/20/23. +// Copyright © 2023 SOPT-iOS. All rights reserved. +// + +import Foundation + +public struct PokeFriendRandomUserEntity: Codable { + public let friendId: Int + public let friendName, friendProfileImage: String + public let friendList: [PokeUserEntity] +} diff --git a/SOPT-iOS/Projects/Modules/Networks/Sources/Entity/PokeUserEntity.swift b/SOPT-iOS/Projects/Modules/Networks/Sources/Entity/PokeUserEntity.swift new file mode 100644 index 00000000..f748e34f --- /dev/null +++ b/SOPT-iOS/Projects/Modules/Networks/Sources/Entity/PokeUserEntity.swift @@ -0,0 +1,22 @@ +// +// PokeUserEntity.swift +// Networks +// +// Created by sejin on 12/19/23. +// Copyright © 2023 SOPT-iOS. All rights reserved. +// + +import Foundation + +public struct PokeUserEntity: Codable { + public let userId: Int + public let playgroundId: Int + public let profileImage, name: String + public let generation: Int + public let part: String + public let pokeNum: Int + public let message: String + public let relationName: String + public let mutual: [String] + public let isFirstMeet, isAlreadyPoke: Bool +} diff --git a/SOPT-iOS/Projects/Modules/Networks/Sources/Foundation/BaseAPI.swift b/SOPT-iOS/Projects/Modules/Networks/Sources/Foundation/BaseAPI.swift index cc58e8c6..3735899c 100644 --- a/SOPT-iOS/Projects/Modules/Networks/Sources/Foundation/BaseAPI.swift +++ b/SOPT-iOS/Projects/Modules/Networks/Sources/Foundation/BaseAPI.swift @@ -22,6 +22,7 @@ public enum APIType { case config case notification case description + case poke } public protocol BaseAPI: TargetType { @@ -54,6 +55,8 @@ extension BaseAPI { base += "/notification" case .description: base += "/description" + case .poke: + base += "/poke" } guard let url = URL(string: base) else { diff --git a/SOPT-iOS/Projects/Modules/Networks/Sources/Service/PokeService.swift b/SOPT-iOS/Projects/Modules/Networks/Sources/Service/PokeService.swift new file mode 100644 index 00000000..5074aedf --- /dev/null +++ b/SOPT-iOS/Projects/Modules/Networks/Sources/Service/PokeService.swift @@ -0,0 +1,34 @@ +// +// PokeService.swift +// Networks +// +// Created by sejin on 12/19/23. +// Copyright © 2023 SOPT-iOS. All rights reserved. +// + +import Foundation +import Combine + +import Moya + +public typealias DefaultPokeService = BaseService + +public protocol PokeService { + func getWhoPokedToMe() -> AnyPublisher + func getFriend() -> AnyPublisher<[PokeUserEntity], Error> + func getFriendRandomUser() -> AnyPublisher<[PokeFriendRandomUserEntity], Error> +} + +extension DefaultPokeService: PokeService { + public func getWhoPokedToMe() -> AnyPublisher { + requestObjectInCombine(.getWhoPokedToMe) + } + + public func getFriend() -> AnyPublisher<[PokeUserEntity], Error> { + requestObjectInCombine(.getFriend) + } + + public func getFriendRandomUser() -> AnyPublisher<[PokeFriendRandomUserEntity], Error> { + requestObjectInCombine(.getFriendRandomUser) + } +} diff --git a/SOPT-iOS/Projects/SOPT-iOS/Sources/Dependency/RegisterDependencies.swift b/SOPT-iOS/Projects/SOPT-iOS/Sources/Dependency/RegisterDependencies.swift index b834f131..eb8de3fa 100644 --- a/SOPT-iOS/Projects/SOPT-iOS/Sources/Dependency/RegisterDependencies.swift +++ b/SOPT-iOS/Projects/SOPT-iOS/Sources/Dependency/RegisterDependencies.swift @@ -136,5 +136,12 @@ extension AppDelegate { ) } ) + container.register(interface: PokeMainRepositoryInterface.self, + implement: { + PokeMainRepository( + service: DefaultPokeService() + ) + } + ) } }