diff --git a/RecorDream-iOS/Projects/Core/Sources/Protocols/Cancellable.swift b/RecorDream-iOS/Projects/Core/Sources/Protocols/Cancellable.swift new file mode 100644 index 00000000..375504d6 --- /dev/null +++ b/RecorDream-iOS/Projects/Core/Sources/Protocols/Cancellable.swift @@ -0,0 +1,13 @@ +// +// Cancellable.swift +// RD-Core +// +// Created by 정은희 on 2022/11/09. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import Foundation + +public protocol Cancellable { + func cancel() +} diff --git a/RecorDream-iOS/Projects/Data/Sources/Network/DataTransform/DreamSearchResuest.swift b/RecorDream-iOS/Projects/Data/Sources/Network/DataTransform/DreamSearchResuest.swift new file mode 100644 index 00000000..3bcc9c82 --- /dev/null +++ b/RecorDream-iOS/Projects/Data/Sources/Network/DataTransform/DreamSearchResuest.swift @@ -0,0 +1,13 @@ +// +// DreamSearchResuest.swift +// Data +// +// Created by 정은희 on 2022/11/09. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import Foundation + +struct DreamSearchResuest: Encodable { + let query: String +} diff --git a/RecorDream-iOS/Projects/Domain/Sources/Entities/DreamSearch/DreamSearchEntity.swift b/RecorDream-iOS/Projects/Domain/Sources/Entities/DreamSearch/DreamSearchEntity.swift new file mode 100644 index 00000000..1246e701 --- /dev/null +++ b/RecorDream-iOS/Projects/Domain/Sources/Entities/DreamSearch/DreamSearchEntity.swift @@ -0,0 +1,37 @@ +// +// DreamSearchEntity.swift +// DomainTests +// +// Created by 정은희 on 2022/11/09. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import Foundation + +public struct Records: Equatable, Identifiable { + public enum Genre: Int { + case comedy + case romance + case action + case thriller + case mystery + case fear + case sf + case fantasy + case family + case etc + case none + } + + public let id: String + public let dreamColor: Int? + public let emotion: Int? + public let date: String? + public let title: String? + public let genre: Genre? +} + +public struct DreamSearchEntity: Equatable { + public let recordsCount: Int + public let records: [Records] +} diff --git a/RecorDream-iOS/Projects/Domain/Sources/Entities/DreamSearch/DreamSearchQuery.swift b/RecorDream-iOS/Projects/Domain/Sources/Entities/DreamSearch/DreamSearchQuery.swift new file mode 100644 index 00000000..3a832ad5 --- /dev/null +++ b/RecorDream-iOS/Projects/Domain/Sources/Entities/DreamSearch/DreamSearchQuery.swift @@ -0,0 +1,13 @@ +// +// DreamSearchQuery.swift +// DomainTests +// +// Created by 정은희 on 2022/11/09. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import Foundation + +public struct DreamSearchQuery: Equatable { + public let query: String +} diff --git a/RecorDream-iOS/Projects/Domain/Sources/RepositoryInterfaces/DremSearchRepository.swift b/RecorDream-iOS/Projects/Domain/Sources/RepositoryInterfaces/DremSearchRepository.swift new file mode 100644 index 00000000..a1782ee8 --- /dev/null +++ b/RecorDream-iOS/Projects/Domain/Sources/RepositoryInterfaces/DremSearchRepository.swift @@ -0,0 +1,18 @@ +// +// DreamSearchRepository.swift +// DomainTests +// +// Created by 정은희 on 2022/11/09. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import Foundation + +import RxSwift +import RD_Core + +public protocol DreamSearchRepository { + @discardableResult + func fetchDreamSearchList(query: DreamSearchQuery, + completion: @escaping(Result) -> Void) -> Cancellable? +} diff --git a/RecorDream-iOS/Projects/Domain/Sources/UseCases/DreamSearchUseCase.swift b/RecorDream-iOS/Projects/Domain/Sources/UseCases/DreamSearchUseCase.swift new file mode 100644 index 00000000..e1bcaf58 --- /dev/null +++ b/RecorDream-iOS/Projects/Domain/Sources/UseCases/DreamSearchUseCase.swift @@ -0,0 +1,36 @@ +// +// DreamSearchUseCase.swift +// DomainTests +// +// Created by 정은희 on 2022/11/09. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import Foundation + +import RD_Core + +public protocol DreamSearchUseCase { + func execute(requestValue: DreamSearchUseCaseRequestValue, + completion: @escaping (Result) -> Void) -> Cancellable? +} + +public final class DefaultDreamSearchUseCase: DreamSearchUseCase { + + private let dreamSearchRepository: DreamSearchRepository + + init(dreamSearchRepository: DreamSearchRepository) { + self.dreamSearchRepository = dreamSearchRepository + } + + public func execute(requestValue: DreamSearchUseCaseRequestValue, + completion: @escaping (Result) -> Void) -> Cancellable? { + return dreamSearchRepository.fetchDreamSearchList(query: requestValue.query) { result in + completion(result) + } + } +} + +public struct DreamSearchUseCaseRequestValue { + let query: DreamSearchQuery +} diff --git a/RecorDream-iOS/Projects/Domain/Sources/UseCases/UseCase.swift b/RecorDream-iOS/Projects/Domain/Sources/UseCases/UseCase.swift new file mode 100644 index 00000000..7529f4ed --- /dev/null +++ b/RecorDream-iOS/Projects/Domain/Sources/UseCases/UseCase.swift @@ -0,0 +1,16 @@ +// +// UseCase.swift +// DomainTests +// +// Created by 정은희 on 2022/11/09. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import Foundation + +import RD_Core + +public protocol UseCase { + @discardableResult + func start() -> Cancellable? +} diff --git a/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Responses/DreamSearchResponse.swift b/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Responses/DreamSearchResponse.swift new file mode 100644 index 00000000..5ca0d8e6 --- /dev/null +++ b/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Responses/DreamSearchResponse.swift @@ -0,0 +1,54 @@ +// +// DreamSearchResponse.swift +// RD-Network +// +// Created by 정은희 on 2022/11/08. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import Foundation + +public struct DreamSearchResponse: Decodable { + public let query: String + public let results: [DreamSearchResult] +} + +public extension DreamSearchResponse { + struct DreamSearchResult: Decodable { + private enum CodingKeys: String, CodingKey { + case recordsCount = "records_count" + case records + } + + public let recordsCount: Int + public let records: [Records] + } + + struct Records: Decodable { + private enum CodingKeys: String, CodingKey { + case id = "_id" + case dreamColor = "dream_color" + case emotion, date, title, genre + } + public enum Genre: Int, Decodable { + case comedy + case romance + case action + case thriller + case mystery + case fear + case sf + case fantasy + case family + case etc + case none + } + + public let id: String? + public let dreamColor: Int? + public let emotion: Int? + public let date: String? + public let title: String? + public let genre: Genre? + } +} diff --git a/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Routers/RecordRouter.swift b/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Routers/RecordRouter.swift index 737f145e..88bb2de7 100644 --- a/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Routers/RecordRouter.swift +++ b/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Routers/RecordRouter.swift @@ -12,6 +12,7 @@ import Alamofire enum RecordRouter { case writeRecord(title: String, date: String, content: String, emotion: Int, genre: [Int], note: String?, voice: URL?) + case searchRecord(keyword: String) } extension RecordRouter: BaseRouter { @@ -19,6 +20,8 @@ extension RecordRouter: BaseRouter { switch self { case .writeRecord: return .post + case .searchRecord: + return .get default: return .get } } @@ -27,6 +30,8 @@ extension RecordRouter: BaseRouter { switch self { case .writeRecord: return "/record" + case .searchRecord: + return "/record/storage/search" default: return "" } } @@ -42,6 +47,11 @@ extension RecordRouter: BaseRouter { "genre": genre ] return .requestBody(requestBody) + case .searchRecord(let keyword): + let query: [String: Any] = [ + "keyword": keyword + ] + return .query(query) default: return .requestPlain } } diff --git a/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Services/RecordService.swift b/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Services/RecordService.swift index 836f89d0..1d954be1 100644 --- a/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Services/RecordService.swift +++ b/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Services/RecordService.swift @@ -13,6 +13,7 @@ import RxSwift public protocol RecordService { func writeDreamRecord(title: String, date: String, content: String, emotion: Int, genre: [Int], note: String?, voice: URL?) -> Observable + func searchDreamRecord(query: String) -> Observable } public class DefaultRecordService: BaseService { @@ -21,8 +22,11 @@ public class DefaultRecordService: BaseService { private override init() {} } -extension DefaultRecordService: RecordService { - public func writeDreamRecord(title: String, date: String, content: String, emotion: Int, genre: [Int], note: String?, voice: URL?) -> RxSwift.Observable { - requestObjectInRx(RecordRouter.writeRecord(title: title, date: date, content: content, emotion: emotion, genre: genre, note: note, voice: voice)) - } -} +//extension DefaultRecordService: RecordService { +// public func writeDreamRecord(title: String, date: String, content: String, emotion: Int, genre: [Int], note: String?, voice: URL?) -> RxSwift.Observable { +// requestObjectInRx(RecordRouter.writeRecord(title: title, date: date, content: content, emotion: emotion, genre: genre, note: note, voice: voice)) +// } +// public func searchDreamRecord(query: String) -> Observable { +// requestObject(RecordRouter.searchRecord(keyword: query), type: DreamSearchResponse.self, decodingMode: .general, completion: <#T##(NetworkResult) -> Void#>) +// } +//} diff --git a/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/DreamSearhControllable.swift b/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/DreamSearhControllable.swift index af052cfe..4fafe2d0 100644 --- a/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/DreamSearhControllable.swift +++ b/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/DreamSearhControllable.swift @@ -12,7 +12,7 @@ import HeeKit public protocol DreamSearhControllable: Presentable, Reusable { } -public class DreamSearchCollectionViewCell: UICollectionReusableView, DreamSearhControllable { +public class DreamSearchCollectionViewCell: UICollectionViewCell, DreamSearhControllable { // MARK: - View Life Cycle override init(frame: CGRect) { super.init(frame: frame) diff --git a/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/Cell/DreamSearchBottomCVC.swift b/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/Cell/DreamSearchBottomCVC.swift index 023adb32..170bfd1d 100644 --- a/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/Cell/DreamSearchBottomCVC.swift +++ b/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/Cell/DreamSearchBottomCVC.swift @@ -10,7 +10,7 @@ import UIKit import RD_DSKit -final class DreamSearchBottomCVC: DreamSearchCollectionViewCell { +final class DreamSearchBottomCVC: UICollectionReusableView { private lazy var rogoImageView: UIImageView = { let iv = UIImageView() iv.image = RDDSKitAsset.Images.rdHomeLogo.image diff --git a/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/Cell/DreamSearchCountCVC.swift b/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/Cell/DreamSearchCountCVC.swift index 40e8346d..e92617d5 100644 --- a/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/Cell/DreamSearchCountCVC.swift +++ b/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/Cell/DreamSearchCountCVC.swift @@ -10,10 +10,9 @@ import UIKit import RD_DSKit -final class DreamSearchCountCVC: DreamSearchCollectionViewCell { +final class DreamSearchCountCVC: UICollectionReusableView { private lazy var countLabel: UILabel = { let lb = UILabel() - lb.text = "4개의 기록" // ✅ lb.textColor = RDDSKitColors.Color.white lb.font = RDDSKitFontFamily.Pretendard.semiBold.font(size: 12) return lb @@ -41,3 +40,9 @@ final class DreamSearchCountCVC: DreamSearchCollectionViewCell { } } } + +extension DreamSearchCountCVC { + func configureCell(viewModel: DreamSearchResultViewModel) { + self.countLabel.text = "\(viewModel.recordsCount)개의 기록" + } +} diff --git a/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/Cell/DreamSearchExistCVC.swift b/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/Cell/DreamSearchExistCVC.swift index 3ab923b1..8252b1a8 100644 --- a/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/Cell/DreamSearchExistCVC.swift +++ b/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/Cell/DreamSearchExistCVC.swift @@ -88,3 +88,10 @@ final class DreamSearchExistCVC: DreamSearchCollectionViewCell { } } } + +extension DreamSearchExistCVC { + func configureCell(viewModel: DreamSearchResultViewModel) { +// self.backgroundImageView +// self.genreView + } +} diff --git a/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/Cell/Layout/DreamSearchSection.swift b/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/Cell/Layout/DreamSearchSection.swift index 0ba6c657..58deaa81 100644 --- a/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/Cell/Layout/DreamSearchSection.swift +++ b/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/Cell/Layout/DreamSearchSection.swift @@ -9,7 +9,6 @@ import UIKit -@available(iOS 16.0, *) extension DreamSearchVC { func layout() -> UICollectionViewLayout { return UICollectionViewCompositionalLayout { [weak self] sectionNumber, environment -> NSCollectionLayoutSection? in @@ -36,7 +35,7 @@ extension DreamSearchVC { heightDimension: .estimated(88.adjustedHeight) ) let group = NSCollectionLayoutGroup.vertical( - layoutSize: groupSize, repeatingSubitem: item, count: 1) + layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) let sectionFooter = self.createSectionFooter() section.orthogonalScrollingBehavior = .continuous @@ -57,7 +56,7 @@ extension DreamSearchVC { heightDimension: .fractionalHeight(1.0) ) let group = NSCollectionLayoutGroup.vertical( - layoutSize: groupSize, repeatingSubitem: item, count: 1 + layoutSize: groupSize, subitems: [item] ) let sectionFooter = self.createSectionFooter() let section = NSCollectionLayoutSection(group: group) @@ -66,7 +65,6 @@ extension DreamSearchVC { section.contentInsets = .init( top: 208, leading: 113, bottom: 282, trailing: 113 ) - return section } private func createSectionFooter() -> NSCollectionLayoutBoundarySupplementaryItem { diff --git a/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/DreamSearchVC.swift b/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/DreamSearchVC.swift index 2eecd3a3..019a7fa8 100644 --- a/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/DreamSearchVC.swift +++ b/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/View/DreamSearchVC.swift @@ -9,8 +9,8 @@ import UIKit import RD_DSKit +import RxSwift -@available(iOS 16.0, *) public class DreamSearchVC: UIViewController { // MARK: - UI Components private lazy var navigationBar = RDNaviBar().title("검색하기") @@ -48,24 +48,27 @@ public class DreamSearchVC: UIViewController { return cv }() + // MARK: - Reactive Properties + private var disposeBag = DisposeBag() + private var viewModel = DreamSearchViewModel() + // MARK: - View Life Cycle public override func viewDidLoad() { super.viewDidLoad() self.setupView() self.setupConstraint() -// self.assignDelegate() + self.assignDelegate() self.registerXib() } } -@available(iOS 16.0, *) +// MARK: - Extensions extension DreamSearchVC: DreamSearhControllable { public func setupView() { self.view.backgroundColor = .black self.view.addSubviews(navigationBar, searchLabel, searchButton, searchTextField, dreamSearchCollectionView) } - public func setupConstraint() { navigationBar.snp.makeConstraints { make in make.top.equalTo(view.safeAreaLayoutGuide.snp.top) @@ -91,35 +94,77 @@ extension DreamSearchVC: DreamSearhControllable { make.leading.trailing.bottom.equalToSuperview() } } -// private func assignDelegate() { -// dreamSearchCollectionView.dataSource = self -// dreamSearchCollectionView.delegate = self -// } + private func assignDelegate() { + dreamSearchCollectionView.dataSource = self + dreamSearchCollectionView.delegate = self + } private func registerXib() { dreamSearchCollectionView.register(DreamSearchBottomCVC.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: DreamSearchBottomCVC.reuseIdentifier) dreamSearchCollectionView.register(DreamSearchCountCVC.self, forCellWithReuseIdentifier: DreamSearchCountCVC.reuseIdentifier) dreamSearchCollectionView.register(DreamSearchEmptyCVC.self, forCellWithReuseIdentifier: DreamSearchEmptyCVC.reuseIdentifier) dreamSearchCollectionView.register(DreamSearchExistCVC.self, forCellWithReuseIdentifier: DreamSearchExistCVC.reuseIdentifier) - } } +extension DreamSearchVC: UICollectionViewDataSource, UICollectionViewDelegate { + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return viewModel.numberOfItems + } -//@available(iOS 16.0, *) -//extension DreamSearchVC: UICollectionViewDataSource, UICollectionViewDelegate { -// public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { -// <#code#> -// } -// -// public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { -//// switch -// } -// public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { -// <#code#> -// } + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + + switch DreamSearchResultType.init(rawValue: indexPath.section) { + case .exist: + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DreamSearchExistCVC.reuseIdentifier, for: indexPath) as? DreamSearchExistCVC + else { return UICollectionViewCell() } + cell.configureCell(viewModel: viewModel.getViewModelForCell(indexPathAtRow: indexPath.row)) + return cell + case .none: + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DreamSearchEmptyCVC.reuseIdentifier, for: indexPath) as? DreamSearchEmptyCVC + else { return UICollectionViewCell() } + return cell + default: + return UICollectionViewCell() + } + } + public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + if kind == UICollectionView.elementKindSectionHeader { + guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: DreamSearchCountCVC.reuseIdentifier, for: indexPath) as? DreamSearchCountCVC + else { return UICollectionReusableView() } + return header + } + else if kind == UICollectionView.elementKindSectionFooter { + guard let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: DreamSearchBottomCVC.reuseIdentifier, for: indexPath) as? DreamSearchBottomCVC + else { return UICollectionReusableView() } + return footer + } + else { + return UICollectionReusableView() + } + } // public func numberOfSections(in collectionView: UICollectionView) -> Int { -// <#code#> +// return viewModel.numberOfItems // } // public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { // <#code#> // } -//} +} + +// MARK: - Reactive stuff +extension DreamSearchVC { + private func reloadDataSubscription() { + self.viewModel.reloadCollectionViewData.subscribe { [weak self] _ in + guard let self = self else { return } + DispatchQueue.main.async { + self.dreamSearchCollectionView.reloadData() + } + }.disposed(by: disposeBag) + } + private func bindSearchData() { + + searchTextField.rx.text.orEmpty + .bind(to: viewModel.searchQuery) + .disposed(by: disposeBag) +// dreamSearchCollectionView.rx.itemSelected +// .bind(to: viewModel.searchResults) + } +} diff --git a/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/ViewModel/DreamSearchResultViewModel.swift b/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/ViewModel/DreamSearchResultViewModel.swift new file mode 100644 index 00000000..13d7e528 --- /dev/null +++ b/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/ViewModel/DreamSearchResultViewModel.swift @@ -0,0 +1,41 @@ +// +// DreamSearchResultViewModel.swift +// Presentation +// +// Created by 정은희 on 2022/10/10. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import Foundation + +import Domain +import RD_Network + +struct DreamSearchResultViewModel { + // Input + var dreamSearchResult: DreamSearchEntity! + // Output + var recordsCount: Int = 0 + var hasRecords: [Records] = [] + + init(dreamSearchResultModel: DreamSearchEntity) { + self.dreamSearchResult = dreamSearchResultModel + self.configureOutputForCell() + } +} + +extension DreamSearchResultViewModel { + mutating func configureOutputForCell() { + self.recordsCount = self.getRecordsCount() + self.hasRecords = self.getRecords() + } +} + +extension DreamSearchResultViewModel { + private func getRecordsCount() -> Int { + return dreamSearchResult.recordsCount + } + private func getRecords() -> [Records] { + return dreamSearchResult.records + } +} diff --git a/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/ViewModel/DreamSearchViewModel.swift b/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/ViewModel/DreamSearchViewModel.swift index 21fef8c9..1da92198 100644 --- a/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/ViewModel/DreamSearchViewModel.swift +++ b/RecorDream-iOS/Projects/Presentation/Sources/SearchScene/DreamSearch/ViewModel/DreamSearchViewModel.swift @@ -2,8 +2,107 @@ // DreamSearchViewModel.swift // Presentation // -// Created by 정은희 on 2022/10/10. +// Created by 정은희 on 2022/11/08. // Copyright © 2022 RecorDream. All rights reserved. // import Foundation + +import Domain +import RD_Network +import RxSwift +import HeeKit + +final class DreamSearchViewModel { + // Input + private var provider: DefaultRecordService! + private var collectionViewDataSource: [DreamSearchResultViewModel] = [] // 서브 뷰모델에 해당 + private var currentSearchQuery = "" + // Output + var numberOfItems: Int = 0 + // Observer input + var searchResults = PublishSubject<[DreamSearchEntity]>() + var cellSelected = PublishSubject() + var reloadCollectionViewData = PublishSubject() + var cancelButtonTapped = PublishSubject() + var searchQuery = PublishSubject() + + private var disposeBag = DisposeBag() + + init(provider: DefaultRecordService = DefaultRecordService.shared) { + self.provider = provider + self.initialize() + } +} + +extension DreamSearchViewModel { + func initialize() { + searchQuery + .asObservable() + .observe(on: MainScheduler.instance) + .flatMapLatest { query -> Observable<[DreamSearchEntity]> in + Log.event(type: .info, "사용자가 \(query)에 대한 검색을 시작함") + self.currentSearchQuery = query + self.resetCollectionViewDataSource() + + return self.searchItemsForTerm() + .catch { error -> Observable<[DreamSearchEntity]> in + self.reloadCollectionViewData.onNext(true) + return Observable.empty() + } + }.subscribe(onNext: { results in + if results.isEmpty { + self.reloadCollectionViewData.onNext(true) + } + else { + self.prepareCollectionViewDataSource(results: results) + self.reloadCollectionViewData.onNext(true) + } + }) + .disposed(by: disposeBag) + + cancelButtonTapped + .asObservable() + .subscribe(onNext: { [weak self] _ in + self?.resetCollectionViewDataSource() + self?.reloadCollectionViewData.onNext(true) + }).disposed(by: disposeBag) + } +} + +extension DreamSearchViewModel { + func prepareCollectionViewDataSource(results: [DreamSearchEntity]) { + self.numberOfItems = numberOfItems + results.count + let preparedData = results.map({ DreamSearchResultViewModel(dreamSearchResultModel: $0) }) + self.collectionViewDataSource.append(contentsOf: preparedData) + } + func searchItemsForTerm() -> Observable<[DreamSearchEntity]> { + return Observable.create({ [weak self] observer -> Disposable in + guard let self = self else { return } + self.provider.searchDreamRecords(keyword: self.currentSearchQuery) { result in + switch result { + case .success(let searchResponse): + if searchResponse.results.isEmpty { + Log.event(type: .error, "검색 결과가 존재하지 않습니다요") + observer.onError(CustomError.search(type: .notFound)) + } + else { + observer.onNext(searchResponse.results) + observer.onCompleted() + } + case .failure(let error): + observer.onError(error) + } + } + return Disposables.create() + }) + } + func getViewModelForCell(indexPathAtRow: Int) -> DreamSearchResultViewModel { + return collectionViewDataSource[indexPathAtRow] + } + private func resetCollectionViewDataSource() { + Log.event(type: .info, "사용자가 취소 버튼 누름 -> 검색 데이터를 삭제하자!") + numberOfItems = 0 + collectionViewDataSource = [] + } +} diff --git a/RecorDream-iOS/Tuist/Dependencies.swift b/RecorDream-iOS/Tuist/Dependencies.swift index 20e2a46c..6b48177e 100644 --- a/RecorDream-iOS/Tuist/Dependencies.swift +++ b/RecorDream-iOS/Tuist/Dependencies.swift @@ -13,7 +13,7 @@ let spm = SwiftPackageManagerDependencies([ .remote(url: "https://github.com/ReactiveX/RxSwift.git", requirement: .upToNextMinor(from: "6.5.0")), .remote(url: "https://github.com/kakao/kakao-ios-sdk", requirement: .upToNextMinor(from: "2.11.1")), .remote(url: "https://github.com/firebase/firebase-ios-sdk.git", requirement: .upToNextMinor(from: "9.6.0")), - .remote(url: "https://github.com/EunHee-Jeong/HeeKit.git", requirement: .revision("304a95e")) + .remote(url: "https://github.com/EunHee-Jeong/HeeKit.git", requirement: .revision("e79a507")) ]) let dependencies = Dependencies(