diff --git a/project/Projects/App/Sources/RootCoordinator/Auth/Worker/WorkerAuthCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Auth/Worker/WorkerAuthCoordinator.swift index c5ba2a6e..8f57a72d 100644 --- a/project/Projects/App/Sources/RootCoordinator/Auth/Worker/WorkerAuthCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Auth/Worker/WorkerAuthCoordinator.swift @@ -49,7 +49,8 @@ extension WorkerAuthCoordinator: WorkerAuthCoordinatable { let coordinator = WorkerRegisterCoordinator( navigationController: navigationController, viewModel: WorkerRegisterViewModel( - inputValidationUseCase: injector.resolve(AuthInputValidationUseCase.self) + inputValidationUseCase: injector.resolve(AuthInputValidationUseCase.self), + authUseCase: injector.resolve(AuthUseCase.self) ) ) diff --git a/project/Projects/App/Sources/RootCoordinator/TestMain/TestMainTabBarCoodinator.swift b/project/Projects/App/Sources/RootCoordinator/TestMain/TestMainTabBarCoodinator.swift index 4a397975..9436e3e4 100644 --- a/project/Projects/App/Sources/RootCoordinator/TestMain/TestMainTabBarCoodinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/TestMain/TestMainTabBarCoodinator.swift @@ -16,7 +16,7 @@ class TestMainTabBarCoodinator: ChildCoordinator { var parent: RootCoordinator? - weak var viewControllerRef: DisposableViewController? + weak var viewControllerRef: UIViewController? init(navigationController: UINavigationController) { self.navigationController = navigationController diff --git a/project/Projects/Data/ConcreteRepository/Auth/DefaultAuthRepository.swift b/project/Projects/Data/ConcreteRepository/Auth/DefaultAuthRepository.swift index e2fcaef3..067f775a 100644 --- a/project/Projects/Data/ConcreteRepository/Auth/DefaultAuthRepository.swift +++ b/project/Projects/Data/ConcreteRepository/Auth/DefaultAuthRepository.swift @@ -16,8 +16,12 @@ public class DefaultAuthRepository: AuthRepository { let networkService = AuthService() public init() { } +} + +// MARK: Center auth +public extension DefaultAuthRepository { - public func requestRegisterCenterAccount( + func requestRegisterCenterAccount( managerName: String, phoneNumber: String, businessNumber: String, @@ -39,22 +43,54 @@ public class DefaultAuthRepository: AuthRepository { .map { _ in return () } } - public func requestCenterLogin(id: String, password: String) -> Single { + func requestCenterLogin(id: String, password: String) -> Single { return networkService.requestDecodable(api: .centerLogin(id: id, password: password), with: .plain) - .flatMap { [weak self] (token: TokenDTO) in - - if let accessToken = token.accessToken, let refreshToken = token.refreshToken { - - guard let self else { fatalError() } - - if let _ = try? self.networkService.keyValueStore.saveAuthToken( - accessToken: accessToken, - refreshToken: refreshToken - ) { - return .just(()) - } - } - return .error(KeyValueStoreError.tokenSavingFailure) + .flatMap { [unowned self] in saveTokenToStore(token: $0) } + } +} + +// MARK: Worker auth +public extension DefaultAuthRepository { + + /// 요양보호사의 경우 회원가입시 곧바로 토큰을 발급받습니다. + func requestRegisterWorkerAccount(registerState: WorkerRegisterState) -> Single { + let dto = WorkerRegistrationDTO( + carerName: registerState.name, + birthYear: registerState.birthYear, + genderType: registerState.gender, + phoneNumber: registerState.phoneNumber, + roadNameAddress: registerState.addressInformation.roadAddress, + lotNumberAddress: registerState.addressInformation.jibunAddress, + longitude: registerState.latitude, + latitude: registerState.logitude + ) + + let data = (try? JSONEncoder().encode(dto)) ?? Data() + + return networkService.requestDecodable(api: .registerWorkerAccount(data: data), with: .plain) + .flatMap { [unowned self] in saveTokenToStore(token: $0) } + } + + func requestWorkerLogin(phoneNumber: String, authNumber: String) -> Single { + return networkService.requestDecodable(api: .workerLogin(phoneNumber: phoneNumber, verificationNumber: authNumber), with: .plain) + .flatMap { [unowned self] in saveTokenToStore(token: $0) } + } +} + +// MARK: Token management +extension DefaultAuthRepository { + + private func saveTokenToStore(token: TokenDTO) -> Single{ + + if let accessToken = token.accessToken, let refreshToken = token.refreshToken { + + if let _ = try? networkService.keyValueStore.saveAuthToken( + accessToken: accessToken, + refreshToken: refreshToken + ) { + return .just(()) } + } + return .error(KeyValueStoreError.tokenSavingFailure) } } diff --git a/project/Projects/Data/NetworkDataSource/API/AuthAPI.swift b/project/Projects/Data/NetworkDataSource/API/AuthAPI.swift index d9a08848..6051ba67 100644 --- a/project/Projects/Data/NetworkDataSource/API/AuthAPI.swift +++ b/project/Projects/Data/NetworkDataSource/API/AuthAPI.swift @@ -11,7 +11,7 @@ import Alamofire public enum AuthAPI { - // Core + // Common case startPhoneNumberAuth(phoneNumber: String) case checkAuthNumber(phoneNumber: String, authNumber: String) @@ -21,6 +21,10 @@ public enum AuthAPI { case registerCenterAccount(data: Data) case centerLogin(id: String, password: String) case reissueToken(refreshToken: String) + + // Worker + case registerWorkerAccount(data: Data) + case workerLogin(phoneNumber: String, verificationNumber: String) } extension AuthAPI: BaseAPI { @@ -44,15 +48,19 @@ extension AuthAPI: BaseAPI { return .post case .reissueToken: return .post + case .registerWorkerAccount: + return .post + case .workerLogin: + return .post } } public var path: String { switch self { case .startPhoneNumberAuth: - "core/send" + "common/send" case .checkAuthNumber: - "core/confirm" + "common/confirm" case .authenticateBusinessNumber(let businessNumber): "center/authentication/\(businessNumber)" case .checkIdDuplication(id: let id): @@ -63,6 +71,10 @@ extension AuthAPI: BaseAPI { "center/login" case .reissueToken: "center/refresh" + case .registerWorkerAccount: + "auth/carer/join" + case .workerLogin: + "auth/carer/login" } } @@ -79,6 +91,9 @@ extension AuthAPI: BaseAPI { params["password"] = password case .reissueToken(let refreshToken): params["refreshToken"] = refreshToken + case .workerLogin(let phoneNumber, let verificationNumber): + params["phoneNumber"] = phoneNumber + params["verificationNumber"] = verificationNumber default: break } @@ -104,6 +119,10 @@ extension AuthAPI: BaseAPI { return .requestParameters(parameters: bodyParameters ?? [:], encoding: parameterEncoding) case .reissueToken: return .requestParameters(parameters: bodyParameters ?? [:], encoding: parameterEncoding) + case .registerWorkerAccount(let data): + return .requestData(data) + case .workerLogin: + return .requestParameters(parameters: bodyParameters ?? [:], encoding: parameterEncoding) default: return .requestPlain } diff --git a/project/Projects/Data/NetworkDataSource/DTO/Auth/WorkerRegistrationDTO.swift b/project/Projects/Data/NetworkDataSource/DTO/Auth/WorkerRegistrationDTO.swift new file mode 100644 index 00000000..3ffa19df --- /dev/null +++ b/project/Projects/Data/NetworkDataSource/DTO/Auth/WorkerRegistrationDTO.swift @@ -0,0 +1,40 @@ +// +// WorkerRegistrationDTO.swift +// NetworkDataSource +// +// Created by choijunios on 7/24/24. +// + +import Foundation +import Entity + +public struct WorkerRegistrationDTO: Encodable { + public let carerName: String + public let birthYear: Int + public let genderType: String + public let phoneNumber: String + public let roadNameAddress: String + public let lotNumberAddress: String + public let longitude: String + public let latitude: String + + public init(carerName: String, birthYear: Int, genderType: Gender, phoneNumber: String, roadNameAddress: String, lotNumberAddress: String, longitude: String, latitude: String) { + self.carerName = carerName + self.birthYear = birthYear + self.genderType = Self.convertGenderValue(genderType) + self.phoneNumber = phoneNumber + self.roadNameAddress = roadNameAddress + self.lotNumberAddress = lotNumberAddress + self.longitude = longitude + self.latitude = latitude + } +} + +public extension WorkerRegistrationDTO { + + static func convertGenderValue(_ gender: Gender) -> String { + + if gender == .notDetermined { fatalError() } + return gender == .male ? "MAN" : "WOMAN" + } +} diff --git a/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthUseCase.swift b/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthUseCase.swift index 15340910..0e1ca968 100644 --- a/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthUseCase.swift @@ -19,7 +19,7 @@ public class DefaultAuthUseCase: AuthUseCase { self.repository = repository } - // MARK: 센터 회원가입 실행 + /// 센터 회원가입 실행 public func registerCenterAccount(registerState: CenterRegisterState) -> Single> { convert( task: repository.requestRegisterCenterAccount( @@ -31,10 +31,24 @@ public class DefaultAuthUseCase: AuthUseCase { )) { [unowned self] error in toDomainError(error: error) } } - // MARK: 센터 로그인 실행 + /// 센터 로그인 실행 public func loginCenterAccount(id: String, password: String) -> Single> { convert(task: repository.requestCenterLogin(id: id, password: password)) { [unowned self] error in toDomainError(error: error) } } + + /// 요양 보호사 회원가입 실행 + public func registerWorkerAccount(registerState: WorkerRegisterState) -> Single> { + convert( + task: repository.requestRegisterWorkerAccount(registerState: registerState)) { [unowned self] error in toDomainError(error: error) + } + } + + /// 요양 보호사 로그인 실행 + public func loginWorkerAccount(phoneNumber: String, authNumber: String) -> Single> { + convert( + task: repository.requestWorkerLogin(phoneNumber: phoneNumber, authNumber: authNumber)) { [unowned self] error in toDomainError(error: error) + } + } } diff --git a/project/Projects/Domain/Entity/State/Auth/Worker/WorkerRegisterState.swift b/project/Projects/Domain/Entity/State/Auth/Worker/WorkerRegisterState.swift index abbc0331..3fc0068c 100644 --- a/project/Projects/Domain/Entity/State/Auth/Worker/WorkerRegisterState.swift +++ b/project/Projects/Domain/Entity/State/Auth/Worker/WorkerRegisterState.swift @@ -10,19 +10,23 @@ import Foundation public class WorkerRegisterState { public var name: String = "" public var gender: Gender = .notDetermined + public var birthYear: Int = 0 public var phoneNumber: String = "" public var addressInformation: AddressInformation = .init(roadAddress: "", jibunAddress: "") - public var detailAddress: String = "" + public var latitude: String = "" + public var logitude: String = "" public init() { } public func clear() { name = "" gender = .notDetermined + birthYear = 0 phoneNumber = "" addressInformation.roadAddress = "" addressInformation.jibunAddress = "" - detailAddress = "" + latitude = "" + logitude = "" } } diff --git a/project/Projects/Domain/Entity/VO/BusinessInfoVO.swift b/project/Projects/Domain/Entity/VO/BusinessInfoVO.swift index 245aaabd..7d77b6e4 100644 --- a/project/Projects/Domain/Entity/VO/BusinessInfoVO.swift +++ b/project/Projects/Domain/Entity/VO/BusinessInfoVO.swift @@ -28,4 +28,16 @@ public class BusinessInfoVO { "센터 소개": "안녕하세요. 홍길동 센터입니다." ] ) + + public static let onError: BusinessInfoVO = .init( + name: "오류가 발생했습니다.", + keyValue: [ + "이름": "오류가 발생했습니다.", + "전화번호": "오류가 발생했습니다.", + "주소": "오류가 발생했습니다.", + "운영시간": "오류가 발생했습니다.", + "휴무일": "오류가 발생했습니다.", + "센터 소개": "오류가 발생했습니다.", + ] + ) } diff --git a/project/Projects/Domain/RepositoryInterface/Auth/Login/AuthRepository.swift b/project/Projects/Domain/RepositoryInterface/Auth/Login/AuthRepository.swift index 6c6239dd..f73ac6da 100644 --- a/project/Projects/Domain/RepositoryInterface/Auth/Login/AuthRepository.swift +++ b/project/Projects/Domain/RepositoryInterface/Auth/Login/AuthRepository.swift @@ -9,7 +9,12 @@ import RxSwift import Entity public protocol AuthRepository: RepositoryBase { - + + // MARK: Center func requestRegisterCenterAccount(managerName: String, phoneNumber: String, businessNumber: String, id: String, password: String) -> Single func requestCenterLogin(id: String, password: String) -> Single + + // MARK: Worker + func requestRegisterWorkerAccount(registerState: WorkerRegisterState) -> Single + func requestWorkerLogin(phoneNumber: String, authNumber: String) -> Single } diff --git a/project/Projects/Domain/UseCaseInterface/Auth/AuthUseCase.swift b/project/Projects/Domain/UseCaseInterface/Auth/AuthUseCase.swift index 88f46e5e..604ab016 100644 --- a/project/Projects/Domain/UseCaseInterface/Auth/AuthUseCase.swift +++ b/project/Projects/Domain/UseCaseInterface/Auth/AuthUseCase.swift @@ -37,4 +37,17 @@ public protocol AuthUseCase: UseCaseBase { id: String, password: String ) -> Single> + + + // #4 + /// 요양 보호사 회원가입 실행 + func registerWorkerAccount( + registerState: WorkerRegisterState + ) -> Single> + // #5 + /// 요양 보호사 로그인 실행 + func loginWorkerAccount( + phoneNumber: String, + authNumber: String + ) -> Single> } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Center/CenterAuthMainCoordinator.swift b/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Center/CenterAuthMainCoordinator.swift index 888dcfad..c50ca37a 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Center/CenterAuthMainCoordinator.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Center/CenterAuthMainCoordinator.swift @@ -12,7 +12,7 @@ public class CenterAuthMainCoordinator: ChildCoordinator { public var navigationController: UINavigationController - public weak var viewControllerRef: DisposableViewController? + public weak var viewControllerRef: UIViewController? public var parent: CenterAuthCoordinatable? diff --git a/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Center/CenterLoginCoordinator.swift b/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Center/CenterLoginCoordinator.swift index 8f4f0757..c3a19aba 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Center/CenterLoginCoordinator.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Center/CenterLoginCoordinator.swift @@ -10,7 +10,7 @@ import PresentationCore public class CenterLoginCoordinator: ChildCoordinator { - public weak var viewControllerRef: (any PresentationCore.DisposableViewController)? + public weak var viewControllerRef: UIViewController? public var navigationController: UINavigationController diff --git a/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Center/CenterRegisterCoordinator.swift b/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Center/CenterRegisterCoordinator.swift index ce12113e..e015a341 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Center/CenterRegisterCoordinator.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Center/CenterRegisterCoordinator.swift @@ -22,13 +22,13 @@ public class CenterRegisterCoordinator: ChildCoordinator { public var parent: CenterAuthCoordinatable? - public weak var viewControllerRef: (any PresentationCore.DisposableViewController)? + public weak var viewControllerRef: UIViewController? weak var pageViewController: UIPageViewController? public var navigationController: UINavigationController - var stageViewControllers: [DisposableViewController] = [] + var stageViewControllers: [UIViewController] = [] private var currentStage: CenterRegisterStage! @@ -75,8 +75,6 @@ public class CenterRegisterCoordinator: ChildCoordinator { public func coordinatorDidFinish() { - viewControllerRef?.cleanUp() - stageViewControllers = [] popViewController() diff --git a/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Center/CenterSetNewPasswordCoordinator.swift b/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Center/CenterSetNewPasswordCoordinator.swift index 68dbbd90..db501de5 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Center/CenterSetNewPasswordCoordinator.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Center/CenterSetNewPasswordCoordinator.swift @@ -18,10 +18,10 @@ enum SetNewPasswordStage: Int { public class CenterSetNewPasswordCoordinator: ChildCoordinator { - public weak var viewControllerRef: (any PresentationCore.DisposableViewController)? + public weak var viewControllerRef: UIViewController? public var navigationController: UINavigationController - var stageViewControllers: [DisposableViewController] = [] + var stageViewControllers: [UIViewController] = [] weak var pageViewController: UIPageViewController? public var parent: CenterAuthCoordinatable? diff --git a/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/SelectAuthTypeCoordinator.swift b/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/SelectAuthTypeCoordinator.swift index 8baa8d33..ef125f39 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/SelectAuthTypeCoordinator.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/SelectAuthTypeCoordinator.swift @@ -12,7 +12,7 @@ public class SelectAuthTypeCoordinator: ChildCoordinator { public var navigationController: UINavigationController - public weak var viewControllerRef: DisposableViewController? + public weak var viewControllerRef: UIViewController? public var parent: AuthCoordinatable? diff --git a/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Worker/WorkerAuthMainCoodinator.swift b/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Worker/WorkerAuthMainCoodinator.swift index ca693da7..9903b76d 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Worker/WorkerAuthMainCoodinator.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Worker/WorkerAuthMainCoodinator.swift @@ -12,7 +12,7 @@ public class WorkerAuthMainCoodinator: ChildCoordinator { public var navigationController: UINavigationController - public weak var viewControllerRef: DisposableViewController? + public weak var viewControllerRef: UIViewController? public var parent: WorkerAuthCoordinatable? diff --git a/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Worker/WorkerRegisterCoordinator.swift b/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Worker/WorkerRegisterCoordinator.swift index 726455f9..2345a66b 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Worker/WorkerRegisterCoordinator.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/Coordinator/Worker/WorkerRegisterCoordinator.swift @@ -11,11 +11,10 @@ import PresentationCore enum WorkerRegisterStage: Int { case registerFinished=0 - case name=1 - case gender=2 - case phoneNumber=3 - case address=4 - case finish=5 + case info=1 + case phoneNumber=2 + case address=3 + case finish=4 } public class WorkerRegisterCoordinator: ChildCoordinator { @@ -24,11 +23,11 @@ public class WorkerRegisterCoordinator: ChildCoordinator { public let navigationController: UINavigationController - public weak var viewControllerRef: DisposableViewController? + public weak var viewControllerRef: UIViewController? weak var pageViewController: UIPageViewController? - var stageViewControllers: [DisposableViewController] = [] + var stageViewControllers: [UIViewController] = [] private var currentStage: WorkerRegisterStage! @@ -40,8 +39,7 @@ public class WorkerRegisterCoordinator: ChildCoordinator { self.navigationController = navigationController self.stageViewControllers = [ - EnterNameViewController(coordinator: self, viewModel: viewModel), - SelectGenderViewController(coordinator: self, viewModel: viewModel), + EntetPersonalInfoViewController(coordinator: self, viewModel: viewModel), ValidatePhoneNumberViewController(coordinator: self, viewModel: viewModel), EnterAddressViewController(coordinator: self, viewModel: viewModel), ] @@ -72,13 +70,11 @@ public class WorkerRegisterCoordinator: ChildCoordinator { navigationController.pushViewController(viewController, animated: true) - excuteStage(.name, moveTo: .next) + excuteStage(.info, moveTo: .next) } public func coordinatorDidFinish() { - viewControllerRef?.cleanUp() - stageViewControllers = [] parent?.removeChildCoordinator(self) @@ -133,9 +129,7 @@ extension WorkerRegisterCoordinator { } func authFinished() { - stageViewControllers = [] - parent?.authFinished() } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/CenterLoginViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/CenterLoginViewController.swift index 840f78bf..7045d86d 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/CenterLoginViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/CenterLoginViewController.swift @@ -10,7 +10,7 @@ import RxSwift import DSKit import PresentationCore -public class CenterLoginViewController: DisposableViewController { +public class CenterLoginViewController: BaseViewController { let viewModel: CenterLoginViewModel @@ -163,26 +163,19 @@ public class CenterLoginViewController: DisposableViewController { // MARK: Input let input = viewModel.input - // 로그인 버튼 활성화 - Observable - .combineLatest( - idField.eventPublisher, - passwordField.eventPublisher - ) - .subscribe(onNext: { [weak self] in - self?.ctaButton.setEnabled(!($0.isEmpty || $1.isEmpty)) - }) + // 인풋 전달 + idField.uITextField.rx.text + .compactMap { $0 } + .bind(to: input.editingId) + .disposed(by: disposeBag) + + passwordField.uITextField.rx.text + .compactMap { $0 } + .bind(to: input.editingPassword) .disposed(by: disposeBag) // 로그인 버튼 눌림 ctaButton.eventPublisher - .map { [weak self] _ in - - let id = self?.idField.uITextField.text ?? "" - let pw = self?.passwordField.uITextField.text ?? "" - - return (id: id, pw: pw) - } .bind(to: input.loginButtonPressed) .disposed(by: disposeBag) @@ -190,9 +183,8 @@ public class CenterLoginViewController: DisposableViewController { // 로그인 시도 결과 수신 output - .loginValidation - .compactMap { $0 } - .subscribe(onNext: { [weak self] isSuccess in + .loginValidation? + .drive(onNext: { [weak self] isSuccess in if isSuccess { self?.onLoginSucceed() } else { @@ -225,9 +217,6 @@ public class CenterLoginViewController: DisposableViewController { .onCustomState { textField in textField.layer.borderColor = DSKitColors.Color.red.cgColor } - - // Alert - } private func onLoginSucceed() { diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/ValidateNewPasswordViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/ValidateNewPasswordViewController.swift index 6414563f..a2367ed8 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/ValidateNewPasswordViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Login/ValidateNewPasswordViewController.swift @@ -12,11 +12,11 @@ import RxSwift import PresentationCore public protocol ChangePasswordSuccessInputable { - var changePasswordButtonClicked: PublishRelay { get } + var changePasswordButtonClicked: PublishRelay { get } } public protocol ChangePasswordSuccessOutputable { - var changePasswordSuccessValidation: PublishRelay { get } + var changePasswordValidation: Driver? { get set } } class ValidateNewPasswordViewController: DisposableViewController @@ -187,7 +187,7 @@ where T.Input: SetPasswordInputable & ChangePasswordSuccessInputable, checkPasswordField.eventPublisher ) .map({ ($0, $1) }) - .bind(to: input.editingPassword) + .bind(to: input.editingPasswords) .disposed(by: disposeBag) ctaButton @@ -204,9 +204,8 @@ where T.Input: SetPasswordInputable & ChangePasswordSuccessInputable, // 비밀번호 검증 output - .passwordValidation - .compactMap { $0 } - .subscribe(onNext: { [weak self] validationState in + .passwordValidation? + .drive(onNext: { [weak self] validationState in switch validationState { case .invalidPassword: self?.onPasswordUnMatched() @@ -220,9 +219,8 @@ where T.Input: SetPasswordInputable & ChangePasswordSuccessInputable, .disposed(by: disposeBag) output - .changePasswordSuccessValidation - .compactMap { $0 } - .subscribe (onNext: { [weak self] isSuccess in + .changePasswordValidation? + .drive (onNext: { [weak self] isSuccess in if isSuccess { // 비밀번호 변경 성공 diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/AuthBusinessOwnerViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/AuthBusinessOwnerViewController.swift index 95018c06..fea73ad9 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/AuthBusinessOwnerViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/AuthBusinessOwnerViewController.swift @@ -13,13 +13,14 @@ import RxCocoa import PresentationCore public protocol AuthBusinessOwnerInputable { - var editingBusinessNumber: PublishRelay { get set } - var requestBusinessNumberValidation: PublishRelay { get set } + var editingBusinessNumber: BehaviorRelay { get set } + var requestBusinessNumberValidation: PublishRelay { get set } } public protocol AuthBusinessOwnerOutputable { - var canSubmitBusinessNumber: PublishRelay { get set } - var businessNumberValidation: PublishRelay { get set } + var canSubmitBusinessNumber: Driver? { get set } + var businessNumberVO: Driver? { get set } + var businessNumberValidationFailrue: Driver? { get set } } public class AuthBusinessOwnerViewController: DisposableViewController @@ -167,12 +168,14 @@ where T.Input: AuthBusinessOwnerInputable, T.Output: AuthBusinessOwnerOutputable businessNumberField .idleTextField .textField.rx.text + .compactMap { $0 } .bind(to: input.editingBusinessNumber) .disposed(by: disposeBag) // 인증, 확인 버튼이 눌린 경우 businessNumberField .eventPublisher + .map { _ in () } .bind(to: input.requestBusinessNumberValidation) .disposed(by: disposeBag) @@ -181,9 +184,7 @@ where T.Input: AuthBusinessOwnerInputable, T.Output: AuthBusinessOwnerOutputable // 입력중인 사업자 번호가 특정 조건(ex: 입력길이)을 만족한 경우 '인증'버튼 활성화 output - .canSubmitBusinessNumber - .asDriver(onErrorJustReturn: nil) - .compactMap { $0 } + .canSubmitBusinessNumber? .drive(onNext: { [weak self] isValid in self?.businessNumberField.button.setEnabled(isValid) }) @@ -191,18 +192,20 @@ where T.Input: AuthBusinessOwnerInputable, T.Output: AuthBusinessOwnerOutputable // 사업자 번호 조회 결과 output - .businessNumberValidation - .asDriver(onErrorJustReturn: nil) - .drive(onNext: { [weak self] info in - if let centerInfo = info { - printIfDebug("✅ \(centerInfo.name) 조회결과") - self?.displayCenterInfo(vo: centerInfo) - self?.ctaButton.setEnabled(true) - } else { - // 정보가 없는 경우 - self?.dismissCenterInfo() - self?.ctaButton.setEnabled(false) - } + .businessNumberVO? + .drive(onNext: { [weak self] vo in + printIfDebug("✅ \(vo.name) 조회결과") + self?.displayCenterInfo(vo: vo) + self?.ctaButton.setEnabled(true) + }) + .disposed(by: disposeBag) + + output + .businessNumberValidationFailrue? + .drive(onNext: { [weak self] in + // 정보가 없는 경우 + self?.dismissCenterInfo() + self?.ctaButton.setEnabled(false) }) .disposed(by: disposeBag) @@ -215,7 +218,6 @@ where T.Input: AuthBusinessOwnerInputable, T.Output: AuthBusinessOwnerOutputable } private func displayCenterInfo(vo: BusinessInfoVO) { - centerInfoBox.update( titleText: vo.name, items: vo.keyValue.map { (key: $0, value: $1) } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/SetIdPasswordViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/SetIdPasswordViewController.swift index 9556a9d6..c9fe9eae 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/SetIdPasswordViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Center/Register/SetIdPasswordViewController.swift @@ -13,28 +13,26 @@ import RxCocoa import PresentationCore public protocol SetIdInputable { - var editingId: PublishRelay { get set } - var requestIdDuplicationValidation: PublishRelay { get set } + var editingId: BehaviorRelay { get set } + var requestIdDuplicationValidation: PublishRelay { get set } } -public protocol SetPasswordInputable { - var editingPassword: PublishRelay<(pwd: String, cpwd: String)?> { get set } +public protocol SetIdOutputable { + var canCheckIdDuplication: Driver? { get set } + var idDuplicationValidation: Driver? { get set } } -public protocol SetIdOutputable { - - var canCheckIdDuplication: PublishRelay { get set } - var idDuplicationValidation: PublishRelay { get set } +public protocol SetPasswordInputable { + var editingPasswords: PublishRelay<(pwd: String, cpwd: String)> { get set } } public protocol SetPasswordOutputable { - - var passwordValidation: PublishRelay { get set } + var passwordValidation: Driver? { get set } } -class SetIdPasswordViewController: DisposableViewController +class SetIdPasswordViewController: BaseViewController where T.Input: SetIdInputable & SetPasswordInputable & CTAButtonEnableInputable, - T.Output: SetIdOutputable & SetPasswordOutputable & RegisterSuccessOutputable { + T.Output: SetIdOutputable & SetPasswordOutputable & RegisterValidationOutputable { var coordinator: CenterRegisterCoordinator? @@ -246,17 +244,17 @@ where T.Input: SetIdInputable & SetPasswordInputable & CTAButtonEnableInputable, // 현재 입력중인 정보 전송 idField.idleTextField.textField.rx.text + .compactMap{ $0 } .bind(to: input.editingId) .disposed(by: disposeBag) - Observable .combineLatest( passwordField.eventPublisher, checkPasswordField.eventPublisher ) .map({ ($0, $1) }) - .bind(to: input.editingPassword) + .bind(to: input.editingPasswords) .disposed(by: disposeBag) ctaButton @@ -280,90 +278,63 @@ where T.Input: SetIdInputable & SetPasswordInputable & CTAButtonEnableInputable, // 중복확인이 가능한 아이디인가? output - .canCheckIdDuplication - .compactMap { $0 } - .subscribe(onNext: { [weak self] in + .canCheckIdDuplication? + .drive(onNext: { [weak self] in self?.idField.button.setEnabled($0) }) .disposed(by: disposeBag) // 아이디 중복확인 결과 let idDuplicationValidation = output - .idDuplicationValidation - .compactMap { [weak self] checkedId in - // 아이디 입력 필드 활성화 - self?.idField.idleTextField.setEnabled(true) - return checkedId + .idDuplicationValidation? + .map { [weak self] isSuccess in + self?.idField.idleTextField.setEnabled(isSuccess) + return isSuccess } + .asObservable() ?? .empty() - // 중복검사를 통과한 아이디와 입력창의 아이디가 일치하는지 확인한다. - let finalIdValidation = Observable - .combineLatest( - idField.idleTextField.eventPublisher, - idDuplicationValidation - ) - .observe(on: MainScheduler.instance) - .map { [weak self] (editingId, validId) in - let isValid = editingId == validId - self?.thisIsValidIdLabel.isHidden = !isValid - return isValid + // 비밀번호 검증 결과 + let passwordValidationResult = output + .passwordValidation? + .map { state in + state == .match } - - // 비밀번호 검증 - let finalPaswordValidation = output - .passwordValidation - .compactMap { $0 } - .map({ [weak self] validationState in - switch validationState { - - case .invalidPassword: - printIfDebug("❌ 비밀번호가 유효하지 않습니다.") - self?.onPasswordUnMatched() - return false - case .unMatch: - printIfDebug("☑️ 비밀번호가 일치하지 않습니다.") - self?.onPasswordUnMatched() - return false - case .match: - printIfDebug("✅ 비밀번호가 일치합니다.") - self?.onPasswordMatched() - self?.ctaButton.setEnabled(true) - return true - } - }) + .asObservable() ?? .empty() // id, password 유효성 검사 Observable .combineLatest( - finalIdValidation, - finalPaswordValidation + idDuplicationValidation, + passwordValidationResult ) .map { $0 && $1 } .subscribe(onNext: { [weak self] in self?.ctaButton.setEnabled($0) }) .disposed(by: disposeBag) - output - .registerValidation - .compactMap { $0 } - .subscribe { [weak self] isSuccess in - if isSuccess { - // 회원가입 성공 - self?.coordinator?.next() - } else { - // 회원가입실패 - self?.ctaButton.setEnabled(true) - } + .registerValidation? + .drive { [weak self] isSuccess in + + // 로그인 시도 + + // 회원가입 성공 + self?.coordinator?.next() } .disposed(by: disposeBag) - + + // 경고창 표시 로직 + output + .alert? + .drive(onNext: { [weak self] vo in + self?.showAlert(vo: vo) + }) + .disposed(by: disposeBag) // MARK: ViewController한정 로직 // CTA버튼 클릭시 버튼 비활성화 ctaButton .eventPublisher .subscribe { [weak self] _ in - self?.ctaButton.setEnabled(false) } .disposed(by: disposeBag) diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/EnterNameViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/EnterNameViewController.swift index a511cc8d..75da09e3 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/EnterNameViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/EnterNameViewController.swift @@ -12,11 +12,11 @@ import RxCocoa import PresentationCore public protocol EnterNameInputable { - var editingName: PublishRelay { get set } + var editingName: PublishRelay { get } } public protocol EnterNameOutputable { - var nameValidation: PublishSubject<(isValid: Bool, name: String)> { get set } + var nameValidation: Driver? { get set } } public class EnterNameViewController: DisposableViewController @@ -133,6 +133,7 @@ where T.Input: EnterNameInputable, T.Output: EnterNameOutputable { textField .textField.rx.text + .compactMap { $0 } .bind(to: input.editingName) .disposed(by: disposeBag) @@ -141,11 +142,8 @@ where T.Input: EnterNameInputable, T.Output: EnterNameOutputable { let output = viewModel.output output - .nameValidation - .subscribe(onNext: { [weak self] (isValid, name) in - - printIfDebug("성함 입력: \(name), 유효성: \(isValid)") - + .nameValidation? + .drive(onNext: { [weak self] isValid in self?.ctaButton.setEnabled(isValid) }) .disposed(by: disposeBag) diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/ValidatePhoneNumberViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/ValidatePhoneNumberViewController.swift index 24a8364e..6704de6b 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/ValidatePhoneNumberViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Common/Register/ValidatePhoneNumberViewController.swift @@ -9,26 +9,35 @@ import UIKit import RxSwift import RxCocoa import DSKit +import Entity import PresentationCore public protocol AuthPhoneNumberInputable { - var editingPhoneNumber: PublishRelay { get set } - var editingAuthNumber: PublishRelay { get set } - var requestAuthForPhoneNumber: PublishRelay { get set } - var requestValidationForAuthNumber: PublishRelay { get set } + var editingPhoneNumber: BehaviorRelay { get set } + var editingAuthNumber: BehaviorRelay { get set } + var requestAuthForPhoneNumber: PublishRelay { get set } + var requestValidationForAuthNumber: PublishRelay { get set } } public protocol AuthPhoneNumberOutputable { - var canSubmitPhoneNumber: PublishRelay { get set } - var canSubmitAuthNumber: PublishRelay { get set } - var phoneNumberValidation: PublishRelay { get set } - var authNumberValidation: PublishRelay { get set } + var canSubmitPhoneNumber: Driver? { get set } + var canSubmitAuthNumber: Driver? { get set } + var phoneNumberValidation: Driver? { get set } + var authNumberValidation: Driver? { get set } + + // 요양보호사 로그인에 성공한 경우(요양보호사 한정 로직) + var loginValidation: Driver? { get set } + + var alert: Driver? { get set } +} +public extension AuthPhoneNumberOutputable { + var loginValidation: Driver? { get { nil } set { } } } -class ValidatePhoneNumberViewController: DisposableViewController -where +class ValidatePhoneNumberViewController: BaseViewController +where T.Input: AuthPhoneNumberInputable, T.Output: AuthPhoneNumberOutputable { @@ -210,21 +219,25 @@ where // 현재 입력중인 정보 전송 phoneNumberField.idleTextField.textField.rx.text + .compactMap { $0 } .bind(to: input.editingPhoneNumber) .disposed(by: disposeBag) authNumberField.idleTextField.textField.rx.text + .compactMap { $0 } .bind(to: input.editingAuthNumber) .disposed(by: disposeBag) // 인증, 확인 버튼이 눌린 경우 phoneNumberField .eventPublisher + .map { _ in () } .bind(to: input.requestAuthForPhoneNumber) .disposed(by: disposeBag) authNumberField .eventPublisher + .map { _ in () } .bind(to: input.requestValidationForAuthNumber) .disposed(by: disposeBag) @@ -233,7 +246,7 @@ where // 입력중인 전화번호가 특정 조건(ex: 입력길이)을 만족한 경우 '인증'버튼 활성화 output - .canSubmitPhoneNumber + .canSubmitPhoneNumber? .compactMap { $0 } .asDriver(onErrorJustReturn: false) .drive(onNext: { [weak self] in self?.phoneNumberField.button.setEnabled($0) }) @@ -241,7 +254,7 @@ where // 입력중인 인증번호가 특정 조건(ex: 입력길이)을 만족한 경우 '확인'버튼 활성화 output - .canSubmitAuthNumber + .canSubmitAuthNumber? .compactMap { $0 } .asDriver(onErrorJustReturn: false) .drive(onNext: { [weak self] in self?.authNumberField.button.setEnabled($0) }) @@ -249,21 +262,34 @@ where // 휴대전화 인증의 시작 output - .phoneNumberValidation - .compactMap { $0 } - .filter { $0 } - .subscribe(onNext: { [weak self] _ in + .phoneNumberValidation? + .drive(onNext: { [weak self] _ in self?.activateAuthNumberField() }) .disposed(by: disposeBag) // 인증번호 인증 성공여부 output - .authNumberValidation - .compactMap { $0 } + .authNumberValidation? .filter { $0 } - .subscribe(onNext: { [weak self] _ in - self?.onAuthSuccess() + .drive(onNext: { [weak self] _ in + self?.onAuthNumberValidationSuccess() + }) + .disposed(by: disposeBag) + + output + .loginValidation? + .drive(onNext: { [weak self] _ in + guard let self else { return } + (coordinator as! WorkerRegisterCoordinator).authFinished() + }) + .disposed(by: disposeBag) + + // Alert + output + .alert? + .drive(onNext: { [weak self] vo in + self?.showAlert(vo: vo) }) .disposed(by: disposeBag) @@ -274,10 +300,11 @@ where .subscribe { [weak self] _ in self?.coordinator?.next() } .disposed(by: disposeBag) } +} + +extension ValidatePhoneNumberViewController { + - func cleanUp() { - - } } extension ValidatePhoneNumberViewController { @@ -290,7 +317,7 @@ extension ValidatePhoneNumberViewController { authNumberField.idleTextField.startTimer(minute: 5, seconds: 0) } - func onAuthSuccess() { + func onAuthNumberValidationSuccess() { // 입력과 관려된 필드와 버튼 비활성화 phoneNumberField.idleTextField.setEnabled(false) diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EnterAddressViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EnterAddressViewController.swift index 737e255d..8003d07c 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EnterAddressViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EnterAddressViewController.swift @@ -13,13 +13,11 @@ import Entity import PresentationCore public protocol EnterAddressInputable { - var addressInformation: PublishRelay { get } -// var editingDetailAddress: PublishRelay { get } + var addressInformation: PublishRelay { get } } -public protocol EnterAddressOutputable { } -public class EnterAddressViewController: DisposableViewController -where T.Input: EnterAddressInputable & CTAButtonEnableInputable, T.Output: EnterAddressOutputable & RegisterSuccessOutputable { +public class EnterAddressViewController: BaseViewController +where T.Input: EnterAddressInputable & CTAButtonEnableInputable, T.Output: RegisterValidationOutputable { public var coordinator: WorkerRegisterCoordinator? @@ -194,14 +192,16 @@ where T.Input: EnterAddressInputable & CTAButtonEnableInputable, T.Output: Enter let output = viewModel.output output - .registerValidation - .compactMap { $0 } - .subscribe(onNext: { [weak self] isSuccess in - if isSuccess { - self?.coordinator?.next() - } else { - self?.ctaButton.setEnabled(true) - } + .registerValidation? + .drive(onNext: { [weak self] _ in + self?.coordinator?.next() + }) + .disposed(by: disposeBag) + + output + .alert? + .drive(onNext: { [weak self] vo in + self?.showAlert(vo: vo) }) .disposed(by: disposeBag) } @@ -213,10 +213,6 @@ where T.Input: EnterAddressInputable & CTAButtonEnableInputable, T.Output: Enter vc.modalPresentationStyle = .fullScreen navigationController?.pushViewController(vc, animated: true) } - - public func cleanUp() { - coordinator?.coordinatorDidFinish() - } } extension EnterAddressViewController: DaumAddressSearchDelegate { diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EntetPersonalInfoViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EntetPersonalInfoViewController.swift new file mode 100644 index 00000000..47616893 --- /dev/null +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/EntetPersonalInfoViewController.swift @@ -0,0 +1,235 @@ +// +// EntetPersonalInfoViewController.swift +// AuthFeature +// +// Created by choijunios on 7/25/24. +// + +import UIKit +import RxSwift +import RxCocoa +import DSKit +import Entity +import PresentationCore + +protocol WorkerPersonalInfoInputable: EnterNameInputable, SelectGenderInputable { + var edtingBirthYear: PublishRelay { get } +} + +protocol WorkerPersonalInfoOutputable: EnterNameOutputable, SelectGenderOutputable { + var edtingBirthYearValidation: Driver? { get set } +} + +class EntetPersonalInfoViewController: BaseViewController +where T.Input: WorkerPersonalInfoInputable & CTAButtonEnableInputable, + T.Output: WorkerPersonalInfoOutputable +{ + + var coordinator: WorkerRegisterCoordinator? + + private let viewModel: T + + // View + + private let processTitle: IdleLabel = { + let label = IdleLabel(typography: .Heading2) + + label.textString = "본인의 인적사항을 입력해주세요." + + return label + }() + + // MARK: 성함 View + private let nameField: IFType2 = { + let textField = IFType2( + titleLabelText: "이름", + placeHolderText: "성함을 입력해주세요." + ) + return textField + }() + + // MARK: 생년 View + private let birthYearField: IFType2 = { + + let textField = IFType2( + titleLabelText: "출생연도", + placeHolderText: "출생연도를 입력해주세요. (예: 1965)", + keyboardType: .numberPad + ) + + return textField + }() + + // MARK: 성별 View + private let femaleButton: StateButtonTyp1 = { + let btn = StateButtonTyp1( + text: "여성", + initial: .normal + ) + return btn + }() + + private let maleButton: StateButtonTyp1 = { + let btn = StateButtonTyp1( + text: "남성", + initial: .normal + ) + return btn + }() + + private let ctaButton: CTAButtonType1 = { + + let button = CTAButtonType1(labelText: "다음") + + return button + }() + + let disposeBag = DisposeBag() + + public init( + coordinator: WorkerRegisterCoordinator? = nil, + viewModel: T + ) { + self.coordinator = coordinator + self.viewModel = viewModel + + super.init(nibName: nil, bundle: nil) + + setAppearance() + setAutoLayout() + setObservable() + } + + required init?(coder: NSCoder) { fatalError() } + + + func setAppearance() { + self.view.backgroundColor = .white + view.layoutMargins = .init(top: 32, left: 20, bottom: 0, right: 20) + } + + private func setAutoLayout() { + let genderButtonStack = VStack( + [ + { + let label = IdleLabel(typography: .Subtitle4) + label.textString = "성별" + label.attrTextColor = DSKitAsset.Colors.gray500.color + return label + }(), + HStack([femaleButton,maleButton], spacing: 4) + ], + spacing: 6, + alignment: .leading + ) + NSLayoutConstraint.activate([ + femaleButton.heightAnchor.constraint(equalToConstant: 44), + femaleButton.widthAnchor.constraint(equalToConstant: 104), + + maleButton.widthAnchor.constraint(equalTo: femaleButton.widthAnchor), + maleButton.heightAnchor.constraint(equalTo: femaleButton.heightAnchor), + ]) + + [ + processTitle, + nameField, + birthYearField, + genderButtonStack, + ctaButton + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + NSLayoutConstraint.activate([ + + processTitle.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), + processTitle.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + processTitle.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + + nameField.topAnchor.constraint(equalTo: processTitle.bottomAnchor, constant: 32), + nameField.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + nameField.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + + birthYearField.topAnchor.constraint(equalTo: nameField.bottomAnchor, constant: 28), + birthYearField.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + birthYearField.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + + genderButtonStack.topAnchor.constraint(equalTo: birthYearField.bottomAnchor, constant: 28), + genderButtonStack.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + genderButtonStack.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + + ctaButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + ctaButton.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + ctaButton.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + ]) + } + + private func setObservable() { + + // - CTA버튼 비활성화 + ctaButton.setEnabled(false) + + let input = viewModel.input + + nameField + .eventPublisher + .compactMap { $0 } + .bind(to: input.editingName) + .disposed(by: disposeBag) + + birthYearField + .eventPublisher + .compactMap { $0 } + .compactMap { Int($0) } + .bind(to: input.edtingBirthYear) + .disposed(by: disposeBag) + + let femaleClicked = femaleButton.eventPublisher + .filter({ $0 == .accent }) + .map { [weak self] _ in + self?.maleButton.setState(.normal, withAnimation: false) + return Gender.female + } + + let maleClicked = maleButton.eventPublisher + .filter({ $0 == .accent }) + .map { [weak self] _ in + self?.femaleButton.setState(.normal, withAnimation: false) + return Gender.male + } + + Observable + .merge(femaleClicked, maleClicked) + .bind(to: viewModel.input.selectingGender) + .disposed(by: disposeBag) + + // MARK: Output + let output = viewModel.output + + let nameValidation = output.nameValidation?.asObservable() ?? .empty() + let birthYearValidation = output.edtingBirthYearValidation?.asObservable() ?? .empty() + let genderIsSelected = output.genderIsSelected?.asObservable() ?? .empty() + + Observable + .combineLatest( + nameValidation, + birthYearValidation, + genderIsSelected + ) + .map { $0 && $1 && $2 } + .asDriver(onErrorJustReturn: false) + .drive(onNext: { [weak self] isAllValid in + self?.ctaButton.setEnabled(isAllValid) + }) + .disposed(by: disposeBag) + + // MARK: ViewController한정 로직 + // CTA버튼 클릭시 화면전환 + ctaButton + .eventPublisher + .subscribe { [weak self] _ in self?.coordinator?.next() } + .disposed(by: disposeBag) + + } +} diff --git a/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/SelectGenderViewController.swift b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/SelectGenderViewController.swift index 9b66672a..23cd8c14 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/SelectGenderViewController.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/View/Worker/Register/SelectGenderViewController.swift @@ -17,11 +17,13 @@ protocol SelectGenderInputable { } protocol SelectGenderOutputable { - var genderIsSelected: PublishRelay { get } + var genderIsSelected: Driver? { get set } } -class SelectGenderViewController: DisposableViewController -where T.Input: SelectGenderInputable & CTAButtonEnableInputable, T.Output: SelectGenderOutputable { +class SelectGenderViewController: BaseViewController +where T.Input: SelectGenderInputable & CTAButtonEnableInputable, + T.Output: SelectGenderOutputable +{ var coordinator: WorkerRegisterCoordinator? @@ -152,14 +154,16 @@ where T.Input: SelectGenderInputable & CTAButtonEnableInputable, T.Output: Selec .bind(to: viewModel.input.selectingGender) .disposed(by: disposeBag) - viewModel - .output - .genderIsSelected - .subscribe(onNext: { [weak self] _ in + let output = viewModel.output + + output + .genderIsSelected? + .drive(onNext: { [weak self] _ in self?.ctaButton.setEnabled(true) }) .disposed(by: disposeBag) + // MARK: ViewController한정 로직 // CTA버튼 클릭시 화면전환 ctaButton @@ -167,8 +171,4 @@ where T.Input: SelectGenderInputable & CTAButtonEnableInputable, T.Output: Selec .subscribe { [weak self] _ in self?.coordinator?.next() } .disposed(by: disposeBag) } - - func cleanUp() { - coordinator?.coordinatorDidFinish() - } } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+IdPassword.swift b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+IdPassword.swift index 0a7b24ae..b582d20c 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+IdPassword.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+IdPassword.swift @@ -22,20 +22,19 @@ extension AuthInOutStreamManager { stateTracker: @escaping (String) -> () ) { + var output = output + // MARK: Id - _ = input + output.canCheckIdDuplication = input .editingId - .compactMap { $0 } - .map({ [unowned useCase, output] id in - output.canCheckIdDuplication.accept( - useCase.checkIdIsValid(id: id) - ) - }) + .map { [unowned useCase] id in + useCase.checkIdIsValid(id: id) + } + .asDriver(onErrorJustReturn: false) // 중복성 검사 let idDuplicationValidation = input .requestIdDuplicationValidation - .compactMap { $0 } .flatMap { [unowned useCase] id in printIfDebug("[CenterRegisterViewModel] 중복성 검사 대상 id: \(id)") @@ -50,33 +49,32 @@ extension AuthInOutStreamManager { return useCase.requestCheckingIdDuplication(id: id) } - .share() - _ = idDuplicationValidation - .compactMap { $0.value } - .map { [weak output] validId in - printIfDebug("[CenterRegisterViewModel] \(validId) 중복체크 결과: ✅ 성공") - // 🚀 상태추적 🚀 - stateTracker(validId) - output?.idDuplicationValidation.accept(validId) + output.idDuplicationValidation = idDuplicationValidation + .map { [stateTracker] result in + switch result { + case .success(let id): + printIfDebug("[CenterRegisterViewModel] 중복체크 결과: ✅ 성공") + // 🚀 상태추적 🚀 + stateTracker(id) + return true + case .failure(let error): + printIfDebug("❌ 아이디중복검사 실패 \n 에러내용: \(error.message)") + return false + } } - - _ = idDuplicationValidation - .compactMap { $0.error } - .map({ [weak output] error in - printIfDebug("❌ 아이디중복검사 실패 \n 에러내용: \(error.message)") - output?.idDuplicationValidation.accept(nil) - }) + .asDriver(onErrorJustReturn: false) } static func passwordInOut( input: SetPasswordInputable & AnyObject, output: SetPasswordOutputable & AnyObject, useCase: AuthInputValidationUseCase, - stateTracker: @escaping (String) -> ()) { - - _ = input.editingPassword - .compactMap { $0 } + stateTracker: @escaping (String) -> ()) + { + var output = output + output.passwordValidation = input.editingPasswords + .filter { (pwd, cpwd) in !pwd.isEmpty && !cpwd.isEmpty } .map { [unowned useCase] (pwd, cpwd) in printIfDebug("[CenterRegisterViewModel] \n 입력중인 비밀번호: \(pwd) \n 확인 비밀번호: \(cpwd)") @@ -94,8 +92,6 @@ extension AuthInOutStreamManager { return PasswordValidationState.match } } - .map { [weak output] result in - output?.passwordValidation.accept(result) - } + .asDriver(onErrorJustReturn: .invalidPassword) } } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+Name.swift b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+Name.swift index fef8b2f7..5db247e6 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+Name.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+Name.swift @@ -23,21 +23,15 @@ extension AuthInOutStreamManager { stateTracker: @escaping (String) -> () ) { // MARK: 성함입력 - _ = input + var output = output + output.nameValidation = input .editingName - .compactMap({ $0 }) - .map { [weak useCase] name in - - guard let useCase else { return (false, name) } - + .map { [useCase] name in + printIfDebug("[\(#function)] 입력중인 이름: \(name)") let isValid = useCase.checkNameIsValid(name: name) - - if isValid { - stateTracker(name) - } - - return (isValid, name) + if isValid { stateTracker(name) } + return isValid } - .bind(to: output.nameValidation) + .asDriver(onErrorJustReturn: false) } } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+PhoneNumber.swift b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+PhoneNumber.swift index bef4b5ee..5b58f8f0 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+PhoneNumber.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+PhoneNumber.swift @@ -18,96 +18,178 @@ extension AuthInOutStreamManager { input: AuthPhoneNumberInputable & AnyObject, output: AuthPhoneNumberOutputable & AnyObject, useCase: AuthInputValidationUseCase, + authUseCase: AuthUseCase? = nil, stateTracker: @escaping (String) -> () ) { + + var output = output + // MARK: 전화번호 입력 - _ = input + output.canSubmitPhoneNumber = input .editingPhoneNumber - .compactMap({ $0 }) .map({ [unowned useCase] phoneNumber in printIfDebug("[CenterRegisterViewModel] 전달받은 전화번호: \(phoneNumber)") return useCase.checkPhoneNumberIsValid(phoneNumber: phoneNumber) }) - .map({ [weak output] isValid in - output?.canSubmitPhoneNumber.accept(isValid) - }) + .asDriver(onErrorJustReturn: false) - _ = input + output.canSubmitAuthNumber = input .editingAuthNumber .compactMap({ $0 }) - .map({ [weak output] authNumber in + .map { authNumber in printIfDebug("[CenterRegisterViewModel] 전달받은 인증번호: \(authNumber)") - output?.canSubmitAuthNumber.accept(authNumber.count == 6) - }) + + return authNumber.count == 6 + } + .asDriver(onErrorJustReturn: false) let phoneNumberAuthRequestResult = input .requestAuthForPhoneNumber - .compactMap({ $0 }) - .flatMap({ [unowned useCase] number in - - let formatted = Self.formatPhoneNumber(phoneNumber: number) + .flatMap { [unowned useCase, input] _ in + + let formatted = Self.formatPhoneNumber(phoneNumber: input.editingPhoneNumber.value) #if DEBUG print("✅ 디버그모드에서 번호인증 요청 무조건 통과") return Single.just(Result.success(formatted)) #endif - return useCase.requestPhoneNumberAuthentication(phoneNumber: formatted) - }) - .share() - - var authingNumber: String = "" + } - _ = phoneNumberAuthRequestResult + output + .phoneNumberValidation = phoneNumberAuthRequestResult .compactMap { $0.value } - .map { [weak output] phoneNumber in - printIfDebug("✅ 번호로 인증을 시작합니다.") - authingNumber = phoneNumber - output?.phoneNumberValidation.accept(true) + .map { phoneNumber in + printIfDebug("✅ \(phoneNumber) 번호로 인증을 시작합니다.") + return true } - - _ = phoneNumberAuthRequestResult + .asDriver(onErrorJustReturn: false) + + let phoneNumeberAuthRequestFailure = phoneNumberAuthRequestResult .compactMap { $0.error } - .map { [weak output] error in + .map { error in printIfDebug("❌ 인증을 시작할 수 없습니다. \n 에러내용: \(error.message)") - output?.phoneNumberValidation.accept(false) + return error.message } + // AuthUseCase를 전달받은 경우 = "요양보호사 로그인을 포함한다" + var phoneNumeberAuthFailure: Observable! - let phoneNumberAuthResult = input.requestValidationForAuthNumber - .compactMap({ authNumber in - if let authNumber { - return (authingNumber, authNumber) + if let authUseCase { + + let loginResult = input.requestValidationForAuthNumber + .flatMap { [authUseCase, unowned input] _ in + + let phoneNumber = formatPhoneNumber(phoneNumber: input.editingPhoneNumber.value) + let authNumber = input.editingAuthNumber.value + + return authUseCase.loginWorkerAccount( + phoneNumber: phoneNumber, + authNumber: authNumber + ) } - return nil - }) - .flatMap { [unowned useCase] (phoneNumber: String, authNumber: String) in - -#if DEBUG - // 디버그시 인증번호 무조건 통과 - print("✅ 디버그모드에서 번호인증 무조건 통과") - return Single.just(Result.success(phoneNumber)) -#endif - - return useCase.authenticateAuthNumber(phoneNumber: phoneNumber, authNumber: authNumber) - } - .share() + .share() + + // 로그인 성공 + output.loginValidation = loginResult + .compactMap { $0.value } + .map { phoneNumber in + printIfDebug("✅ 요양보호사 로그인 성공") + return () + } + .asDriver(onErrorJustReturn: ()) + + // 로그인 실패시 번호인증 진행(신규가입자라고 판단) + let phoneNumberAuthResult = loginResult + .compactMap { $0.error } + .flatMap { [useCase, unowned input] error in + printIfDebug("❌ 로그인 실패: \(error.message)") + + // 번호인증 시도 + let phoneNumber = formatPhoneNumber(phoneNumber: input.editingPhoneNumber.value) + let authNumber = input.editingAuthNumber.value + + #if DEBUG + // 디버그시 인증번호 무조건 통과 + print("✅ 디버그모드에서 번호인증 무조건 통과") + return Single.just(Result.success(phoneNumber)) + #endif + + return useCase.authenticateAuthNumber(phoneNumber: phoneNumber, authNumber: authNumber) + } + .share() + + // 번호인증 성공 + output.authNumberValidation = phoneNumberAuthResult + .compactMap { $0.value } + .map { phoneNumber in + printIfDebug("✅ \(phoneNumber) 인증성공") + return true + } + .asDriver(onErrorJustReturn: false) + + // 번호인증 실패 + phoneNumeberAuthFailure = phoneNumberAuthResult + .compactMap { $0.error?.message } + .asObservable() + } else { + + let phoneNumberAuthResult = input.requestValidationForAuthNumber + .flatMap { [useCase, unowned input] _ in + + let phoneNumber = formatPhoneNumber(phoneNumber: input.editingPhoneNumber.value) + let authNumber = input.editingAuthNumber.value + #if DEBUG + // 디버그시 인증번호 무조건 통과 + print("✅ 디버그모드에서 번호인증 무조건 통과") + return Single.just(Result.success(phoneNumber)) + #endif + + return useCase.authenticateAuthNumber(phoneNumber: phoneNumber, authNumber: authNumber) + } + .share() + + // 번호인증 성공 + output.authNumberValidation = phoneNumberAuthResult + .compactMap { $0.value } + .map { phoneNumber in + printIfDebug("✅ \(phoneNumber) 인증성공") + stateTracker(phoneNumber) + return true + } + .asDriver(onErrorJustReturn: false) + + // 번호인증 실패 + phoneNumeberAuthFailure = phoneNumberAuthResult + .compactMap { $0.error?.message ?? nil } + .asObservable() + } - // 번호인증 성공 - _ = phoneNumberAuthResult - .compactMap { $0.value } - .map { [weak output] phoneNumber in - printIfDebug("✅ \(phoneNumber) 인증성공") - stateTracker(phoneNumber) - output?.authNumberValidation.accept(true) - } - - // 번호인증 실패 - _ = phoneNumberAuthResult - .compactMap { $0.error } - .map { [weak output] error in - printIfDebug("❌ 번호 인증실패 \n 에러내용: \(error.message)") - output?.authNumberValidation.accept(false) + // 번호 인증 관련 Alert + let phoneAuthValidation = Observable + .merge( + phoneNumeberAuthRequestFailure, + phoneNumeberAuthFailure + ) + .map { errorMessage in + DefaultAlertContentVO( + title: "번호 인증 실패", + message: errorMessage + ) } + + // 이미 alert드라이버가 존재할 경우 merge + var newAlertDrvier: Observable! + if let alertDrvier = output.alert { + newAlertDrvier = Observable + .merge( + alertDrvier.asObservable(), + phoneAuthValidation + ) + } else { + newAlertDrvier = phoneAuthValidation + } + output + .alert = newAlertDrvier.asDriver(onErrorJustReturn: .default) } } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterLoginViewModel.swift b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterLoginViewModel.swift index 952d74fb..26cc6c30 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterLoginViewModel.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterLoginViewModel.swift @@ -33,42 +33,52 @@ public class CenterLoginViewModel: ViewModelType { func setObservable() { + output.canRequestLoginAction = Observable + .combineLatest( + input.editingId, + input.editingPassword + ) + .map { (id, password) in + return !id.isEmpty && !password.isEmpty + } + .asDriver(onErrorJustReturn: false) + let loginResult = input .loginButtonPressed - .compactMap { $0 } - .flatMap { [unowned self] (id, pw) in - self.authUseCase - .loginCenterAccount(id: id, password: pw) + .flatMap { [unowned self, input] _ in + let id = input.editingId.value + let password = input.editingPassword.value + return self.authUseCase + .loginCenterAccount(id: id, password: password) } .share() - _ = loginResult - .compactMap { $0.value } - .map { [weak self] _ in - printIfDebug("✅ 로그인 성공") + output.loginValidation = loginResult + .map { result in - self?.output.loginValidation.accept(true) - } - - _ = loginResult - .compactMap { $0.error } - .map { [weak self] error in - printIfDebug("❌ 로그인 실패, 에러내용: \(error.message)") - - self?.output.loginValidation.accept(false) + switch result { + case .success: + printIfDebug("✅ 로그인 성공") + return true + case .failure(let error): + printIfDebug("❌ 로그인 실패: \(error.message)") + return false + } } + .asDriver(onErrorJustReturn: false) } } public extension CenterLoginViewModel { class Input { - - public var loginButtonPressed: PublishRelay<(id: String, pw: String)?> = .init() + public let editingId: BehaviorRelay = .init(value: "") + public let editingPassword: BehaviorRelay = .init(value: "") + public let loginButtonPressed: PublishRelay = .init() } class Output { - - public var loginValidation: PublishRelay = .init() + public var canRequestLoginAction: Driver? + public var loginValidation: Driver? } } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterSetNewPasswordViewModel.swift b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterSetNewPasswordViewModel.swift index 0e2ffad0..d4f65641 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterSetNewPasswordViewModel.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Login/CenterSetNewPasswordViewModel.swift @@ -20,7 +20,8 @@ public class CenterSetNewPasswordViewModel: ViewModelType { public var input: Input = .init() public var output: Output = .init() - var validPassword: String? + // State + private var validPassword: String? public init( authUseCase: AuthUseCase, @@ -52,30 +53,34 @@ public class CenterSetNewPasswordViewModel: ViewModelType { output: output, useCase: inputValidationUseCase) { _ in } + changePasswordInOut() + } + + private func changePasswordInOut() { + let changePasswordResult = input.changePasswordButtonClicked - .compactMap { $0 } .flatMap { [weak self] _ in printIfDebug("변경 요청 비밀번호 \(self?.validPassword ?? "")") + // TODO: 비밀번호 변경 API 연동 // 이벤트 전송 return Single.just(Result.success(())) } .share() - _ = changePasswordResult - .compactMap { $0.value } - .map({ [weak self] _ in - printIfDebug("비밀번호 변경 성공") - self?.output.changePasswordSuccessValidation.accept(true) - }) - - _ = changePasswordResult - .compactMap { $0.error } - .map({ [weak self] _ in - printIfDebug("비밀번호 변경 실패") - self?.output.changePasswordSuccessValidation.accept(false) - }) + output.changePasswordValidation = changePasswordResult + .map { result in + switch result { + case .success: + printIfDebug("비밀번호 변경 성공") + return true + case .failure(let error): + printIfDebug("비밀번호 변경 실패") + return false + } + } + .asDriver(onErrorJustReturn: false) } } @@ -84,31 +89,33 @@ public extension CenterSetNewPasswordViewModel { class Input { // 전화번호 입력 - public var editingPhoneNumber: PublishRelay = .init() - public var editingAuthNumber: PublishRelay = .init() - public var requestAuthForPhoneNumber: PublishRelay = .init() - public var requestValidationForAuthNumber: PublishRelay = .init() + public var editingPhoneNumber: BehaviorRelay = .init(value: "") + public var editingAuthNumber: BehaviorRelay = .init(value: "") + public var requestAuthForPhoneNumber: PublishRelay = .init() + public var requestValidationForAuthNumber: PublishRelay = .init() // Password - public var editingPassword: PublishRelay<(pwd: String, cpwd: String)?> = .init() + public var editingPasswords: PublishRelay<(pwd: String, cpwd: String)> = .init() // Change password - public var changePasswordButtonClicked: PublishRelay = .init() + public var changePasswordButtonClicked: PublishRelay = .init() } class Output { // 전화번호 입력 - public var canSubmitPhoneNumber: PublishRelay = .init() - public var canSubmitAuthNumber: PublishRelay = .init() - public var phoneNumberValidation: PublishRelay = .init() - public var authNumberValidation: PublishRelay = .init() + public var canSubmitPhoneNumber: Driver? + public var canSubmitAuthNumber: Driver? + public var phoneNumberValidation: Driver? + public var authNumberValidation: Driver? // Password - public var passwordValidation: PublishRelay = .init() + public var passwordValidation: Driver? // Change password - public var changePasswordSuccessValidation: PublishRelay = .init() + public var changePasswordValidation: Driver? + + public var alert: Driver? } } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Register/CenterRegisterViewModel.swift b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Register/CenterRegisterViewModel.swift index 9bc0923a..8f51ea50 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Register/CenterRegisterViewModel.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/Register/CenterRegisterViewModel.swift @@ -50,7 +50,6 @@ public class CenterRegisterViewModel: ViewModelType { registerInOut() validateBusinessNumberInOut() - AuthInOutStreamManager.idInOut( input: input, output: output, @@ -79,53 +78,57 @@ extension CenterRegisterViewModel { public class Input { // CTA 버튼 클릭시 - public var ctaButtonClicked: PublishRelay = .init() + public var ctaButtonClicked: PublishRelay = .init() // 이름입력 - public var editingName: PublishRelay = .init() + public var editingName: PublishRelay = .init() // 전화번호 입력 - public var editingPhoneNumber: PublishRelay = .init() - public var editingAuthNumber: PublishRelay = .init() - public var requestAuthForPhoneNumber: PublishRelay = .init() - public var requestValidationForAuthNumber: PublishRelay = .init() + public var editingPhoneNumber: BehaviorRelay = .init(value: "") + public var editingAuthNumber: BehaviorRelay = .init(value: "") + public var requestAuthForPhoneNumber: PublishRelay = .init() + public var requestValidationForAuthNumber: PublishRelay = .init() // 사업자 번호 입력 - public var editingBusinessNumber: PublishRelay = .init() - public var requestBusinessNumberValidation: PublishRelay = .init() + public var editingBusinessNumber: BehaviorRelay = .init(value: "") + public var requestBusinessNumberValidation: PublishRelay = .init() // Id - public var editingId: PublishRelay = .init() - public var requestIdDuplicationValidation: PublishRelay = .init() + public var editingId: BehaviorRelay = .init(value: "") + public var requestIdDuplicationValidation: PublishRelay = .init() // Password - public var editingPassword: PublishRelay<(pwd: String, cpwd: String)?> = .init() + public var editingPasswords: PublishRelay<(pwd: String, cpwd: String)> = .init() } public class Output { // 이름 입력 - public var nameValidation: PublishSubject<(isValid: Bool, name: String)> = .init() + public var nameValidation: Driver? // 전화번호 입력 - public var canSubmitPhoneNumber: PublishRelay = .init() - public var canSubmitAuthNumber: PublishRelay = .init() - public var phoneNumberValidation: PublishRelay = .init() - public var authNumberValidation: PublishRelay = .init() + public var canSubmitPhoneNumber: Driver? + public var canSubmitAuthNumber: Driver? + public var phoneNumberValidation: Driver? + public var authNumberValidation: Driver? // 사업자 번호 입력 - public var canSubmitBusinessNumber: PublishRelay = .init() - public var businessNumberValidation: PublishRelay = .init() + public var canSubmitBusinessNumber: Driver? + public var businessNumberVO: Driver? + public var businessNumberValidationFailrue: Driver? // Id - public var canCheckIdDuplication: PublishRelay = .init() - public var idDuplicationValidation: PublishRelay = .init() + public var canCheckIdDuplication: Driver? + public var idDuplicationValidation: Driver? // Password - public var passwordValidation: PublishRelay = .init() + public var passwordValidation: Driver? // Register success - public var registerValidation: PublishRelay = .init() + public var registerValidation: Driver? + + // Alert + public var alert: Driver? } } @@ -135,27 +138,53 @@ extension CenterRegisterViewModel { // MARK: 최종 회원가입 버튼 let registerValidation = input .ctaButtonClicked - .compactMap({ $0 }) .flatMap { [unowned self] _ in self.authUseCase .registerCenterAccount(registerState: self.stateObject) } .share() - _ = registerValidation + let loginResult = registerValidation .compactMap { $0.value } .map { [unowned self] _ in - printIfDebug("[CenterRegisterViewModel] ✅ 회원가입 성공 \n 가임정보 \(self.stateObject.description)") - self.stateObject.clear() - self.output.registerValidation.accept(true) + printIfDebug("[\(#function)] ✅ 회원가입 성공 \n 가임정보 \(stateObject.description)") + return (id: stateObject.id, password: stateObject.password) + } + .flatMap { [authUseCase] (id, pw) in + printIfDebug("[\(#function)] 로그인 실행") + return authUseCase + .loginCenterAccount(id: id, password: pw) + } + .map { [weak self] _ in + // 로그인 결과무시 + self?.stateObject.clear() + return () } + output.registerValidation = loginResult.asDriver(onErrorJustReturn: ()) - _ = registerValidation + let registrationFailure = registerValidation .compactMap { $0.error } - .map({ error in + .map { error in printIfDebug("❌ 회원가입 실패: \(error.message)") - self.output.registerValidation.accept(false) - }) + return DefaultAlertContentVO( + title: "회원가입 실패", + message: error.message + ) + } + + // 이미 alert드라이버가 존재할 경우 merge + var newAlertDrvier: Observable! + if let alertDrvier = output.alert { + newAlertDrvier = Observable + .merge( + alertDrvier.asObservable(), + registrationFailure + ) + } else { + newAlertDrvier = registrationFailure + } + output + .alert = newAlertDrvier.asDriver(onErrorJustReturn: .default) } } @@ -163,41 +192,42 @@ extension CenterRegisterViewModel { func validateBusinessNumberInOut() { // MARK: 사업자 번호 입력 - _ = input + output.canSubmitBusinessNumber = input .editingBusinessNumber - .compactMap { $0 } - .map({ [unowned self] businessNumber in + .map { [unowned self] businessNumber in self.inputValidationUseCase.checkBusinessNumberIsValid(businessNumber: businessNumber) - }) - .bind(to: output.canSubmitBusinessNumber) + } + .asDriver(onErrorJustReturn: false) let businessNumberValidationResult = input .requestBusinessNumberValidation .compactMap { $0 } - .flatMap({ [unowned self] businessNumber in + .flatMap { [unowned input] _ in + let businessNumber = input.editingBusinessNumber.value let formatted = AuthInOutStreamManager.formatBusinessNumber(businessNumber: businessNumber) printIfDebug("[CenterRegisterViewModel] 사업자 번호 인증 요청: \(formatted)") return self.inputValidationUseCase .requestBusinessNumberAuthentication(businessNumber: formatted) - }) + } .share() - _ = businessNumberValidationResult + output.businessNumberVO = businessNumberValidationResult .compactMap { $0.value } - .map({ [weak self] (businessNumber, infoVO) in + .map { [stateObject] (businessNumber, infoVO) in printIfDebug("✅ 사업자번호 검색 성공") // 🚀 상태추적 🚀 - self?.stateObject.businessNumber = businessNumber - self?.output.businessNumberValidation.accept(infoVO) - }) - + stateObject.businessNumber = businessNumber + return infoVO + } + .asDriver(onErrorJustReturn: .onError) - _ = businessNumberValidationResult + output.businessNumberValidationFailrue = businessNumberValidationResult .compactMap { $0.error } - .map({ [weak self] error in + .map { error in printIfDebug("❌ 사업자번호 검색실패 \n 에러내용: \(error.message)") - self?.output.businessNumberValidation.accept(nil) - }) + return () + } + .asDriver(onErrorJustReturn: ()) } } @@ -222,9 +252,9 @@ extension CenterRegisterViewModel.Output: AuthBusinessOwnerOutputable { } // Id & Password extension CenterRegisterViewModel.Input: SetIdInputable { } -extension CenterRegisterViewModel.Output: SetIdOutputable { } extension CenterRegisterViewModel.Input: SetPasswordInputable { } +extension CenterRegisterViewModel.Output: SetIdOutputable { } extension CenterRegisterViewModel.Output: SetPasswordOutputable { } // Register -extension CenterRegisterViewModel.Output: RegisterSuccessOutputable { } +extension CenterRegisterViewModel.Output: RegisterValidationOutputable { } diff --git a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Worker/Register/WorkerRegisterViewModel.swift b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Worker/Register/WorkerRegisterViewModel.swift index 803a337f..441aab15 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Worker/Register/WorkerRegisterViewModel.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Worker/Register/WorkerRegisterViewModel.swift @@ -16,18 +16,21 @@ public class WorkerRegisterViewModel: ViewModelType { // UseCase public let inputValidationUseCase: AuthInputValidationUseCase + public let authUseCase: AuthUseCase public var input: Input = .init() public var output: Output = .init() private let stateObject = WorkerRegisterState() - public func transform(input: Input) -> Output { - return output - } + private let disposeBag = DisposeBag() - public init(inputValidationUseCase: AuthInputValidationUseCase) { + public init( + inputValidationUseCase: AuthInputValidationUseCase, + authUseCase: AuthUseCase + ) { self.inputValidationUseCase = inputValidationUseCase + self.authUseCase = authUseCase setInput() } @@ -35,162 +38,111 @@ public class WorkerRegisterViewModel: ViewModelType { private func setInput() { // MARK: 이름 입력 - _ = input - .editingName - .compactMap({ $0 }) - .map { [weak self] name in - - guard let self else { return (false, name) } - - let isValid = self.inputValidationUseCase.checkNameIsValid(name: name) - - if isValid { - stateObject.name = name - } - - return (isValid, name) + AuthInOutStreamManager.enterNameInOut( + input: input, + output: output, + useCase: inputValidationUseCase) { [weak self] validName in + // 🚀 상태추적 🚀 + self?.stateObject.name = validName } - .bind(to: output.nameValidation) // MARK: 성별 선택 - _ = input - .selectingGender + output.genderIsSelected = input.selectingGender .filter({ $0 != .notDetermined }) - .map({ [weak self] gender in + .map { [weak self] gender in printIfDebug("선택된 성별: \(gender)") self?.stateObject.gender = gender - return () - }) - .bind(to: output.genderIsSelected) - - // MARK: 전화번호 입력 - _ = input - .editingPhoneNumber - .compactMap({ $0 }) - .map({ [unowned self] phoneNumber in - printIfDebug("[CenterRegisterViewModel] 전달받은 전화번호: \(phoneNumber)") - return self.inputValidationUseCase.checkPhoneNumberIsValid(phoneNumber: phoneNumber) - }) - .bind(to: output.canSubmitPhoneNumber) - - _ = input - .editingAuthNumber - .compactMap({ $0 }) - .map({ authNumber in - printIfDebug("[CenterRegisterViewModel] 전달받은 인증번호: \(authNumber)") - return authNumber.count >= 6 - }) - .bind(to: output.canSubmitAuthNumber) - - let phoneNumberAuthRequestResult = input - .requestAuthForPhoneNumber - .compactMap({ $0 }) - .flatMap({ [unowned self] number in - - let formatted = self.formatPhoneNumber(phoneNumber: number) - - // 상태추적 - self.stateObject.phoneNumber = formatted - - #if DEBUG - print("✅ 디버그모드에서 번호인증 요청 무조건 통과") - return Single.just(Result.success(formatted)) - #endif - - return self.inputValidationUseCase.requestPhoneNumberAuthentication(phoneNumber: formatted) - }) - .share() - - _ = phoneNumberAuthRequestResult - .compactMap { $0.value } - .map { [weak self] phoneNumber in - printIfDebug("✅ 번호로 인증을 시작합니다.") - self?.stateObject.phoneNumber = phoneNumber - self?.output.phoneNumberValidation.accept(true) - } - - _ = phoneNumberAuthRequestResult - .compactMap { $0.error } - .map { [weak self] error in - printIfDebug("❌ 인증을 시작할 수 없습니다. \n 에러내용: \(error.message)") - self?.output.phoneNumberValidation.accept(false) + return true } + .asDriver(onErrorJustReturn: false) + - let phoneNumberAuthResult = input.requestValidationForAuthNumber - .compactMap({ [weak self] authNumber in - if let phoneNumber = self?.stateObject.phoneNumber, let authNumber { - return (phoneNumber, authNumber) + // MARK: 생년월일 입력 + output.edtingBirthYearValidation = input + .edtingBirthYear + .map { [unowned self] in + printIfDebug("입력중인 생년월일: \($0)") + let isValid = self.validateBirthYear($0) + if isValid { + self.stateObject.birthYear = $0 } - return nil - }) - .flatMap { [unowned self] (phoneNumber: String, authNumber: String) in - - #if DEBUG - // 디버그시 인증번호 무조건 통과 - print("✅ 디버그모드에서 번호인증 무조건 통과") - return Single.just(Result.success(phoneNumber)) - #endif - - return self.inputValidationUseCase - .authenticateAuthNumber(phoneNumber: phoneNumber, authNumber: authNumber) - } - .share() - - // 번호인증 성공 - _ = phoneNumberAuthResult - .compactMap { $0.value } - .map { [weak self] _ in - printIfDebug("✅ 인증성공") - self?.output.authNumberValidation.accept(true) + return isValid } - - // 번호인증 실패 - _ = phoneNumberAuthResult - .compactMap { $0.error } - .map { [weak self] error in - printIfDebug("❌ 번호 인증실패 \n 에러내용: \(error.message)") - self?.output.authNumberValidation.accept(false) + .asDriver(onErrorJustReturn: false) + + // MARK: 전화번호 입력 + AuthInOutStreamManager.validatePhoneNumberInOut( + input: input, + output: output, + useCase: inputValidationUseCase, + authUseCase: authUseCase + ) { [weak self] authedPhoneNumber in + // 🚀 상태추적 🚀 + self?.stateObject.phoneNumber = authedPhoneNumber } // MARK: 주소 입력 - _ = input + // 예외적으로 ViewModel에서 구독처리 + input .addressInformation - .compactMap { $0 } - .map { [unowned self] addressInfo in - self.stateObject.addressInformation = addressInfo + .subscribe { [unowned self] info in + self.stateObject.addressInformation = info + + // TODO: 위동 경도 API 적용 + self.stateObject.latitude = "37.5036833" + self.stateObject.logitude = "127.0448556" } + .disposed(by: disposeBag) - // MARK: 회원가입 성공 여부 - + registerInOut() + } + + private func validateBirthYear(_ year: Int) -> Bool { + let currentYear: Int = Calendar.current.component(.year, from: Date()) + return (1900...success(()) - #endif - - //TODO: UseCase사용 - return Result.success(()) + .flatMap { [authUseCase, stateObject] _ in + authUseCase.registerWorkerAccount(registerState: stateObject) } .share() - _ = registerValidation + output.registerValidation = registerValidation .compactMap { $0.value } - .map { [weak self] in + .map { print("✅ 회원가입 성공") - self?.output.registerValidation.accept(true) + return () } + .asDriver(onErrorJustReturn: ()) - _ = registerValidation + let registerFailure = registerValidation .compactMap { $0.error } - .map { [weak self] error in + .map { error in print("❌ 회원가입 실패 \n 에러내용: \(error.message)") - self?.output.registerValidation.accept(false) + return DefaultAlertContentVO( + title: "회원가입 실패", + message: error.message + ) } + + // 이미 alert드라이버가 존재할 경우 merge + var newAlertDrvier: Observable! + if let alertDrvier = output.alert { + newAlertDrvier = Observable + .merge( + alertDrvier.asObservable(), + registerFailure + ) + } else { + newAlertDrvier = registerFailure + } + output + .alert = newAlertDrvier.asDriver(onErrorJustReturn: .default) } } @@ -219,53 +171,55 @@ extension WorkerRegisterViewModel { public class Input { // CTA 버튼 클릭시 - public var ctaButtonClicked: PublishRelay = .init() - - // 이름입력 - public var editingName: PublishRelay = .init() + public var ctaButtonClicked: PublishRelay = .init() - // 성별 선택 + // 이름입력, 생년월일 입력, 성별 선택 + public var editingName: PublishRelay = .init() + public var edtingBirthYear: PublishRelay = .init() public var selectingGender: BehaviorRelay = .init(value: .notDetermined) // 전화번호 입력 - public var editingPhoneNumber: PublishRelay = .init() - public var editingAuthNumber: PublishRelay = .init() - public var requestAuthForPhoneNumber: PublishRelay = .init() - public var requestValidationForAuthNumber: PublishRelay = .init() + public var editingPhoneNumber: BehaviorRelay = .init(value: "") + public var editingAuthNumber: BehaviorRelay = .init(value: "") + public var requestAuthForPhoneNumber: PublishRelay = .init() + public var requestValidationForAuthNumber: PublishRelay = .init() // 주소 입력 - public var addressInformation: PublishRelay = .init() + public var addressInformation: PublishRelay = .init() // public var editingDetailAddress: PublishRelay = .init() } public class Output { // 이름 입력 - public var nameValidation: PublishSubject<(isValid: Bool, name: String)> = .init() + public var nameValidation: Driver? + public var edtingBirthYearValidation: Driver? - // 성별 선택완료 - public var genderIsSelected: PublishRelay = .init() + // 성별 + public var genderIsSelected: Driver? // 전화번호 입력 - public var canSubmitPhoneNumber: PublishRelay = .init() - public var canSubmitAuthNumber: PublishRelay = .init() - public var phoneNumberValidation: PublishRelay = .init() - public var authNumberValidation: PublishRelay = .init() + public var canSubmitPhoneNumber: Driver? + public var canSubmitAuthNumber: Driver? + public var phoneNumberValidation: Driver? + public var authNumberValidation: Driver? // 회원가입 성공 여부 - public var registerValidation: PublishRelay = .init() + public var registerValidation: Driver? + + // 요양보호사 로그인 성공 여부 + public var loginValidation: Driver? + + // Alert + public var alert: Driver? } } // CTAButton extension WorkerRegisterViewModel.Input: CTAButtonEnableInputable { } -// Enter name -extension WorkerRegisterViewModel.Input: EnterNameInputable { } -extension WorkerRegisterViewModel.Output: EnterNameOutputable { } - -// Gender selection -extension WorkerRegisterViewModel.Input: SelectGenderInputable { } -extension WorkerRegisterViewModel.Output: SelectGenderOutputable { } +// Enter personal info +extension WorkerRegisterViewModel.Input: WorkerPersonalInfoInputable { } +extension WorkerRegisterViewModel.Output: WorkerPersonalInfoOutputable { } // Auth phoneNumber extension WorkerRegisterViewModel.Input: AuthPhoneNumberInputable { } @@ -273,6 +227,5 @@ extension WorkerRegisterViewModel.Output: AuthPhoneNumberOutputable { } // Postal code extension WorkerRegisterViewModel.Input: EnterAddressInputable { } -extension WorkerRegisterViewModel.Output: EnterAddressOutputable { } -extension WorkerRegisterViewModel.Output: RegisterSuccessOutputable { } +extension WorkerRegisterViewModel.Output: RegisterValidationOutputable { } diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift b/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift index f9e9d0f5..e2d6fa48 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/Profile/CenterProfileViewController.swift @@ -19,7 +19,7 @@ public protocol CenterProfileViewModelable where Input: CenterProfileInputable, var output: Output? { get } } -public protocol CenterProfileInputable: AnyObject { +public protocol CenterProfileInputable { var readyToFetch: PublishRelay { get } var editingButtonPressed: PublishRelay { get } var editingFinishButtonPressed: PublishRelay { get } @@ -28,7 +28,7 @@ public protocol CenterProfileInputable: AnyObject { var selectedImage: PublishRelay { get } } -public protocol CenterProfileOutputable: AnyObject { +public protocol CenterProfileOutputable: DefaultAlertOutputable { var centerName: Driver { get } var centerLocation: Driver { get } var centerPhoneNumber: Driver { get } @@ -36,10 +36,9 @@ public protocol CenterProfileOutputable: AnyObject { var displayingImage: Driver { get } var isEditingMode: Driver { get } var editingValidation: Driver { get } - var alert: Driver { get } } -public class CenterProfileViewController: DisposableViewController { +public class CenterProfileViewController: BaseViewController { var viewModel: (any CenterProfileViewModelable)? @@ -103,7 +102,7 @@ public class CenterProfileViewController: DisposableViewController { let centerPhoneNumeberField: MultiLineTextField = { let textView = MultiLineTextField( typography: .Body3, - placeholderText: "연락처를 입력해주세요." + placeholderText: "추가적으로 요구사항이 있다면 작성해주세요." ) textView.textContainerInset = .init(top: 10, left: 16, bottom: 10, right: 24) textView.isScrollEnabled = false @@ -461,17 +460,6 @@ public class CenterProfileViewController: DisposableViewController { // 바인딩 종료 bindFinished.accept(()) } - - public func showAlert(vo: DefaultAlertContentVO) { - let alret = UIAlertController(title: vo.title, message: vo.message, preferredStyle: .alert) - let close = UIAlertAction(title: "닫기", style: .default, handler: nil) - alret.addAction(close) - present(alret, animated: true, completion: nil) - } - - public func cleanUp() { - - } } extension CenterProfileViewController { diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/BaseViewController.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/BaseViewController.swift new file mode 100644 index 00000000..44579952 --- /dev/null +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/BaseViewController.swift @@ -0,0 +1,22 @@ +// +// BaseViewController.swift +// PresentationCore +// +// Created by choijunios on 7/23/24. +// + +import UIKit +import Entity + +open class BaseViewController: UIViewController { } + +// MARK: Alert +extension BaseViewController { + + public func showAlert(vo: DefaultAlertContentVO) { + let alret = UIAlertController(title: vo.title, message: vo.message, preferredStyle: .alert) + let close = UIAlertAction(title: "닫기", style: .default, handler: nil) + alret.addAction(close) + present(alret, animated: true, completion: nil) + } +} diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Coordinator.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Coordinator.swift index 47c29db7..be593031 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Coordinator.swift +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Coordinator.swift @@ -64,8 +64,6 @@ public extension ParentCoordinator { let child = coordinator as! ChildCoordinator - child.viewControllerRef?.cleanUp() - if let middleViewController = child.viewControllerRef { middleViewControllers.append(middleViewController) @@ -87,11 +85,6 @@ public extension ParentCoordinator { if let lastCoordinator { - if lastCoordinator is ChildCoordinator { - - (lastCoordinator as! ChildCoordinator).viewControllerRef?.cleanUp() - } - self.removeChildCoordinator(lastCoordinator) lastCoordinator.popViewController() } @@ -104,7 +97,7 @@ public extension ParentCoordinator { // MARK: ChildCoordinator public protocol ChildCoordinator: Coordinator { - var viewControllerRef: DisposableViewController? { get } + var viewControllerRef: UIViewController? { get } func coordinatorDidFinish() } diff --git a/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/Auth/RegisterSuccessOutputable.swift b/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/Auth/RegisterSuccessOutputable.swift index 41fb1e51..8d3ad453 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/Auth/RegisterSuccessOutputable.swift +++ b/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/Auth/RegisterSuccessOutputable.swift @@ -7,7 +7,9 @@ import Foundation import RxCocoa +import Entity -public protocol RegisterSuccessOutputable { - var registerValidation: PublishRelay { get } +public protocol RegisterValidationOutputable { + var registerValidation: Driver? { get } + var alert: Driver? { get } } diff --git a/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/CTAButton.swift b/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/CTAButton.swift index a0448ece..b298c04c 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/CTAButton.swift +++ b/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/CTAButton.swift @@ -15,9 +15,9 @@ public enum CTAButtonAction { } public protocol CTAButtonEnableInputable { - var ctaButtonClicked: PublishRelay { get set } + var ctaButtonClicked: PublishRelay { get set } } public protocol CTAButtonEnableOutPutable { - var ctaButtonEnabled: BehaviorSubject? { get } + var ctaButtonEnabled: Driver? { get } } diff --git a/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/DefaultAlertOutputable.swift b/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/DefaultAlertOutputable.swift new file mode 100644 index 00000000..ab9aec40 --- /dev/null +++ b/project/Projects/Presentation/PresentationCore/Sources/ViewModelType/InputOuputConstraint/DefaultAlertOutputable.swift @@ -0,0 +1,14 @@ +// +// DefaultAlertOutputable.swift +// PresentationCore +// +// Created by choijunios on 7/25/24. +// + +import Entity +import RxCocoa + +public protocol DefaultAlertOutputable { + + var alert: Driver { get } +}