diff --git a/Projects/Domain/Sources/Enums/DevelopmentType.swift b/Projects/Domain/Sources/Enums/DevelopmentType.swift index 98943911..c49e020f 100644 --- a/Projects/Domain/Sources/Enums/DevelopmentType.swift +++ b/Projects/Domain/Sources/Enums/DevelopmentType.swift @@ -25,4 +25,15 @@ public enum DevelopmentType: String, Codable { return "iOS" } } + + public init?(localizedString: String) { + switch localizedString { + case "전체", "All": self = .all + case "서버", "Server": self = .server + case "웹", "Web": self = .web + case "안드로이드", "Android": self = .android + case "iOS": self = .ios + default: return nil + } + } } diff --git a/Projects/Flow/Sources/MyPage/BugReport/BugReportFlow.swift b/Projects/Flow/Sources/MyPage/BugReport/BugReportFlow.swift index b03abc61..43a3f1f7 100644 --- a/Projects/Flow/Sources/MyPage/BugReport/BugReportFlow.swift +++ b/Projects/Flow/Sources/MyPage/BugReport/BugReportFlow.swift @@ -33,7 +33,7 @@ private extension BugReportFlow { func navigateToBugReport() -> FlowContributors { return .one(flowContributor: .contribute( withNextPresentable: rootViewController, - withNextStepper: rootViewController.viewModel + withNextStepper: rootViewController.reactor )) } @@ -42,7 +42,7 @@ private extension BugReportFlow { Flows.use(majorBottomSheetFlow, when: .created) { root in let view = root as? MajorBottomSheetViewController view?.dismiss = { majorType in - self.rootViewController.viewModel.majorType.accept(majorType) + self.rootViewController.reactor.action.onNext(.updateMajorType(majorType)) } self.rootViewController.present( root, diff --git a/Projects/Flow/Sources/MyPage/InterestField/InterestFieldCheckFlow.swift b/Projects/Flow/Sources/MyPage/InterestField/InterestFieldCheckFlow.swift index dad89a69..da84423c 100644 --- a/Projects/Flow/Sources/MyPage/InterestField/InterestFieldCheckFlow.swift +++ b/Projects/Flow/Sources/MyPage/InterestField/InterestFieldCheckFlow.swift @@ -28,7 +28,7 @@ public final class InterestFieldCheckFlow: Flow { return navigateToInterestFieldCheck() case .popHomeFieldIsRequired: - return navigateToInterestField() + return popToMyPage() } } } @@ -37,7 +37,7 @@ private extension InterestFieldCheckFlow { func navigateToInterestField() -> FlowContributors { return .one(flowContributor: .contribute( withNextPresentable: rootViewController, - withNextStepper: rootViewController.viewModel + withNextStepper: rootViewController.reactor )) } @@ -51,11 +51,12 @@ private extension InterestFieldCheckFlow { return .one(flowContributor: .contribute( withNextPresentable: interestFieldCheckViewController, - withNextStepper: interestFieldCheckViewController.viewModel + withNextStepper: interestFieldCheckViewController.reactor )) } -// func navigateToHomeField() -> FlowContributors { -// -// } + func popToMyPage() -> FlowContributors { + rootViewController.navigationController?.popToRootViewController(animated: true) + return .none + } } diff --git a/Projects/Flow/Sources/MyPage/InterestField/InterestFieldFlow.swift b/Projects/Flow/Sources/MyPage/InterestField/InterestFieldFlow.swift index 4981d0b5..e8c04962 100644 --- a/Projects/Flow/Sources/MyPage/InterestField/InterestFieldFlow.swift +++ b/Projects/Flow/Sources/MyPage/InterestField/InterestFieldFlow.swift @@ -18,14 +18,23 @@ public final class InterestFieldFlow: Flow { } public func navigate(to step: Step) -> FlowContributors { - guard let step = step as? InterestFieldStep else { return .none } - - switch step { - case .interestFieldIsRequired: - return navigateToInterestField() - case .interestFieldCheckIsRequired: - return navigateToInterestFieldCheck() + if let step = step as? InterestFieldStep { + switch step { + case .interestFieldIsRequired: + return navigateToInterestField() + case .interestFieldCheckIsRequired: + return navigateToInterestFieldCheck() + } + } else if let step = step as? InterestFieldCheckStep { + switch step { + case .popHomeFieldIsRequired: + return popToMyPage() + default: + return .none + } } + + return .none } } @@ -33,7 +42,7 @@ private extension InterestFieldFlow { func navigateToInterestField() -> FlowContributors { return .one(flowContributor: .contribute( withNextPresentable: rootViewController, - withNextStepper: rootViewController.viewModel + withNextStepper: rootViewController.reactor )) } @@ -43,7 +52,12 @@ private extension InterestFieldFlow { return .one(flowContributor: .contribute( withNextPresentable: interestFieldCheckViewController, - withNextStepper: interestFieldCheckViewController.viewModel + withNextStepper: interestFieldCheckViewController.reactor )) } + + func popToMyPage() -> FlowContributors { + rootViewController.navigationController?.popToRootViewController(animated: true) + return .none + } } diff --git a/Projects/Flow/Sources/MyPage/Notification/NotificationSettingFlow.swift b/Projects/Flow/Sources/MyPage/Notification/NotificationSettingFlow.swift index 9d53546d..c53847dd 100644 --- a/Projects/Flow/Sources/MyPage/Notification/NotificationSettingFlow.swift +++ b/Projects/Flow/Sources/MyPage/Notification/NotificationSettingFlow.swift @@ -30,7 +30,7 @@ private extension NotificationSettingFlow { func navigateToNotificationSetting() -> FlowContributors { return .one(flowContributor: .contribute( withNextPresentable: rootViewController, - withNextStepper: rootViewController.viewModel + withNextStepper: rootViewController.reactor )) } } diff --git a/Projects/Presentation/Sources/BugReport/BugReportReactor.swift b/Projects/Presentation/Sources/BugReport/BugReportReactor.swift new file mode 100644 index 00000000..a7f84c25 --- /dev/null +++ b/Projects/Presentation/Sources/BugReport/BugReportReactor.swift @@ -0,0 +1,110 @@ +import ReactorKit +import RxSwift +import RxCocoa +import RxFlow +import Core +import Domain + +public final class BugReportReactor: BaseReactor, Stepper { + public let steps = PublishRelay() + public let initialState: State + private let reportBugUseCase: ReportBugUseCase + + public init( + reportBugUseCase: ReportBugUseCase + ) { + self.initialState = .init() + self.reportBugUseCase = reportBugUseCase + } + + public enum Action { + case updateTitle(String) + case updateContent(String) + case updateImageList([String]) + case updateMajorType(String) + case majorViewDidTap + case bugReportButtonDidTap + } + + public enum Mutation { + case setTitle(String) + case setContent(String) + case setImageList([String]) + case setMajorType(String) + case setBugReportButtonIsEnabled(Bool) + case setBugReportCompleted + } + + public struct State { + var title: String = "" + var content: String = "" + var imageList: [String] = [] + var majorType: String = "전체" + var isBugReportButtonEnabled: Bool = false + var isBugReportCompleted: Bool = false + } +} + +extension BugReportReactor { + public func mutate(action: Action) -> Observable { + switch action { + case let .updateTitle(title): + let isEnabled = !title.isEmpty && !currentState.content.isEmpty + return .concat([ + .just(.setTitle(title)), + .just(.setBugReportButtonIsEnabled(isEnabled)) + ]) + + case let .updateContent(content): + let isEnabled = !currentState.title.isEmpty && !content.isEmpty + return .concat([ + .just(.setContent(content)), + .just(.setBugReportButtonIsEnabled(isEnabled)) + ]) + + case let .updateImageList(imageList): + return .just(.setImageList(imageList)) + + case let .updateMajorType(majorType): + return .just(.setMajorType(majorType)) + + case .majorViewDidTap: + steps.accept(BugReportStep.majorBottomSheetIsRequired) + return .empty() + + case .bugReportButtonDidTap: + return reportBugUseCase.execute(req: .init( + title: currentState.title, + content: currentState.content, + developmentArea: DevelopmentType(localizedString: currentState.majorType) ?? .all, + attachmentUrls: currentState.imageList + )) + .asObservable() + .map { _ in Mutation.setBugReportCompleted } + } + } + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case let .setTitle(title): + newState.title = title + + case let .setContent(content): + newState.content = content + + case let .setImageList(imageList): + newState.imageList = imageList + + case let .setMajorType(majorType): + newState.majorType = majorType + + case let .setBugReportButtonIsEnabled(isEnabled): + newState.isBugReportButtonEnabled = isEnabled + + case .setBugReportCompleted: + newState.isBugReportCompleted = true + } + return newState + } +} diff --git a/Projects/Presentation/Sources/BugReport/BugReportViewController.swift b/Projects/Presentation/Sources/BugReport/BugReportViewController.swift index 026d41b5..7f495598 100644 --- a/Projects/Presentation/Sources/BugReport/BugReportViewController.swift +++ b/Projects/Presentation/Sources/BugReport/BugReportViewController.swift @@ -7,13 +7,9 @@ import Core import DesignSystem import PhotosUI -public final class BugReportViewController: BaseViewController { - private let bugReportButtonDidTap = PublishRelay() - private let bugReportImageList = BehaviorRelay<[String]>(value: []) +public final class BugReportViewController: BaseReactorViewController { var imageStringList: [String] = [] var imageList: [UIImage] = [] - var titleBool: Bool = false - var contentBool: Bool = false private let bugReportMajorView = BugReportMajorView() private let bugReportTitleTextField = JobisTextField().then { @@ -154,27 +150,41 @@ public final class BugReportViewController: BaseViewController() - private let disposeBag = DisposeBag() - public var majorType = BehaviorRelay(value: "전체") - - private let reportBugUseCase: ReportBugUseCase - - init( - reportBugUseCase: ReportBugUseCase - ) { - self.reportBugUseCase = reportBugUseCase - } - - public struct Input { - let title: Driver - let content: Driver - let bugReportImageList: BehaviorRelay<[String]> - let majorViewDidTap: PublishRelay - let bugReportButtonDidTap: PublishRelay - } - - public struct Output { - let majorType: BehaviorRelay - let bugReportButtonIsEnable: PublishRelay - } - - public func transform(_ input: Input) -> Output { - let bugReportButtonIsEnable = PublishRelay() - let info = Driver.combineLatest(input.title, input.content) - - input.majorViewDidTap.asObservable() - .map { _ in BugReportStep.majorBottomSheetIsRequired } - .bind(to: steps) - .disposed(by: disposeBag) - - input.bugReportButtonDidTap.asObservable() - .withLatestFrom(info) - .flatMap { title, content in - self.reportBugUseCase.execute(req: .init( - title: title, - content: content, - developmentArea: DevelopmentType(rawValue: self.majorType.value.uppercased()) ?? .all, - attachmentUrls: input.bugReportImageList.value - )) - } - .subscribe() - .disposed(by: disposeBag) - - Driver.combineLatest(input.title, input.content) - .asObservable() - .map { new, check in - !new.isEmpty && !check.isEmpty - } - .bind(to: bugReportButtonIsEnable) - .disposed(by: disposeBag) - - return Output( - majorType: self.majorType, - bugReportButtonIsEnable: bugReportButtonIsEnable - ) - } -} diff --git a/Projects/Presentation/Sources/DI/Assembly/SettingPresentationAssembly.swift b/Projects/Presentation/Sources/DI/Assembly/SettingPresentationAssembly.swift index 0847832d..4f921e71 100644 --- a/Projects/Presentation/Sources/DI/Assembly/SettingPresentationAssembly.swift +++ b/Projects/Presentation/Sources/DI/Assembly/SettingPresentationAssembly.swift @@ -45,15 +45,15 @@ public final class SettingPresentationAssembly: Assembly { } // Notification Setting - container.register(NotificationSettingViewModel.self) { resolver in - NotificationSettingViewModel( + container.register(NotificationSettingReactor.self) { resolver in + NotificationSettingReactor( subscribeNotificationUseCase: resolver.resolve(SubscribeNotificationUseCase.self)!, subscribeAllNotificationUseCase: resolver.resolve(SubscribeAllNotificationUseCase.self)!, fetchSubscribeStateUseCase: resolver.resolve(FetchSubscribeStateUseCase.self)! ) } container.register(NotificationSettingViewController.self) { resolver in - NotificationSettingViewController(resolver.resolve(NotificationSettingViewModel.self)!) + NotificationSettingViewController(resolver.resolve(NotificationSettingReactor.self)!) } // Notice @@ -79,10 +79,10 @@ public final class SettingPresentationAssembly: Assembly { // Bug Report container.register(BugReportViewController.self) { resolver in - BugReportViewController(resolver.resolve(BugReportViewModel.self)!) + BugReportViewController(resolver.resolve(BugReportReactor.self)!) } - container.register(BugReportViewModel.self) { resolver in - BugReportViewModel( + container.register(BugReportReactor.self) { resolver in + BugReportReactor( reportBugUseCase: resolver.resolve(ReportBugUseCase.self)! ) } @@ -108,11 +108,11 @@ public final class SettingPresentationAssembly: Assembly { // Interest Field container.register(InterestFieldViewController.self) { resolver in InterestFieldViewController( - resolver.resolve(InterestFieldViewModel.self)! + resolver.resolve(InterestFieldReactor.self)! ) } - container.register(InterestFieldViewModel.self) { resolver in - InterestFieldViewModel( + container.register(InterestFieldReactor.self) { resolver in + InterestFieldReactor( fetchCodeListUseCase: resolver.resolve(FetchCodeListUseCase.self)!, changeInterestsUseCase: resolver.resolve(ChangeInterestsUseCase.self)!, fetchStudentInfoUseCase: resolver.resolve(FetchStudentInfoUseCase.self)! @@ -120,11 +120,11 @@ public final class SettingPresentationAssembly: Assembly { } container.register(InterestFieldCheckViewController.self) { resolver in InterestFieldCheckViewController( - resolver.resolve(InterestFieldCheckViewModel.self)! + resolver.resolve(InterestFieldCheckReactor.self)! ) } - container.register(InterestFieldCheckViewModel.self) { resolver in - InterestFieldCheckViewModel( + container.register(InterestFieldCheckReactor.self) { resolver in + InterestFieldCheckReactor( fetchStudentInfoUseCase: resolver.resolve(FetchStudentInfoUseCase.self)! ) } diff --git a/Projects/Presentation/Sources/InterestField/InterestFieldCheckReactor.swift b/Projects/Presentation/Sources/InterestField/InterestFieldCheckReactor.swift new file mode 100644 index 00000000..c3d6aa96 --- /dev/null +++ b/Projects/Presentation/Sources/InterestField/InterestFieldCheckReactor.swift @@ -0,0 +1,62 @@ +import ReactorKit +import RxSwift +import RxCocoa +import RxFlow +import Core +import Domain + +public final class InterestFieldCheckReactor: BaseReactor, Stepper { + public let steps = PublishRelay() + public let initialState: State + private let fetchStudentInfoUseCase: FetchStudentInfoUseCase + + public init(fetchStudentInfoUseCase: FetchStudentInfoUseCase) { + self.initialState = .init() + self.fetchStudentInfoUseCase = fetchStudentInfoUseCase + } + + public enum Action { + case fetchStudentInfo + case startAutoNavigation + } + + public enum Mutation { + case setStudentName(String) + } + + public struct State { + var studentName: String = "" + } +} + +extension InterestFieldCheckReactor { + public func mutate(action: Action) -> Observable { + switch action { + case .fetchStudentInfo: + return fetchStudentInfoUseCase.execute() + .asObservable() + .map { .setStudentName($0.studentName) } + + case .startAutoNavigation: + return Observable.timer( + .seconds(2), + scheduler: MainScheduler.instance + ) + .do(onNext: { [weak self] _ in + self?.steps.accept(InterestFieldCheckStep.popHomeFieldIsRequired) + }) + .flatMap { _ in Observable.empty() } + } + } + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case let .setStudentName(name): + newState.studentName = name + } + + return newState + } +} diff --git a/Projects/Presentation/Sources/InterestField/InterestFieldCheckViewController.swift b/Projects/Presentation/Sources/InterestField/InterestFieldCheckViewController.swift index 36bb2044..1b747537 100644 --- a/Projects/Presentation/Sources/InterestField/InterestFieldCheckViewController.swift +++ b/Projects/Presentation/Sources/InterestField/InterestFieldCheckViewController.swift @@ -7,7 +7,7 @@ import Core import DesignSystem import Domain -public final class InterestFieldCheckViewController: BaseViewController { +public final class InterestFieldCheckViewController: BaseReactorViewController { private let interestView = InterestCheckView() // private let backButton = JobisButton(style: .main).then { // $0.setText("홈으로 가기") @@ -38,18 +38,23 @@ public final class InterestFieldCheckViewController: BaseViewController() - private let disposeBag = DisposeBag() - - private let fetchStudentInfoUseCase: FetchStudentInfoUseCase - private let studentNameRelay = BehaviorRelay(value: "") - - public init(fetchStudentInfoUseCase: FetchStudentInfoUseCase) { - self.fetchStudentInfoUseCase = fetchStudentInfoUseCase - } - - public struct Input { - let viewWillAppear: Observable -// let backButtonTap: Observable - } - - public struct Output { - let studentName: Driver - } - - public func transform(_ input: Input) -> Output { - input.viewWillAppear - .flatMapLatest { [unowned self] in - self.fetchStudentInfoUseCase.execute().asObservable() - } - .map { $0.studentName } - .bind(to: studentNameRelay) - .disposed(by: disposeBag) - -// input.backButtonTap -// .map { InterestFieldCheckStep.popHomeFieldIsRequired } -// .bind(to: steps) -// .disposed(by: disposeBag) - - return Output( - studentName: studentNameRelay.asDriver() - ) - } -} diff --git a/Projects/Presentation/Sources/InterestField/InterestFieldReactor.swift b/Projects/Presentation/Sources/InterestField/InterestFieldReactor.swift new file mode 100644 index 00000000..13fcd47c --- /dev/null +++ b/Projects/Presentation/Sources/InterestField/InterestFieldReactor.swift @@ -0,0 +1,101 @@ +import ReactorKit +import RxSwift +import RxCocoa +import RxFlow +import Core +import Domain + +public final class InterestFieldReactor: BaseReactor, Stepper { + public let steps = PublishRelay() + public let initialState: State + private let fetchCodeListUseCase: FetchCodeListUseCase + private let changeInterestsUseCase: ChangeInterestsUseCase + private let fetchStudentInfoUseCase: FetchStudentInfoUseCase + + public init( + fetchCodeListUseCase: FetchCodeListUseCase, + changeInterestsUseCase: ChangeInterestsUseCase, + fetchStudentInfoUseCase: FetchStudentInfoUseCase + ) { + self.initialState = .init() + self.fetchCodeListUseCase = fetchCodeListUseCase + self.changeInterestsUseCase = changeInterestsUseCase + self.fetchStudentInfoUseCase = fetchStudentInfoUseCase + } + + public enum Action { + case fetchInterestFields + case toggleInterest(CodeEntity) + case selectButtonDidTap + } + + public enum Mutation { + case setAvailableInterests([CodeEntity]) + case setStudentName(String) + case toggleSelectedInterest(CodeEntity) + } + + public struct State { + var availableInterests: [CodeEntity] = [] + var selectedInterests: [CodeEntity] = [] + var studentName: String = "" + + var selectedCount: Int { + selectedInterests.count + } + } +} + +extension InterestFieldReactor { + public func mutate(action: Action) -> Observable { + switch action { + case .fetchInterestFields: + let interests = fetchCodeListUseCase.execute( + keyword: nil, + type: .job, + parentCode: nil + ) + .asObservable() + .map { Mutation.setAvailableInterests($0) } + + let studentInfo = fetchStudentInfoUseCase.execute() + .asObservable() + .map { Mutation.setStudentName($0.studentName) } + + return .merge(interests, studentInfo) + + case let .toggleInterest(interest): + return .just(.toggleSelectedInterest(interest)) + + case .selectButtonDidTap: + let codeIDs = currentState.selectedInterests.map { $0.code } + return changeInterestsUseCase.execute(codeIDs: codeIDs) + .asObservable() + .do(onCompleted: { [weak self] in + self?.steps.accept(InterestFieldStep.interestFieldCheckIsRequired) + }) + .flatMap { _ in Observable.empty() } + } + } + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case let .setAvailableInterests(interests): + newState.availableInterests = interests + + case let .setStudentName(name): + newState.studentName = name + + case let .toggleSelectedInterest(interest): + if let index = newState.selectedInterests.firstIndex(where: { $0.code == interest.code }) { + newState.selectedInterests.remove(at: index) + } else { + newState.selectedInterests.append(interest) + } + } + + return newState + } +} diff --git a/Projects/Presentation/Sources/InterestField/InterestFieldViewController.swift b/Projects/Presentation/Sources/InterestField/InterestFieldViewController.swift index 025e3f83..c57eb5a6 100644 --- a/Projects/Presentation/Sources/InterestField/InterestFieldViewController.swift +++ b/Projects/Presentation/Sources/InterestField/InterestFieldViewController.swift @@ -7,10 +7,7 @@ import Core import DesignSystem import Domain -public final class InterestFieldViewController: BaseViewController { - private let selectedIndexesRelay = BehaviorRelay>(value: []) - private let interestsRelay = BehaviorRelay<[CodeEntity]>(value: []) - private let selectedInterestsRelay = BehaviorRelay<[CodeEntity]>(value: []) +public final class InterestFieldViewController: BaseReactorViewController { private let interestFieldTitleLabel = UILabel().then { $0.setJobisText( @@ -83,56 +80,44 @@ public final class InterestFieldViewController: BaseViewController CodeEntity? in + guard let self = self else { return nil } + return self.reactor.currentState.availableInterests[safe: indexPath.item] + } + .map { InterestFieldReactor.Action.toggleInterest($0) } + .bind(to: reactor.action) + .disposed(by: disposeBag) - output.availableInterests - .bind(to: interestsRelay) + selectButton.rx.tap + .map { InterestFieldReactor.Action.selectButtonDidTap } + .bind(to: reactor.action) .disposed(by: disposeBag) + } - interestsRelay + public override func bindState() { + reactor.state.map { $0.availableInterests } .bind(to: majorCollectionView.rx.items( cellIdentifier: MajorCollectionViewCell.identifier, cellType: MajorCollectionViewCell.self )) { [weak self] index, codeEntity, cell in - let indexPath = IndexPath(item: index, section: 0) - let isSelected = self?.selectedIndexesRelay.value.contains(indexPath) ?? false + guard let self = self else { return } + let isSelected = self.reactor.currentState.selectedInterests.contains(where: { $0.code == codeEntity.code }) cell.adapt(model: codeEntity) cell.isCheck = isSelected } .disposed(by: disposeBag) - majorCollectionView.rx.itemSelected - .withUnretained(self) - .bind { owner, indexPath in - var currentSelected = owner.selectedIndexesRelay.value - - if currentSelected.contains(indexPath) { - currentSelected.remove(indexPath) - } else { - currentSelected.insert(indexPath) - } - - owner.selectedIndexesRelay.accept(currentSelected) - owner.updateSelectedInterests() - - DispatchQueue.main.async { - owner.majorCollectionView.reloadItems(at: [indexPath]) - } - } - .disposed(by: disposeBag) - - output.selectedInterests - .map { $0.count } + reactor.state.map { $0.selectedCount } .distinctUntilChanged() - .drive(onNext: { [weak self] count in + .bind(onNext: { [weak self] count in if count == 0 { self?.selectButton.setText("관심 분야를 선택해 주세요!") self?.selectButton.isEnabled = false @@ -143,8 +128,10 @@ public final class InterestFieldViewController: BaseViewController Element? { + return indices.contains(index) ? self[index] : nil } } diff --git a/Projects/Presentation/Sources/InterestField/InterestFieldViewModel.swift b/Projects/Presentation/Sources/InterestField/InterestFieldViewModel.swift deleted file mode 100644 index da2edd05..00000000 --- a/Projects/Presentation/Sources/InterestField/InterestFieldViewModel.swift +++ /dev/null @@ -1,90 +0,0 @@ -import UIKit -import RxSwift -import RxCocoa -import RxFlow -import Core -import Domain - -public final class InterestFieldViewModel: BaseViewModel, Stepper { - public let steps = PublishRelay() - private let disposeBag = DisposeBag() - private let fetchCodeListUseCase: FetchCodeListUseCase - private let changeInterestsUseCase: ChangeInterestsUseCase - private let fetchStudentInfoUseCase: FetchStudentInfoUseCase - private let studentNameRelay = BehaviorRelay(value: "") - - public init( - fetchCodeListUseCase: FetchCodeListUseCase, - changeInterestsUseCase: ChangeInterestsUseCase, - fetchStudentInfoUseCase: FetchStudentInfoUseCase - ) { - self.fetchCodeListUseCase = fetchCodeListUseCase - self.changeInterestsUseCase = changeInterestsUseCase - self.fetchStudentInfoUseCase = fetchStudentInfoUseCase - fetchStudentInfo() - } - - public struct Input { - let viewAppear: PublishRelay - let selectButtonDidTap: Signal - let selectedInterests: Observable<[CodeEntity]> - } - - public struct Output { - let availableInterests: BehaviorRelay<[CodeEntity]> - let selectedInterests: Driver<[CodeEntity]> - let studentName: Driver - } - - public func transform(_ input: Input) -> Output { - let availableInterests = BehaviorRelay<[CodeEntity]>(value: []) - - input.viewAppear.asObservable() - .flatMap { [weak self] _ -> Single<[CodeEntity]> in - guard let self = self else { return .just([]) } - return self.fetchCodeListUseCase.execute(keyword: nil, type: .job, parentCode: nil) - } - .bind(to: availableInterests) - .disposed(by: disposeBag) - - let selectedInterests = input.selectedInterests - .asDriver(onErrorJustReturn: []) - - input.selectButtonDidTap - .withLatestFrom(selectedInterests) - .emit(onNext: { [weak self] interests in - guard let self = self else { return } - - let codeIDs = interests.map { $0.code } - - self.changeInterestsUseCase.execute(codeIDs: codeIDs) - .subscribe( - onCompleted: { [weak self] in - self?.steps.accept(InterestFieldStep.interestFieldCheckIsRequired) - }, - onError: { _ in - } - ) - .disposed(by: self.disposeBag) - }) - .disposed(by: disposeBag) - - return Output( - availableInterests: availableInterests, - selectedInterests: selectedInterests, - studentName: studentNameRelay.asDriver() - ) - } - - private func fetchStudentInfo() { - fetchStudentInfoUseCase.execute() - .subscribe( - onSuccess: { [weak self] studentInfo in - self?.studentNameRelay.accept(studentInfo.studentName) - }, - onFailure: { _ in - } - ) - .disposed(by: disposeBag) - } -} diff --git a/Projects/Presentation/Sources/NotificationSetting/NotificationSettingReactor.swift b/Projects/Presentation/Sources/NotificationSetting/NotificationSettingReactor.swift new file mode 100644 index 00000000..2b722603 --- /dev/null +++ b/Projects/Presentation/Sources/NotificationSetting/NotificationSettingReactor.swift @@ -0,0 +1,109 @@ +import ReactorKit +import RxSwift +import RxCocoa +import RxFlow +import Core +import Domain +import FirebaseMessaging + +public final class NotificationSettingReactor: BaseReactor, Stepper { + public let steps = PublishRelay() + public let initialState: State + private let subscribeNotificationUseCase: SubscribeNotificationUseCase + private let subscribeAllNotificationUseCase: SubscribeAllNotificationUseCase + private let fetchSubscribeStateUseCase: FetchSubscribeStateUseCase + + public init( + subscribeNotificationUseCase: SubscribeNotificationUseCase, + subscribeAllNotificationUseCase: SubscribeAllNotificationUseCase, + fetchSubscribeStateUseCase: FetchSubscribeStateUseCase + ) { + self.initialState = .init() + self.subscribeNotificationUseCase = subscribeNotificationUseCase + self.subscribeAllNotificationUseCase = subscribeAllNotificationUseCase + self.fetchSubscribeStateUseCase = fetchSubscribeStateUseCase + } + + public enum Action { + case fetchNotificationSettings + case toggleNotification(NotificationType) + case toggleAllNotifications + } + + public enum Mutation { + case setSubscribeStates([NotificationType: Bool]) + case toggleNotificationState(NotificationType) + case setAllNotificationStates(Bool) + } + + public struct State { + var subscribeStates: [NotificationType: Bool] = [:] + + var isNoticeEnabled: Bool { + subscribeStates[.notice] ?? false + } + var isApplicationEnabled: Bool { + subscribeStates[.application] ?? false + } + var isRecruitmentEnabled: Bool { + subscribeStates[.recruitment] ?? false + } + var isWinterInternEnabled: Bool { + subscribeStates[.winterIntern] ?? false + } + var isAllNotificationEnabled: Bool { + isNoticeEnabled && isApplicationEnabled && isRecruitmentEnabled && isWinterInternEnabled + } + } +} + +extension NotificationSettingReactor { + public func mutate(action: Action) -> Observable { + switch action { + case .fetchNotificationSettings: + return fetchSubscribeStateUseCase.execute() + .asObservable() + .map { entities -> [NotificationType: Bool] in + var states: [NotificationType: Bool] = [:] + entities.forEach { states[$0.topic] = $0.isSubscribed } + return states + } + .map { .setSubscribeStates($0) } + + case let .toggleNotification(notificationType): + return subscribeNotificationUseCase.execute( + token: Messaging.messaging().fcmToken ?? "", + notificationType: notificationType + ) + .asObservable() + .map { _ in .toggleNotificationState(notificationType) } + + case .toggleAllNotifications: + let newState = !currentState.isAllNotificationEnabled + return subscribeAllNotificationUseCase.execute() + .asObservable() + .map { _ in .setAllNotificationStates(newState) } + } + } + + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case let .setSubscribeStates(states): + newState.subscribeStates = states + + case let .toggleNotificationState(notificationType): + let currentValue = newState.subscribeStates[notificationType] ?? false + newState.subscribeStates[notificationType] = !currentValue + + case let .setAllNotificationStates(isEnabled): + newState.subscribeStates[.notice] = isEnabled + newState.subscribeStates[.application] = isEnabled + newState.subscribeStates[.recruitment] = isEnabled + newState.subscribeStates[.winterIntern] = isEnabled + } + + return newState + } +} diff --git a/Projects/Presentation/Sources/NotificationSetting/NotificationSettingViewController.swift b/Projects/Presentation/Sources/NotificationSetting/NotificationSettingViewController.swift index 95a2e399..b3fefec0 100644 --- a/Projects/Presentation/Sources/NotificationSetting/NotificationSettingViewController.swift +++ b/Projects/Presentation/Sources/NotificationSetting/NotificationSettingViewController.swift @@ -6,7 +6,7 @@ import Then import Core import DesignSystem -public final class NotificationSettingViewController: BaseViewController { +public final class NotificationSettingViewController: BaseReactorViewController { private lazy var switchViewArray = [ noticeSwitchView, applicationSwitchView, @@ -87,52 +87,76 @@ public final class NotificationSettingViewController: BaseViewController() - private let disposeBag = DisposeBag() - private let subscribeNotificationUseCase: SubscribeNotificationUseCase - private let subscribeAllNotificationUseCase: SubscribeAllNotificationUseCase - private let fetchSubscribeStateUseCase: FetchSubscribeStateUseCase - - init( - subscribeNotificationUseCase: SubscribeNotificationUseCase, - subscribeAllNotificationUseCase: SubscribeAllNotificationUseCase, - fetchSubscribeStateUseCase: FetchSubscribeStateUseCase - ) { - self.subscribeNotificationUseCase = subscribeNotificationUseCase - self.subscribeAllNotificationUseCase = subscribeAllNotificationUseCase - self.fetchSubscribeStateUseCase = fetchSubscribeStateUseCase - } - - public struct Input { - let viewAppear: PublishRelay - let allSwitchButtonDidTap: ControlProperty - let noticeSwitchButtonDidTap: ControlProperty - let applicationSwitchButtonDidTap: ControlProperty - let recruitmentSwitchButtonDidTap: ControlProperty -// let interestSwitchButtonDidTap: ControlProperty - let winterInternSwitchButtonDidTap: ControlProperty - } - - public struct Output { - let subscribeNoticeState: PublishRelay - let subscribeApplicationState: PublishRelay - let subscribeRecruitmentState: PublishRelay -// let subscribeInteresteState: PublishRelay - let subscribeWinterInternState: PublishRelay - let allSubscribeState: PublishRelay - } - - public func transform(_ input: Input) -> Output { - let subscribeStateList = PublishRelay<[SubscribeStateEntity]>() - let subscribeNoticeState = PublishRelay() - let subscribeApplicationState = PublishRelay() - let subscribeRecruitmentState = PublishRelay() -// let subscribeInteresteState = PublishRelay() - let subscribeWinterInternState = PublishRelay() - - let noticeState = BehaviorRelay(value: false) - let applicationState = BehaviorRelay(value: false) - let recruitmentState = BehaviorRelay(value: false) -// let interesteState = BehaviorRelay(value: false) - let winterInternState = BehaviorRelay(value: false) - let allSubscribeState = PublishRelay() - - input.viewAppear.asObservable() - .flatMap { - self.fetchSubscribeStateUseCase.execute() - } - .bind(to: subscribeStateList) - .disposed(by: disposeBag) - - subscribeStateList.asObservable() - .subscribe(onNext: { - for state in $0 { - if state.topic == .notice { - subscribeNoticeState.accept(state) - noticeState.accept(state.isSubscribed) - } else if state.topic == .application { - subscribeApplicationState.accept(state) - applicationState.accept(state.isSubscribed) - } else if state.topic == .recruitment { - subscribeRecruitmentState.accept(state) - recruitmentState.accept(state.isSubscribed) -// } else if state.topic == .interestRecruitment { -// subscribeInteresteState.accept(state) -// interesteState.accept(state.isSubscribed) - } else if state.topic == .winterIntern { - subscribeWinterInternState.accept(state) - winterInternState.accept(state.isSubscribed) - } - - if ( - noticeState.value && - applicationState.value && - recruitmentState.value && - winterInternState.value - ) == true { - allSubscribeState.accept(true) - } else { - allSubscribeState.accept(false) - } - } - }) - .disposed(by: disposeBag) - - input.allSwitchButtonDidTap.asObservable() - .skip(1) - .flatMap { _ in - self.subscribeAllNotificationUseCase.execute() - } - .subscribe() - .disposed(by: disposeBag) - - input.noticeSwitchButtonDidTap.asObservable() - .skip(1) - .flatMap { _ in - self.subscribeNotificationUseCase.execute( - token: Messaging.messaging().fcmToken ?? "", - notificationType: .notice - ) - } - .subscribe() - .disposed(by: disposeBag) - - input.applicationSwitchButtonDidTap.asObservable() - .skip(1) - .flatMap { _ in - self.subscribeNotificationUseCase.execute( - token: Messaging.messaging().fcmToken ?? "", - notificationType: .application - ) - } - .subscribe() - .disposed(by: disposeBag) - - input.recruitmentSwitchButtonDidTap.asObservable() - .skip(1) - .flatMap { _ in - self.subscribeNotificationUseCase.execute( - token: Messaging.messaging().fcmToken ?? "", - notificationType: .recruitment - ) - } - .subscribe() - .disposed(by: disposeBag) - - input.winterInternSwitchButtonDidTap.asObservable() - .skip(1) - .flatMap { _ in - self.subscribeNotificationUseCase.execute( - token: Messaging.messaging().fcmToken ?? "", - notificationType: .winterIntern - ) - } - .subscribe() - .disposed(by: disposeBag) - - return Output( - subscribeNoticeState: subscribeNoticeState, - subscribeApplicationState: subscribeApplicationState, - subscribeRecruitmentState: subscribeRecruitmentState, -// subscribeInteresteState: subscribeInteresteState, - subscribeWinterInternState: subscribeWinterInternState, - allSubscribeState: allSubscribeState - ) - } -}