diff --git a/DailyQuest/DailyQuest.xcodeproj/project.pbxproj b/DailyQuest/DailyQuest.xcodeproj/project.pbxproj index 8a87335..eed2e83 100644 --- a/DailyQuest/DailyQuest.xcodeproj/project.pbxproj +++ b/DailyQuest/DailyQuest.xcodeproj/project.pbxproj @@ -49,10 +49,12 @@ 3449AD5B2922164B00B87619 /* Quest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3449AD5A2922164B00B87619 /* Quest.swift */; }; 3449AD5D2922197000B87619 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3449AD5C2922197000B87619 /* User.swift */; }; 3449AD6029222B3900B87619 /* UserInfoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3449AD5F29222B3900B87619 /* UserInfoCell.swift */; }; + 344A459A293DC495007A3D37 /* EnrollUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344A4599293DC495007A3D37 /* EnrollUseCase.swift */; }; 345687F42937329E00CA51E3 /* EnrollViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345687F32937329E00CA51E3 /* EnrollViewController.swift */; }; 345687F62937430200CA51E3 /* PlanDatePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345687F52937430200CA51E3 /* PlanDatePickerView.swift */; }; 345687F829374D2500CA51E3 /* DayNamePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345687F729374D2500CA51E3 /* DayNamePickerView.swift */; }; 345687FA2937815900CA51E3 /* QuantityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345687F92937815900CA51E3 /* QuantityView.swift */; }; + 345687FE29378AB900CA51E3 /* EnrollViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345687FD29378AB900CA51E3 /* EnrollViewModel.swift */; }; 34642AB62925D9E40052FA0E /* UserInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34642AB52925D9E40052FA0E /* UserInfoView.swift */; }; 347D258B292C60F40038FCA2 /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347D258A292C60F40038FCA2 /* StatusView.swift */; }; 347D258D292C6E220038FCA2 /* MessageBubble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347D258C292C6E220038FCA2 /* MessageBubble.swift */; }; @@ -103,6 +105,7 @@ 34EE0C662935FD7D002BEC23 /* BrowseItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3499552629235D1E007AB99E /* BrowseItemViewModel.swift */; }; 34EE6EB72924C674005AF583 /* QuestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34EE6EB62924C674005AF583 /* QuestView.swift */; }; 34EE6EB92924CAA1005AF583 /* QuestViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34EE6EB82924CAA1005AF583 /* QuestViewModel.swift */; }; + 34FCD366293DE62700E0DC8A /* DefaultEnrollUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34FCD365293DE62700E0DC8A /* DefaultEnrollUseCase.swift */; }; 34FEFB992935EA6D00954A40 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 34FEFB982935EA6D00954A40 /* Kingfisher */; }; 34FF6C5A292B86F8002AFD4D /* SnapKit-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 34FF6C46292B8014002AFD4D /* SnapKit-Dynamic */; }; 34FF6C5D292B8B27002AFD4D /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 34FF6C5C292B8B27002AFD4D /* RxCocoa */; }; @@ -211,10 +214,12 @@ 3449AD5A2922164B00B87619 /* Quest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quest.swift; sourceTree = ""; }; 3449AD5C2922197000B87619 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 3449AD5F29222B3900B87619 /* UserInfoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoCell.swift; sourceTree = ""; }; + 344A4599293DC495007A3D37 /* EnrollUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnrollUseCase.swift; sourceTree = ""; }; 345687F32937329E00CA51E3 /* EnrollViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnrollViewController.swift; sourceTree = ""; }; 345687F52937430200CA51E3 /* PlanDatePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanDatePickerView.swift; sourceTree = ""; }; 345687F729374D2500CA51E3 /* DayNamePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayNamePickerView.swift; sourceTree = ""; }; 345687F92937815900CA51E3 /* QuantityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuantityView.swift; sourceTree = ""; }; + 345687FD29378AB900CA51E3 /* EnrollViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnrollViewModel.swift; sourceTree = ""; }; 34642AB52925D9E40052FA0E /* UserInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfoView.swift; sourceTree = ""; }; 347D258A292C60F40038FCA2 /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = ""; }; 347D258C292C6E220038FCA2 /* MessageBubble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBubble.swift; sourceTree = ""; }; @@ -260,6 +265,7 @@ 34EE0C632935FD6B002BEC23 /* BrowseViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseViewModelTests.swift; sourceTree = ""; }; 34EE6EB62924C674005AF583 /* QuestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestView.swift; sourceTree = ""; }; 34EE6EB82924CAA1005AF583 /* QuestViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestViewModel.swift; sourceTree = ""; }; + 34FCD365293DE62700E0DC8A /* DefaultEnrollUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultEnrollUseCase.swift; sourceTree = ""; }; 9B1CFB3E292B585700CCE97A /* QuestDTO+Mapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QuestDTO+Mapping.swift"; sourceTree = ""; }; 9BD8CCF22935BC0D00E6EA2F /* DefaultBrowseRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultBrowseRepository.swift; sourceTree = ""; }; 9BD8CCF42935C38300E6EA2F /* UserDTO+Mapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDTO+Mapping.swift"; sourceTree = ""; }; @@ -376,6 +382,7 @@ children = ( 3416FC86292B54BF00B504C5 /* Protocols */, 3416FC89292B560800B504C5 /* DefaultQuestUseCase.swift */, + 34FCD365293DE62700E0DC8A /* DefaultEnrollUseCase.swift */, ); path = Home; sourceTree = ""; @@ -384,6 +391,7 @@ isa = PBXGroup; children = ( 3416FC87292B54DB00B504C5 /* QuestUseCase.swift */, + 344A4599293DC495007A3D37 /* EnrollUseCase.swift */, ); path = Protocols; sourceTree = ""; @@ -690,6 +698,7 @@ isa = PBXGroup; children = ( 34EE6EB82924CAA1005AF583 /* QuestViewModel.swift */, + 345687FD29378AB900CA51E3 /* EnrollViewModel.swift */, B5115452292CD07100FDBD22 /* CalendarViewModel.swift */, ); path = ViewModel; @@ -1147,6 +1156,7 @@ 34D8360129359C8A001DE9DF /* BrowseRepository.swift in Sources */, 34091552292DE9D1007873A8 /* QuestEntity.swift in Sources */, 342830F6292E1ACA00AE811B /* PlainItemViewModel.swift in Sources */, + 345687FE29378AB900CA51E3 /* EnrollViewModel.swift in Sources */, 34A529D129247880001BAD34 /* Coordinator.swift in Sources */, 34113BEB2934A3B200AB4919 /* LoginViewController.swift in Sources */, 340A724929348B1B00B26AA6 /* AuthUseCase.swift in Sources */, @@ -1228,8 +1238,10 @@ A51F01D029233C510031ECA2 /* UserInfoEntity+Mapping.swift in Sources */, 3499552B29236041007AB99E /* BrowseViewModel.swift in Sources */, 34113BED2934BD3D00AB4919 /* TextFieldForm.swift in Sources */, + 344A459A293DC495007A3D37 /* EnrollUseCase.swift in Sources */, 3449AD5B2922164B00B87619 /* Quest.swift in Sources */, 34283101292E2D7A00AE811B /* ToggleItemViewModel.swift in Sources */, + 34FCD366293DE62700E0DC8A /* DefaultEnrollUseCase.swift in Sources */, A5AC96E829223F27003B7637 /* RealmQuestsStorage.swift in Sources */, 345687F829374D2500CA51E3 /* DayNamePickerView.swift in Sources */, 349955122923220E007AB99E /* SwiftUIPreview.swift in Sources */, diff --git a/DailyQuest/DailyQuest/Application/DIContainer/HomeSceneDIContainer.swift b/DailyQuest/DailyQuest/Application/DIContainer/HomeSceneDIContainer.swift index 9a8cd22..8660b96 100644 --- a/DailyQuest/DailyQuest/Application/DIContainer/HomeSceneDIContainer.swift +++ b/DailyQuest/DailyQuest/Application/DIContainer/HomeSceneDIContainer.swift @@ -21,16 +21,28 @@ final class HomeSceneDIContainer { return DefaultQuestUseCase(questsRepository: makeQuestsRepository()) } + func makeEnrollUseCase() -> EnrollUseCase { + return DefaultEnrollUseCase(questsRepository: makeQuestsRepository()) + } + // MARK: - View Models func makeQuestViewModel() -> QuestViewModel { return QuestViewModel(questUseCase: makeQuestUseCase()) } + func makeEnrollViewModel() -> EnrollViewModel { + return EnrollViewModel(enrollUseCase: makeEnrollUseCase()) + } + // MARK: - View Controller func makeHomeViewController() -> HomeViewController { return HomeViewController.create(with: makeQuestViewModel()) } + func makeEnrollViewController() -> EnrollViewController { + return EnrollViewController.create(with: makeEnrollViewModel()) + } + // MARK: - Flow func makeHomeCoordinator(navigationController: UINavigationController, homeSceneDIContainer: HomeSceneDIContainer) -> HomeCoordinator { diff --git a/DailyQuest/DailyQuest/Domain/UseCases/Home/DefaultEnrollUseCase.swift b/DailyQuest/DailyQuest/Domain/UseCases/Home/DefaultEnrollUseCase.swift new file mode 100644 index 0000000..9f40e6a --- /dev/null +++ b/DailyQuest/DailyQuest/Domain/UseCases/Home/DefaultEnrollUseCase.swift @@ -0,0 +1,30 @@ +// +// DefaultEnrollUseCase.swift +// DailyQuest +// +// Created by jinwoong Kim on 2022/12/05. +// + +import Foundation + +import RxSwift + +final class DefaultEnrollUseCase { + private let questsRepository: QuestsRepository + + init(questsRepository: QuestsRepository) { + self.questsRepository = questsRepository + } +} + +extension DefaultEnrollUseCase: EnrollUseCase { + func save(with quests: [Quest]) -> Observable { + return questsRepository + .save(with: quests) + .map { _ in + true + } + .catchAndReturn(false) + .asObservable() + } +} diff --git a/DailyQuest/DailyQuest/Domain/UseCases/Home/Protocols/EnrollUseCase.swift b/DailyQuest/DailyQuest/Domain/UseCases/Home/Protocols/EnrollUseCase.swift new file mode 100644 index 0000000..32f4db4 --- /dev/null +++ b/DailyQuest/DailyQuest/Domain/UseCases/Home/Protocols/EnrollUseCase.swift @@ -0,0 +1,14 @@ +// +// EnrollUseCase.swift +// DailyQuest +// +// Created by jinwoong Kim on 2022/12/05. +// + +import Foundation + +import RxSwift + +protocol EnrollUseCase { + func save(with quests: [Quest]) -> Observable +} diff --git a/DailyQuest/DailyQuest/Presentation/Home/Flow/HomeCoordinator.swift b/DailyQuest/DailyQuest/Presentation/Home/Flow/HomeCoordinator.swift index 1f10a05..ec7b699 100644 --- a/DailyQuest/DailyQuest/Presentation/Home/Flow/HomeCoordinator.swift +++ b/DailyQuest/DailyQuest/Presentation/Home/Flow/HomeCoordinator.swift @@ -49,8 +49,8 @@ final class DefaultHomeCoordinator: HomeCoordinator { } func showAddQuestFlow() { - let addQuestsViewController = EnrollViewController() - navigationController.present(addQuestsViewController, animated: true) + let enrollViewController = homeSceneDIContainer.makeEnrollViewController() + navigationController.present(enrollViewController, animated: true) } func showAddFriendsFlow() { diff --git a/DailyQuest/DailyQuest/Presentation/Home/View/DayNamePickerView.swift b/DailyQuest/DailyQuest/Presentation/Home/View/DayNamePickerView.swift index 28a02ad..6a45ea2 100644 --- a/DailyQuest/DailyQuest/Presentation/Home/View/DayNamePickerView.swift +++ b/DailyQuest/DailyQuest/Presentation/Home/View/DayNamePickerView.swift @@ -11,12 +11,7 @@ import RxSwift import RxCocoa final class DayNamePickerView: UIStackView { - private var selectedDay = [0: false, 1: false, 2: false, 3: false, 4: false, 5: false, 6: false] - private(set) var selectedDayObservable = BehaviorRelay<[Int: Bool]>(value: [0: false, 1: false, 2: false, 3: false, 4: false, 5: false, 6: false]) - - private var disposableBag = DisposeBag() - - private lazy var buttons: [UIButton] = { + private(set) lazy var buttons: [UIButton] = { let days = ["S", "M", "T", "W", "T", "F", "S"] return days.map { day in @@ -35,8 +30,6 @@ final class DayNamePickerView: UIStackView { super.init(frame: frame) configureUI() - - bind() } required init(coder: NSCoder) { @@ -52,26 +45,5 @@ final class DayNamePickerView: UIStackView { self.addArrangedSubview(button) } } - - private func bind() { - let taps = buttons.enumerated().map { index, button in - button.rx.tap.map { _ in index } - } - - Observable.from(taps).merge() - .withUnretained(self) - .subscribe(onNext: { (owner, value) in - owner.selectedDay[value]?.toggle() - owner.selectedDayObservable.accept(owner.selectedDay) - - guard let isSelected = owner.selectedDay[value] else { return } - if isSelected { - owner.buttons[value].configuration?.baseBackgroundColor = .maxYellow - } else { - owner.buttons[value].configuration?.baseBackgroundColor = .maxLightYellow - } - }) - .disposed(by: disposableBag) - } } diff --git a/DailyQuest/DailyQuest/Presentation/Home/View/PlanDatePickerView.swift b/DailyQuest/DailyQuest/Presentation/Home/View/PlanDatePickerView.swift index a3db4f3..488cd46 100644 --- a/DailyQuest/DailyQuest/Presentation/Home/View/PlanDatePickerView.swift +++ b/DailyQuest/DailyQuest/Presentation/Home/View/PlanDatePickerView.swift @@ -11,7 +11,6 @@ import RxSwift import SnapKit final class PlanDatePickerView: UIView { - private(set) var date = PublishSubject() private var disposableBag = DisposeBag() private lazy var titleLabel: UILabel = { @@ -22,7 +21,7 @@ final class PlanDatePickerView: UIView { return titleLabel }() - private lazy var datePicker: UIDatePicker = { + private(set) lazy var datePicker: UIDatePicker = { let datePicker = UIDatePicker() datePicker.datePickerMode = .date @@ -33,8 +32,6 @@ final class PlanDatePickerView: UIView { super.init(frame: frame) configureUI() - - bind() } required init?(coder: NSCoder) { @@ -56,13 +53,4 @@ final class PlanDatePickerView: UIView { make.edges.equalToSuperview().inset(15) } } - - private func bind() { - datePicker - .rx - .controlEvent(.valueChanged) - .withLatestFrom(datePicker.rx.date) - .bind(to: date) - .disposed(by: disposableBag) - } } diff --git a/DailyQuest/DailyQuest/Presentation/Home/View/QuantityView.swift b/DailyQuest/DailyQuest/Presentation/Home/View/QuantityView.swift index f03b97b..e795b0b 100644 --- a/DailyQuest/DailyQuest/Presentation/Home/View/QuantityView.swift +++ b/DailyQuest/DailyQuest/Presentation/Home/View/QuantityView.swift @@ -7,6 +7,9 @@ import UIKit +import RxSwift +import RxCocoa + final class QuantityView: UIView { private lazy var titleLabel: UILabel = { @@ -17,10 +20,11 @@ final class QuantityView: UIView { return titleLabel }() - private lazy var quantityField: UITextField = { + private(set) lazy var quantityField: UITextField = { let quantityField = UITextField() quantityField.textAlignment = .right quantityField.placeholder = "0" + quantityField.keyboardType = .numberPad return quantityField }() @@ -29,8 +33,6 @@ final class QuantityView: UIView { super.init(frame: frame) configureUI() - - bind() } required init?(coder: NSCoder) { @@ -53,8 +55,4 @@ final class QuantityView: UIView { make.width.equalToSuperview().multipliedBy(0.3) } } - - private func bind() { - - } } diff --git a/DailyQuest/DailyQuest/Presentation/Home/ViewController/EnrollViewController.swift b/DailyQuest/DailyQuest/Presentation/Home/ViewController/EnrollViewController.swift index 7422c10..8be8677 100644 --- a/DailyQuest/DailyQuest/Presentation/Home/ViewController/EnrollViewController.swift +++ b/DailyQuest/DailyQuest/Presentation/Home/ViewController/EnrollViewController.swift @@ -8,9 +8,11 @@ import UIKit import RxSwift +import RxCocoa import SnapKit final class EnrollViewController: UIViewController { + private var viewModel: EnrollViewModel! private var disposableBag = DisposeBag() private lazy var container: UIStackView = { @@ -50,21 +52,31 @@ final class EnrollViewController: UIViewController { return QuantityView() }() + private lazy var submitButton: UIButton = { + var config = UIButton.Configuration.filled() + config.title = "등록하기" + config.baseBackgroundColor = .maxYellow + config.baseForegroundColor = .maxViolet + + return UIButton(configuration: config) + }() + // MARK: - Life Cycle + static func create(with viewModel: EnrollViewModel) -> EnrollViewController { + let vc = EnrollViewController() + vc.viewModel = viewModel + + return vc + } + override func viewDidLoad() { super.viewDidLoad() + quantityView.quantityField.delegate = self + configureUI() - daysPicker - .selectedDayObservable - .map { dictionary in - dictionary.filter { key, value in - value - } - } - .subscribe(onNext: { print($0) }) - .disposed(by: disposableBag) + bind() } private func configureUI() { @@ -75,6 +87,7 @@ final class EnrollViewController: UIViewController { container.addArrangedSubview(endDate) container.addArrangedSubview(daysPicker) container.addArrangedSubview(quantityView) + container.addArrangedSubview(submitButton) view.addSubview(container) @@ -84,4 +97,71 @@ final class EnrollViewController: UIViewController { make.width.equalToSuperview().multipliedBy(0.9) } } + + func bind() { + let titleDidChanged = titleField.rx.text.orEmpty.asObservable() + let startDateDidSet = startDate.datePicker.rx.date.asObservable() + let endDateDidSet = endDate.datePicker.rx.date.asObservable() + let quantityDidSet = quantityView.quantityField.rx.text.orEmpty.asObservable() + let submitButtonDidClicked = submitButton.rx.tap.asObservable() + + let taps = daysPicker + .buttons + .enumerated() + .map { index, button in + button.rx.tap.map { _ in index + 1 } + } + + let dayButtonDidClicked = Observable.from(taps).merge() + + let output = viewModel.transform( + input: EnrollViewModel.Input( + titleDidChanged: titleDidChanged, + startDateDidSet: startDateDidSet, + endDateDidSet: endDateDidSet, + quantityDidSet: quantityDidSet, + submitButtonDidClicked: submitButtonDidClicked, + dayButtonDidClicked: dayButtonDidClicked + ) + ) + + bindSubmitButton(output: output) + bindDayNamePickerView(output: output) + + output.enrollResult.subscribe(onNext: { print($0) }) + .disposed(by: disposableBag) + } + + private func bindSubmitButton(output: EnrollViewModel.Output) { + output + .buttonEnabled + .drive(submitButton.rx.isEnabled) + .disposed(by: disposableBag) + } + + private func bindDayNamePickerView(output: EnrollViewModel.Output) { + output + .dayButtonStatus + .bind(onNext: { [weak self] index, isSelected in + guard let isSelected = isSelected else { return } + if isSelected { + self?.daysPicker.buttons[index-1].configuration?.baseBackgroundColor = .maxYellow + } else { + self?.daysPicker.buttons[index-1].configuration?.baseBackgroundColor = .maxLightYellow + } + }) + .disposed(by: disposableBag) + } +} + +extension EnrollViewController: UITextFieldDelegate { + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + guard CharacterSet(charactersIn: "0123456789").isSuperset(of: CharacterSet(charactersIn: string)) else { + return false + } + guard let oldText = textField.text else { return true } + let text = (oldText as NSString).replacingCharacters(in: range, with: string) + + return text.count < 4 + } } diff --git a/DailyQuest/DailyQuest/Presentation/Home/ViewModel/EnrollViewModel.swift b/DailyQuest/DailyQuest/Presentation/Home/ViewModel/EnrollViewModel.swift new file mode 100644 index 0000000..ea7ad73 --- /dev/null +++ b/DailyQuest/DailyQuest/Presentation/Home/ViewModel/EnrollViewModel.swift @@ -0,0 +1,126 @@ +// +// EnrollViewModel.swift +// DailyQuest +// +// Created by jinwoong Kim on 2022/11/30. +// + +import Foundation + +import RxSwift +import RxCocoa + +final class EnrollViewModel { + private var selectedDay = [1: false, 2: false, 3: false, 4: false, 5: false, 6: false, 7: false] + private var selectedDayObservable = BehaviorRelay<[Int: Bool]>(value: [1: false, 2: false, 3: false, 4: false, 5: false, 6: false, 7: false]) + + private let enrollUseCase: EnrollUseCase + + private var disposableBag = DisposeBag() + + init(enrollUseCase: EnrollUseCase) { + self.enrollUseCase = enrollUseCase + } + + struct Input { + let titleDidChanged: Observable + let startDateDidSet: Observable + let endDateDidSet: Observable + let quantityDidSet: Observable + let submitButtonDidClicked: Observable + + let dayButtonDidClicked: Observable + } + + struct Output { + let buttonEnabled: Driver + let enrollResult: Observable + let dayButtonStatus: Observable<(Int, Bool?)> + } + + func transform(input: Input) -> Output { + let dates = Observable.combineLatest( + input.startDateDidSet, + input.endDateDidSet, + self.selectedDayObservable + ) + .compactMap(getDates(start:end:weekday:)) + .asObservable() + + let buttonEnabled = Observable.combineLatest( + input.titleDidChanged, + input.quantityDidSet) { title, quantity in + !title.isEmpty && !quantity.isEmpty + } + .asDriver(onErrorJustReturn: false) + + let dayButtonStatus = input + .dayButtonDidClicked + .map(didClicked(by:)) + .asObservable() + + let enrollResult = input.submitButtonDidClicked + .withLatestFrom(Observable + .combineLatest( + input.titleDidChanged, + dates, + input.quantityDidSet) + ) + .map(createQuests(title:dates:quantity:)) + .flatMap(enrollUseCase.save(with:)) + + return Output(buttonEnabled: buttonEnabled, + enrollResult: enrollResult, + dayButtonStatus: dayButtonStatus) + } +} + +extension EnrollViewModel { + private func getDates(start: Date, end: Date, weekday: [Int: Bool]) -> [Date]? { + let weekdays = weekday + .compactMap { (key: Int, value: Bool) in + if value { + return key + } else { + return nil + } + } + + var dates: [Date] = [] + var date = start + + while date <= end { + guard let weekday = Calendar(identifier: .gregorian).dateComponents([.weekday], from: date).weekday else { return nil } + if weekdays.contains(weekday) { + dates.append(date) + } + + guard let newDate = Calendar.current.date(byAdding: .day, value: 1, to: date) else { break } + date = newDate + } + + return dates + } + + private func didClicked(by index: Int) -> (Int, Bool?) { + selectedDay[index]?.toggle() + selectedDayObservable.accept(selectedDay) + + return (index, selectedDay[index]) + } + + private func createQuests(title: String, dates: [Date], quantity: String) -> [Quest] { + let groupID = UUID() + guard let totalCount = Int(quantity) else { return [] } + + return dates.map { date in + Quest(groupId: groupID, + uuid: UUID(), + date: date, + title: title, + currentCount: 0, + totalCount: totalCount + ) + } + } +}