diff --git a/CMC/Sources/App/AppCoordinator.swift b/CMC/Sources/App/AppCoordinator.swift index f8bde88..21c8ad2 100644 --- a/CMC/Sources/App/AppCoordinator.swift +++ b/CMC/Sources/App/AppCoordinator.swift @@ -64,7 +64,7 @@ class AppCoordinator: CoordinatorType { ) ) ) - self.pushViewController(viewController: splashViewController) + self.navigationController.pushViewController(splashViewController, animated: false) } } diff --git a/CMC/Sources/Data/DTO/Auth/ConfirmCertifyCodeDTO.swift b/CMC/Sources/Data/DTO/Auth/ConfirmCertifyCodeDTO.swift new file mode 100644 index 0000000..8f4766c --- /dev/null +++ b/CMC/Sources/Data/DTO/Auth/ConfirmCertifyCodeDTO.swift @@ -0,0 +1,21 @@ +// +// ConfirmCertifyCodeDTO.swift +// CMC +// +// Created by Siri on 11/25/23. +// Copyright © 2023 com.softsquared.cmc. All rights reserved. +// + +// MARK: - ConfirmCertifyCodeDTO +struct ConfirmCertifyCodeDTO: Codable { + let isSuccess: Bool + let code: String + let message: String + let result: String + + func toDomain() -> ConfirmCertifyCodeModel { + return ConfirmCertifyCodeModel( + message: result + ) + } +} diff --git a/CMC/Sources/Data/DTO/Auth/ResettingPasswordDTO.swift b/CMC/Sources/Data/DTO/Auth/ResettingPasswordDTO.swift new file mode 100644 index 0000000..6a644ac --- /dev/null +++ b/CMC/Sources/Data/DTO/Auth/ResettingPasswordDTO.swift @@ -0,0 +1,21 @@ +// +// ResettingPasswordDTO.swift +// CMC +// +// Created by Siri on 11/25/23. +// Copyright © 2023 com.softsquared.cmc. All rights reserved. +// + +// MARK: - RessetingPasswordDTO +struct ResettingPasswordDTO: Codable { + let isSuccess: Bool + let code: String + let message: String + let result: String + + func toDomain() -> ResettingPasswordModel { + return ResettingPasswordModel( + message: result + ) + } +} diff --git a/CMC/Sources/Data/DTO/Auth/SendCertifyCodeDTO.swift b/CMC/Sources/Data/DTO/Auth/SendCertifyCodeDTO.swift new file mode 100644 index 0000000..f8cc5e9 --- /dev/null +++ b/CMC/Sources/Data/DTO/Auth/SendCertifyCodeDTO.swift @@ -0,0 +1,23 @@ +// +// SendCertifyCodeDTO.swift +// CMC +// +// Created by Siri on 11/25/23. +// Copyright © 2023 com.softsquared.cmc. All rights reserved. +// + +import Foundation + +// MARK: - SendCertifyCodeDTO +struct SendCertifyCodeDTO: Codable { + let isSuccess: Bool + let code: String + let message: String + let result: String + + func toDomain() -> SendCertifyCodeModel { + return SendCertifyCodeModel( + message: result + ) + } +} diff --git a/CMC/Sources/Data/Endpoint/AuthEndpoint.swift b/CMC/Sources/Data/Endpoint/AuthEndpoint.swift index 783e9c0..16fdf19 100644 --- a/CMC/Sources/Data/Endpoint/AuthEndpoint.swift +++ b/CMC/Sources/Data/Endpoint/AuthEndpoint.swift @@ -13,6 +13,9 @@ enum AuthEndpoint: Endpoint { case signIn(body: SignInBody) case signUp(body: SignUpBody) case emailDup(query: EmailDupQuery) + case sendCertifyCode(query: SendCertifyCodeQuery) + case confirmCertifyCode(body: ConfirmCertifyCodeBody) + case resettingPassword(body: ResettingPasswordBody) var baseURL: URL? { return URL(string: Xcconfig.BASE_URL + "/auth") @@ -20,10 +23,12 @@ enum AuthEndpoint: Endpoint { var method: HTTPMethod { switch self { - case .signUp, .signIn: + case .signUp, .signIn, .confirmCertifyCode: return .POST - case .emailDup: + case .emailDup, .sendCertifyCode: return .GET + case .resettingPassword: + return .PATCH } } @@ -37,11 +42,13 @@ enum AuthEndpoint: Endpoint { var path: String { switch self { case .signUp: - return "/signUp" + return "/sign-up" case .signIn: - return "/signIn" + return "/log-in" case .emailDup: return "/email" + case .sendCertifyCode, .confirmCertifyCode, .resettingPassword: + return "/password" } } @@ -56,6 +63,14 @@ enum AuthEndpoint: Endpoint { return .query([ "email": query.email ]) + case .sendCertifyCode(let query): + return .query([ + "email": query.email + ]) + case .confirmCertifyCode(let body): + return .body(body) + case .resettingPassword(let body): + return .body(body) } } diff --git a/CMC/Sources/Data/NetworkService/Body/Auth/ConfirmCertifyCodeBody.swift b/CMC/Sources/Data/NetworkService/Body/Auth/ConfirmCertifyCodeBody.swift new file mode 100644 index 0000000..72b44f5 --- /dev/null +++ b/CMC/Sources/Data/NetworkService/Body/Auth/ConfirmCertifyCodeBody.swift @@ -0,0 +1,15 @@ +// +// ConfirmCertifyCodeBody.swift +// CMC +// +// Created by Siri on 11/25/23. +// Copyright © 2023 com.softsquared.cmc. All rights reserved. +// + +import Foundation + +// MARK: - ConfirmCertifyCodeBody +struct ConfirmCertifyCodeBody: Codable { + let email: String + let code: String +} diff --git a/CMC/Sources/Data/NetworkService/Body/Auth/ResettingPasswordBody.swift b/CMC/Sources/Data/NetworkService/Body/Auth/ResettingPasswordBody.swift new file mode 100644 index 0000000..7563047 --- /dev/null +++ b/CMC/Sources/Data/NetworkService/Body/Auth/ResettingPasswordBody.swift @@ -0,0 +1,15 @@ +// +// ResettingPasswordBody.swift +// CMC +// +// Created by Siri on 11/25/23. +// Copyright © 2023 com.softsquared.cmc. All rights reserved. +// + +import Foundation + +// MARK: - ResettingPasswordBody +struct ResettingPasswordBody: Codable { + let email: String + let password: String +} diff --git a/CMC/Sources/Data/NetworkService/Body/Auth/SendCertifyCodeQuery.swift b/CMC/Sources/Data/NetworkService/Body/Auth/SendCertifyCodeQuery.swift new file mode 100644 index 0000000..38e3ec6 --- /dev/null +++ b/CMC/Sources/Data/NetworkService/Body/Auth/SendCertifyCodeQuery.swift @@ -0,0 +1,14 @@ +// +// SendCertifyCodeQuery.swift +// CMC +// +// Created by Siri on 11/25/23. +// Copyright © 2023 com.softsquared.cmc. All rights reserved. +// + +import Foundation + +// MARK: - SendCertifyCodeQuery +struct SendCertifyCodeQuery: Codable { + let email: String +} diff --git a/CMC/Sources/Data/NetworkService/Endpoint.swift b/CMC/Sources/Data/NetworkService/Endpoint.swift index f589999..d59f448 100644 --- a/CMC/Sources/Data/NetworkService/Endpoint.swift +++ b/CMC/Sources/Data/NetworkService/Endpoint.swift @@ -9,7 +9,7 @@ import Foundation enum HTTPMethod: String { - case GET, POST, DELETE, FETCH + case GET, POST, DELETE, PATCH } typealias HTTPHeaders = [String: String] diff --git a/CMC/Sources/Data/Repositories/Auth/DefaultAuthRepository.swift b/CMC/Sources/Data/Repositories/Auth/DefaultAuthRepository.swift index 2f2d50a..fe9ab2c 100644 --- a/CMC/Sources/Data/Repositories/Auth/DefaultAuthRepository.swift +++ b/CMC/Sources/Data/Repositories/Auth/DefaultAuthRepository.swift @@ -50,4 +50,38 @@ final class DefaultAuthRepository: AuthRepository { } } + func sendCertifyCode(query: SendCertifyCodeQuery) -> Single { + let endpoint = AuthEndpoint.sendCertifyCode(query: query) + return networkService.request(endpoint) + .flatMap { data in + guard let dto = Utility.decode(SendCertifyCodeDTO.self, from: data) else { + return Single.error(NetworkError.decodingFailed) + } + return Single.just(dto) + } + } + + func confirmCertifyCode(body: ConfirmCertifyCodeBody) -> Single { + let endpoint = AuthEndpoint.confirmCertifyCode(body: body) + return networkService.request(endpoint) + .flatMap { data in + guard let dto = Utility.decode(ConfirmCertifyCodeDTO.self, from: data) else { + return Single.error(NetworkError.decodingFailed) + } + return Single.just(dto) + } + } + + func reSettingPassword(body: ResettingPasswordBody) -> Single { + let endpoint = AuthEndpoint.resettingPassword(body: body) + return networkService.request(endpoint) + .flatMap { data in + guard let dto = Utility.decode(ResettingPasswordDTO.self, from: data) else { + return Single.error(NetworkError.decodingFailed) + } + return Single.just(dto) + } + } + + } diff --git a/CMC/Sources/Data/Repositories/Auth/FakeAuthRepository.swift b/CMC/Sources/Data/Repositories/Auth/FakeAuthRepository.swift index 6ee01e9..c79d51c 100644 --- a/CMC/Sources/Data/Repositories/Auth/FakeAuthRepository.swift +++ b/CMC/Sources/Data/Repositories/Auth/FakeAuthRepository.swift @@ -48,5 +48,34 @@ final class FakeAuthRepository: AuthRepository { ) return Single.just(fakeEmailDupDTO) } + + func sendCertifyCode(query: SendCertifyCodeQuery) -> Single { + let fakeSendCertifyCodeDTO = SendCertifyCodeDTO( + isSuccess: true, + code: "200", + message: "성공", + result: "인증번호가 발송되었습니다." + ) + return Single.just(fakeSendCertifyCodeDTO) + } + + func confirmCertifyCode(body: ConfirmCertifyCodeBody) -> Single { + let fakeConfirmCertifyCodeDTO = ConfirmCertifyCodeDTO( + isSuccess: true, + code: "200", + message: "성공", + result: "인증번호가 발송되었습니다." + ) + return Single.just(fakeConfirmCertifyCodeDTO) + } + func reSettingPassword(body: ResettingPasswordBody) -> Single { + let fakeResettingPasswordDTO = ResettingPasswordDTO( + isSuccess: true, + code: "200", + message: "성공", + result: "인증번호가 발송되었습니다." + ) + return Single.just(fakeResettingPasswordDTO) + } } diff --git a/CMC/Sources/Domain/Models/Auth/ConfirmCertifyCodeModel.swift b/CMC/Sources/Domain/Models/Auth/ConfirmCertifyCodeModel.swift new file mode 100644 index 0000000..4031fa1 --- /dev/null +++ b/CMC/Sources/Domain/Models/Auth/ConfirmCertifyCodeModel.swift @@ -0,0 +1,14 @@ +// +// ConfirmCertifyCodeModel.swift +// CMC +// +// Created by Siri on 11/25/23. +// Copyright © 2023 com.softsquared.cmc. All rights reserved. +// + +import Foundation + +// MARK: - ConfirmCertifyCodeModel +struct ConfirmCertifyCodeModel: Codable { + let message: String +} diff --git a/CMC/Sources/Domain/Models/Auth/ResettingPasswordModel.swift b/CMC/Sources/Domain/Models/Auth/ResettingPasswordModel.swift new file mode 100644 index 0000000..307ce50 --- /dev/null +++ b/CMC/Sources/Domain/Models/Auth/ResettingPasswordModel.swift @@ -0,0 +1,14 @@ +// +// ResettingPasswordModel.swift +// CMC +// +// Created by Siri on 11/25/23. +// Copyright © 2023 com.softsquared.cmc. All rights reserved. +// + +import Foundation + +// MARK: - ResettingPasswordModel +struct ResettingPasswordModel: Codable { + let message: String +} diff --git a/CMC/Sources/Domain/Models/Auth/SendCertifyCodeModel.swift b/CMC/Sources/Domain/Models/Auth/SendCertifyCodeModel.swift new file mode 100644 index 0000000..fb1d474 --- /dev/null +++ b/CMC/Sources/Domain/Models/Auth/SendCertifyCodeModel.swift @@ -0,0 +1,14 @@ +// +// SendCertifyCodeModel.swift +// CMC +// +// Created by Siri on 11/25/23. +// Copyright © 2023 com.softsquared.cmc. All rights reserved. +// + +import Foundation + +// MARK: - SendCertifyCodeModel +struct SendCertifyCodeModel: Codable { + let message: String +} diff --git a/CMC/Sources/Domain/Repositories/Auth/AuthRepository.swift b/CMC/Sources/Domain/Repositories/Auth/AuthRepository.swift index 28704c5..43054fc 100644 --- a/CMC/Sources/Domain/Repositories/Auth/AuthRepository.swift +++ b/CMC/Sources/Domain/Repositories/Auth/AuthRepository.swift @@ -13,4 +13,7 @@ protocol AuthRepository { func signUp(body: SignUpBody) -> Single func signIn(body: SignInBody) -> Single func emailDup(query: EmailDupQuery) -> Single + func sendCertifyCode(query: SendCertifyCodeQuery) -> Single + func confirmCertifyCode(body: ConfirmCertifyCodeBody) -> Single + func reSettingPassword(body: ResettingPasswordBody) -> Single } diff --git a/CMC/Sources/Domain/Usecases/Auth/AuthUsecase.swift b/CMC/Sources/Domain/Usecases/Auth/AuthUsecase.swift index edf236a..67f5c19 100644 --- a/CMC/Sources/Domain/Usecases/Auth/AuthUsecase.swift +++ b/CMC/Sources/Domain/Usecases/Auth/AuthUsecase.swift @@ -13,4 +13,7 @@ protocol AuthUsecase { func signUp(body: SignUpBody) -> Single func signIn(body: SignInBody) -> Single func emailDup(query: EmailDupQuery) -> Single + func sendCertifyCode(query: SendCertifyCodeQuery) -> Single + func confirmCertifyCode(body: ConfirmCertifyCodeBody) -> Single + func reSettingPassword(body: ResettingPasswordBody) -> Single } diff --git a/CMC/Sources/Domain/Usecases/Auth/DefaultAuthUsecase.swift b/CMC/Sources/Domain/Usecases/Auth/DefaultAuthUsecase.swift index da9f644..4de7150 100644 --- a/CMC/Sources/Domain/Usecases/Auth/DefaultAuthUsecase.swift +++ b/CMC/Sources/Domain/Usecases/Auth/DefaultAuthUsecase.swift @@ -32,11 +32,30 @@ final class DefaultAuthUsecase: AuthUsecase { } func emailDup(query: EmailDupQuery) -> Single { - return authRepository.emailDup(query: query) .map { dto in return dto.toDomain() } } + func sendCertifyCode(query: SendCertifyCodeQuery) -> Single { + return authRepository.sendCertifyCode(query: query) + .map { dto in + return dto.toDomain() + } + } + + func confirmCertifyCode(body: ConfirmCertifyCodeBody) -> Single { + return authRepository.confirmCertifyCode(body: body) + .map { dto in + return dto.toDomain() + } + } + + func reSettingPassword(body: ResettingPasswordBody) -> Single { + return authRepository.reSettingPassword(body: body) + .map { dto in + return dto.toDomain() + } + } } diff --git a/CMC/Sources/Presenter/Auth/Coordinators/AuthCoordinator.swift b/CMC/Sources/Presenter/Auth/Coordinators/AuthCoordinator.swift index da221f3..0b24187 100644 --- a/CMC/Sources/Presenter/Auth/Coordinators/AuthCoordinator.swift +++ b/CMC/Sources/Presenter/Auth/Coordinators/AuthCoordinator.swift @@ -45,31 +45,34 @@ class AuthCoordinator: CoordinatorType { CMCIndecatorManager.shared.show() switch state{ case .main: - let mainAuthViewController = MainAuthViewController( - viewModel: MainAuthViewModel( - coordinator: self - ) - ) if self.navigationController.viewControllers.contains(where: {$0 is MainAuthViewController}) { - self.navigationController.popViewController(animated: true) - }else { - self.pushViewController(viewController: mainAuthViewController) - CMCIndecatorManager.shared.hide() + self.navigationController.popToRootViewController(animated: true) } + let mainAuthViewController = MainAuthViewController( + viewModel: MainAuthViewModel( + coordinator: self + ) + ) + self.pushViewController(viewController: mainAuthViewController) + CMCIndecatorManager.shared.hide() case .signUp: + if self.navigationController.viewControllers.contains(where: {$0 is SignUpViewController}) { + self.navigationController.popToRootViewController(animated: true) + } let signUpViewController = SignUpViewController( viewModel: SignUpViewModel( - coordinator: self, - authUsecase: DefaultAuthUsecase( - authRepository: DefaultAuthRepository() - ) - ) - ) + coordinator: self, + authUsecase: DefaultAuthUsecase( + authRepository: DefaultAuthRepository() + ) + ) + ) + self.pushViewController(viewController: signUpViewController) + CMCIndecatorManager.shared.hide() + case .signIn: if self.navigationController.viewControllers.contains(where: {$0 is SignInViewController}) { - self.navigationController.popViewController(animated: true) + self.navigationController.popToRootViewController(animated: true) } - self.pushViewController(viewController: signUpViewController) - case .signIn: let signInViewController = SignInViewController( viewModel: SignInViewModel( coordinator: self, @@ -78,9 +81,6 @@ class AuthCoordinator: CoordinatorType { ) ) ) - if self.navigationController.viewControllers.contains(where: {$0 is SignUpViewController}) { - self.navigationController.popViewController(animated: true) - } self.pushViewController(viewController: signInViewController) CMCIndecatorManager.shared.hide() } diff --git a/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/ConfirmCertifyCode/ConfirmCertifyCodeView.swift b/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/ConfirmCertifyCode/ConfirmCertifyCodeView.swift index 09e83be..c10966a 100644 --- a/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/ConfirmCertifyCode/ConfirmCertifyCodeView.swift +++ b/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/ConfirmCertifyCode/ConfirmCertifyCodeView.swift @@ -51,6 +51,16 @@ final class ConfirmCertifyCodeView: BaseView { return textField }() + private lazy var confirmCertifyCodeButton: CMCButton = { + let button = CMCButton( + isRound: false, + iconTitle: nil, + type: .login(.disabled), + title: "인증번호 확인" + ) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() // MARK: - Properties private var viewModel: ConfirmCertifyCodeViewModel @@ -79,6 +89,7 @@ final class ConfirmCertifyCodeView: BaseView { addSubview(titleLabel) addSubview(subTitle) addSubview(certifyCodeTextField) + addSubview(confirmCertifyCodeButton) } override func setConstraint() { @@ -100,6 +111,11 @@ final class ConfirmCertifyCodeView: BaseView { $0.height.equalTo(74) } + confirmCertifyCodeButton.snp.makeConstraints{ confirmCertifyCodeButton in + confirmCertifyCodeButton.leading.trailing.equalToSuperview().inset(20) + confirmCertifyCodeButton.bottom.equalTo(self.keyboardLayoutGuide.snp.top).offset(-20) + confirmCertifyCodeButton.height.equalTo(56) + } } override func bind() { @@ -108,37 +124,73 @@ final class ConfirmCertifyCodeView: BaseView { .when(.recognized) .withUnretained(self) .subscribe(onNext: { owner, gesture in - let location = gesture.location(in: owner) - if !owner.isPointInsideTextField(location) { - owner.endEditing(true) - } + owner.endEditing(true) }) .disposed(by: disposeBag) + parentViewModel.timerStart + .withUnretained(self) + .subscribe(onNext: { owner, _ in + owner.certifyCodeTextField.resetTimer() + }) + .disposed(by: disposeBag) let input = ConfirmCertifyCodeViewModel.Input( - nowPage: parentViewModel.pageAppeared.asObservable(), + email: parentViewModel.email.asObservable(), certifiedCode: certifyCodeTextField.rx.text.orEmpty.asObservable(), - reSendButtonTapped: certifyCodeTextField.accessoryCMCButton.rx.tap.asObservable() + reSendButtonTapped: certifyCodeTextField.accessoryCMCButton.rx.tap.asObservable(), + certifyCodeTapped: confirmCertifyCodeButton.rx.tap.asObservable() ) let output = viewModel.transform(input: input) - output.startTimer - .withUnretained(self) - .subscribe(onNext: { owner, isStart in - if isStart { - owner.certifyCodeTextField.resetTimer() + output.certifyCodeResult + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] isSuccessed in + guard let ss = self else { return } + if isSuccessed { + ss.parentViewModel.nowPage.accept(3) + } else { + CMCBottomSheetManager.shared.showBottomSheet( + title: "올바르지 않은 인증번호에요", + body: "인증번호를 확인해주세요 :(", + buttonTitle: "확인" + ) } }) .disposed(by: disposeBag) - output.nextAvailable - .withUnretained(self) - .subscribe(onNext: { owner, isAvailable in - owner.parentViewModel.readyForNextButton.accept(isAvailable) + output.resendCertifyCode + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] isSuccessed in + guard let ss = self else { return } + if isSuccessed { + ss.parentViewModel.timerStart.accept(()) + CMCBottomSheetManager.shared.showBottomSheet( + title: "인증번호를 전송했어요", + body: "3분 내 인증번호를 입력해주세요 :)", + buttonTitle: "확인" + ) + } else { + CMCBottomSheetManager.shared.showBottomSheet( + title: "존재하지 않는 계정이에요", + body: "아이디 찾기는 운영진에게 문의해주세요 :)", + buttonTitle: "확인" + ) + } }) .disposed(by: disposeBag) + + output.codeValidation + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] isValid in + guard let ss = self else { return } + isValid + ? ss.confirmCertifyCodeButton.rxType.accept(.login(.inactive)) + : ss.confirmCertifyCodeButton.rxType.accept(.login(.disabled)) + }) + .disposed(by: disposeBag) + } } diff --git a/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/ConfirmCertifyCode/ConfirmCertifyCodeViewModel.swift b/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/ConfirmCertifyCode/ConfirmCertifyCodeViewModel.swift index 43454ea..b2f65a7 100644 --- a/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/ConfirmCertifyCode/ConfirmCertifyCodeViewModel.swift +++ b/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/ConfirmCertifyCode/ConfirmCertifyCodeViewModel.swift @@ -16,50 +16,69 @@ import RxSwift final class ConfirmCertifyCodeViewModel: ViewModelType { struct Input { - let nowPage: Observable + let email: Observable let certifiedCode: Observable let reSendButtonTapped: Observable + let certifyCodeTapped: Observable } struct Output { - let startTimer: Observable - let nextAvailable: Observable + let resendCertifyCode: Observable + let certifyCodeResult: Observable + let codeValidation: Observable } var disposeBag: DisposeBag = DisposeBag() private let usecase: AuthUsecase - private let startTimer = BehaviorRelay(value: false) - private let nextAvailable = BehaviorRelay(value: false) - + // MARK: - Initializers init( usecase: AuthUsecase ) { self.usecase = usecase } - func transform(input: Input) -> Output { - input.nowPage + let resendCertifyCode = input.reSendButtonTapped + .withLatestFrom(input.email) .withUnretained(self) - .subscribe(onNext: { owner, page in - if page == 2 { - owner.startTimer.accept(true) - } - }) - .disposed(by: disposeBag) + .flatMapLatest { owner, email -> Observable in + let query = SendCertifyCodeQuery(email: email) + return owner.usecase.sendCertifyCode(query: query) + .asObservable() + .map { _ in true} + .catch { _ in + return .just(false) + } + } + .share() - input.certifiedCode + let certifyCodeResult = input.certifyCodeTapped + .withLatestFrom(Observable.combineLatest(input.certifiedCode, input.email)) .withUnretained(self) - .subscribe(onNext: { owner, code in - code.count >= 6 ? owner.nextAvailable.accept(true) : owner.nextAvailable.accept(false) - }) - .disposed(by: disposeBag) + .flatMapLatest { owner, result -> Observable in + let (code, email) = result + let body = ConfirmCertifyCodeBody(email: email, code: code) + return owner.usecase.confirmCertifyCode(body: body) + .asObservable() + .map { _ in true} + .catch { _ in + return .just(false) + } + } + .share() + + let codeValidation = input.certifiedCode + .map { code -> Bool in + return code.count >= 6 + } + .asObservable() return Output( - startTimer: startTimer.asObservable(), - nextAvailable: nextAvailable.asObservable() + resendCertifyCode: resendCertifyCode, + certifyCodeResult: certifyCodeResult, + codeValidation: codeValidation ) } diff --git a/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/ResettingPassword/ResettingPasswordView.swift b/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/ResettingPassword/ResettingPasswordView.swift index 93dd4a9..352b797 100644 --- a/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/ResettingPassword/ResettingPasswordView.swift +++ b/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/ResettingPassword/ResettingPasswordView.swift @@ -89,6 +89,19 @@ final class ResettingPasswordView: BaseView { return errorCell }() + + private lazy var reSettingPasswordButton: CMCButton = { + let button = CMCButton( + isRound: false, + iconTitle: nil, + type: .login(.disabled), + title: "비밀번호 변경하기" + ) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + // MARK: - Properties private var viewModel: ResettingPasswordViewModel private var parentViewModel: FindPasswordViewModel @@ -123,12 +136,14 @@ final class ResettingPasswordView: BaseView { mainContentView.addSubview(passwordErrorStackView) mainContentView.addSubview(confirmPasswordTextField) mainContentView.addSubview(passwordCheckErrorCell) + self.addSubview(reSettingPasswordButton) } override func setConstraint() { scrollView.snp.makeConstraints { make in - make.top.leading.trailing.bottom.equalToSuperview() + make.top.leading.trailing.equalToSuperview() + make.bottom.equalTo(reSettingPasswordButton.snp.top) } mainContentView.snp.makeConstraints { make in @@ -166,6 +181,11 @@ final class ResettingPasswordView: BaseView { make.bottom.equalToSuperview().offset(-24) } + reSettingPasswordButton.snp.makeConstraints{ reSettingPasswordButton in + reSettingPasswordButton.leading.trailing.equalToSuperview().inset(20) + reSettingPasswordButton.bottom.equalTo(self.keyboardLayoutGuide.snp.top).offset(-20) + reSettingPasswordButton.height.equalTo(56) + } } override func bind() { @@ -181,7 +201,6 @@ final class ResettingPasswordView: BaseView { }) .disposed(by: disposeBag) - passwordTextField.accessoryState .observe(on: MainScheduler.instance) .withUnretained(self) @@ -199,8 +218,11 @@ final class ResettingPasswordView: BaseView { .disposed(by: disposeBag) let input = ResettingPasswordViewModel.Input( + email: parentViewModel.email.asObservable(), password: passwordTextField.rx.text.orEmpty.asObservable(), - rePassword: confirmPasswordTextField.rx.text.orEmpty.asObservable() + rePassword: confirmPasswordTextField.rx.text.orEmpty.asObservable(), + reSettingPasswordTapped: reSettingPasswordButton.rx.tap.asObservable() + ) let output = viewModel.transform(input: input) @@ -224,10 +246,33 @@ final class ResettingPasswordView: BaseView { }) .disposed(by: disposeBag) - output.nextAvailable - .withUnretained(self) - .subscribe(onNext: { owner, moveNext in - owner.parentViewModel.readyForNextButton.accept(moveNext) + output.reSettingButtonActive + .withUnretained(self) + .subscribe(onNext: { owner, isEnable in + isEnable == true + ? owner.reSettingPasswordButton.rxType.accept(.login(.inactive)) + : owner.reSettingPasswordButton.rxType.accept(.login(.disabled)) + }) + .disposed(by: disposeBag) + + output.reSettingPasswordResult + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] isSuccessed in + guard let ss = self else { return } + if isSuccessed { + CMCBottomSheetManager.shared.showBottomSheet( + title: "비밀번호가 변경되었습니다\n다시 로그인해주세요 :)", + body: nil, + buttonTitle: "확인" + ) + ss.parentViewModel.coordinator?.userActionState.accept(.main) + } else { + CMCBottomSheetManager.shared.showBottomSheet( + title: "비밀번호 변경에 실패하였습니다.", + body: "다시 시도해주세요 :(", + buttonTitle: "확인" + ) + } }) .disposed(by: disposeBag) } diff --git a/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/ResettingPassword/ResettingPasswordViewModel.swift b/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/ResettingPassword/ResettingPasswordViewModel.swift index 22ea516..6c05927 100644 --- a/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/ResettingPassword/ResettingPasswordViewModel.swift +++ b/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/ResettingPassword/ResettingPasswordViewModel.swift @@ -16,21 +16,27 @@ import UIKit final class ResettingPasswordViewModel: ViewModelType { struct Input { + let email: Observable let password: Observable let rePassword: Observable + let reSettingPasswordTapped: Observable } struct Output { let passwordValidations: [Observable] let passwordConfirmRegex: Observable - let nextAvailable: Observable + let reSettingButtonActive: Observable + let reSettingPasswordResult: Observable } var disposeBag: DisposeBag = DisposeBag() + private let usecase: AuthUsecase // MARK: - Initializers - init() { - + init( + usecase: AuthUsecase + ) { + self.usecase = usecase } func transform(input: Input) -> Output { @@ -49,16 +55,32 @@ final class ResettingPasswordViewModel: ViewModelType { let passwordConfirmRegex = Observable.combineLatest(input.password, input.rePassword) .map { $0 == $1 && !$0.isEmpty} - let nextAvailable = Observable.combineLatest( + let reSettingButtonActive = Observable.combineLatest( passwordRegex, passwordConfirmRegex ) .map { $0 && $1 } + let reSettingPasswordResult = input.reSettingPasswordTapped + .withLatestFrom(Observable.combineLatest(input.email, input.password)) + .withUnretained(self) + .flatMapLatest { owner, result -> Observable in + let (email, password) = result + let body = ResettingPasswordBody(email: email, password: password) + return owner.usecase.reSettingPassword(body: body) + .asObservable() + .map { _ in true} + .catch { _ in + return .just(false) + } + } + .share() + return Output( passwordValidations: passwordValidations, passwordConfirmRegex: passwordConfirmRegex, - nextAvailable: nextAvailable + reSettingButtonActive: reSettingButtonActive, + reSettingPasswordResult: reSettingPasswordResult ) } diff --git a/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/SendCertifyCode/SendCertifyCodeView.swift b/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/SendCertifyCode/SendCertifyCodeView.swift index 73d4a96..2b6fae1 100644 --- a/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/SendCertifyCode/SendCertifyCodeView.swift +++ b/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/SendCertifyCode/SendCertifyCodeView.swift @@ -51,6 +51,16 @@ final class SendCertifyCodeView: BaseView { return textField }() + private lazy var receiveCertiftyButton: CMCButton = { + let button = CMCButton( + isRound: false, + iconTitle: nil, + type: .login(.disabled), + title: "인증번호 받기" + ) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() // MARK: - Properties private var viewModel: SendCertifyCodeViewModel @@ -79,6 +89,7 @@ final class SendCertifyCodeView: BaseView { addSubview(titleLabel) addSubview(subTitle) addSubview(emailTextField) + addSubview(receiveCertiftyButton) } override func setConstraint() { @@ -100,6 +111,11 @@ final class SendCertifyCodeView: BaseView { $0.height.equalTo(74) } + receiveCertiftyButton.snp.makeConstraints{ receiveCertiftyButton in + receiveCertiftyButton.leading.trailing.equalToSuperview().inset(20) + receiveCertiftyButton.bottom.equalTo(self.keyboardLayoutGuide.snp.top).offset(-20) + receiveCertiftyButton.height.equalTo(56) + } } override func bind() { @@ -115,23 +131,48 @@ final class SendCertifyCodeView: BaseView { }) .disposed(by: disposeBag) + self.emailTextField.rx.text.orEmpty + .bind(to: parentViewModel.email) + .disposed(by: disposeBag) + let input = SendCertifyCodeViewModel.Input( - email: emailTextField.rx.text.orEmpty.asObservable() + email: emailTextField.rx.text.orEmpty.asObservable(), + receiveCertifyTapped: receiveCertiftyButton.rx.tap.asObservable() ) let output = viewModel.transform(input: input) - Observable.combineLatest( - output.certifyEmail, - output.emailValidation - ) + output.emailValidation .withUnretained(self) .subscribe(onNext: { owner, isEnable in - let (certifyEmail, emailValidation) = isEnable - owner.parentViewModel.readyForNextButton.accept(certifyEmail && emailValidation) + isEnable == true + ? owner.receiveCertiftyButton.rxType.accept(.login(.inactive)) + : owner.receiveCertiftyButton.rxType.accept(.login(.disabled)) }) .disposed(by: disposeBag) + output.sendCertifyResult + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] isSuccessed in + guard let ss = self else { return } + if isSuccessed { + ss.parentViewModel.nowPage.accept(2) + ss.parentViewModel.timerStart.accept(()) + CMCBottomSheetManager.shared.showBottomSheet( + title: "인증번호를 전송했어요", + body: "3분 내 인증번호를 입력해주세요 :)", + buttonTitle: "확인" + ) + } else { + CMCBottomSheetManager.shared.showBottomSheet( + title: "존재하지 않는 계정이에요", + body: "아이디 찾기는 운영진에게 문의해주세요 :)", + buttonTitle: "확인" + ) + } + }) + .disposed(by: disposeBag) + } } diff --git a/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/SendCertifyCode/SendCertifyCodeViewModel.swift b/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/SendCertifyCode/SendCertifyCodeViewModel.swift index d7b6dbe..d00544a 100644 --- a/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/SendCertifyCode/SendCertifyCodeViewModel.swift +++ b/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordScrollPages/SendCertifyCode/SendCertifyCodeViewModel.swift @@ -17,29 +17,46 @@ final class SendCertifyCodeViewModel: ViewModelType { struct Input { let email: Observable + let receiveCertifyTapped: Observable } struct Output { - let certifyEmail: Observable + let sendCertifyResult: Observable let emailValidation: Observable } + // MARK: - Properties var disposeBag: DisposeBag = DisposeBag() + private let usecase: AuthUsecase + + // MARK: - Initializers + init( + usecase: AuthUsecase + ) { + self.usecase = usecase + } private var allcertifyEmailRelay = BehaviorRelay(value: false) func transform(input: Input) -> Output { let emailValidation: Observable = Utility.checkEmailValidation(email: input.email, validate: .emailRegex) - input.email + let sendCertifyResult = input.receiveCertifyTapped + .withLatestFrom(input.email) .withUnretained(self) - .subscribe(onNext: { owner, email in - owner.allcertifyEmailRelay.accept(!email.isEmpty) - }) - .disposed(by: disposeBag) + .flatMapLatest { owner, email -> Observable in + let query = SendCertifyCodeQuery(email: email) + return owner.usecase.sendCertifyCode(query: query) + .asObservable() + .map { _ in true} + .catch { _ in + return .just(false) + } + } + .share() return Output( - certifyEmail: allcertifyEmailRelay.asObservable(), + sendCertifyResult: sendCertifyResult, emailValidation: emailValidation ) } diff --git a/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordViewController.swift b/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordViewController.swift index f234cdd..938f255 100644 --- a/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordViewController.swift +++ b/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordViewController.swift @@ -31,7 +31,11 @@ class FindPasswordViewController: BaseViewController { private lazy var emailView: SendCertifyCodeView = { let view = SendCertifyCodeView( - viewModel: SendCertifyCodeViewModel(), + viewModel: SendCertifyCodeViewModel( + usecase: DefaultAuthUsecase( + authRepository: DefaultAuthRepository() + ) + ), parentViewModel: viewModel ) view.translatesAutoresizingMaskIntoConstraints = false @@ -53,7 +57,11 @@ class FindPasswordViewController: BaseViewController { private lazy var resettingPasswordView: ResettingPasswordView = { let view = ResettingPasswordView( - viewModel: ResettingPasswordViewModel(), + viewModel: ResettingPasswordViewModel( + usecase: DefaultAuthUsecase( + authRepository: DefaultAuthRepository() + ) + ), parentViewModel: viewModel ) view.translatesAutoresizingMaskIntoConstraints = false @@ -71,25 +79,9 @@ class FindPasswordViewController: BaseViewController { return scrollView }() - private lazy var nextButton: CMCButton = { - let button = CMCButton( - isRound: false, - iconTitle: nil, - type: .login(.inactive), - title: "인증번호 전송하기" - ) - button.translatesAutoresizingMaskIntoConstraints = false - return button - }() - // MARK: - Properties private let viewModel: FindPasswordViewModel private let nowPage = BehaviorRelay(value: 1) - private let nextButtonTitles: [String] = [ - "인증번호 전송하기", - "인증번호 확인하기", - "비밀번호 재설정" - ] private var contentOffset: Double = 0 // MARK: - Initializers @@ -108,7 +100,7 @@ class FindPasswordViewController: BaseViewController { reSettingPasswordPager.addSubview(page) page.snp.makeConstraints { make in make.top.equalTo(navigationBar.snp.bottom) - make.bottom.equalTo(nextButton.snp.top).offset(-12) + make.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom) make.width.equalTo(self.view.frame.size.width) make.leading.equalToSuperview().offset(CGFloat(index) * self.view.frame.size.width) } @@ -124,7 +116,6 @@ class FindPasswordViewController: BaseViewController { override func setAddSubView() { self.view.addSubview(navigationBar) self.view.addSubview(reSettingPasswordPager) - self.view.addSubview(nextButton) } @@ -135,57 +126,31 @@ class FindPasswordViewController: BaseViewController { navigationBar.height.equalTo(68) } - nextButton.snp.makeConstraints{ nextButton in - nextButton.leading.trailing.equalToSuperview().inset(20) - nextButton.bottom.equalTo(self.view.keyboardLayoutGuide.snp.top).offset(-20) - nextButton.height.equalTo(56) - } - reSettingPasswordPager.snp.makeConstraints{ cmcPager in cmcPager.top.equalTo(navigationBar.snp.bottom) cmcPager.leading.trailing.equalToSuperview() - cmcPager.bottom.equalTo(nextButton.snp.top).offset(-12) + cmcPager.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom) } } override func bind() { - nextButton.rx.tap - .withUnretained(self) - .subscribe(onNext: { owner, _ in - let page = owner.nowPage.value + 1 - owner.nowPage.accept(page) - owner.view.endEditing(true) - }) - .disposed(by: disposeBag) - nowPage.asObservable() + let input = FindPasswordViewModel.Input( + backButtonTapped: navigationBar.backButton.rx.tapped().asObservable() + ) + + let output = viewModel.transform(input: input) + + + output.afterPage .subscribe(onNext: { [weak self] page in guard let self = self else { return } let xOffset = CGFloat(page - 1) * CGFloat(self.view.frame.width) self.reSettingPasswordPager.setContentOffset( CGPoint(x: xOffset, y: 0), animated: true ) - let title = self.nextButtonTitles[page - 1] - self.nextButton.setTitle(title: title) - }) - .disposed(by: disposeBag) - - let input = FindPasswordViewModel.Input( - backButtonTapped: navigationBar.backButton.rx.tapped().asObservable(), - nextButtonTapped: nextButton.rx.tap.asObservable(), - nowPage: nowPage.asObservable() - ) - - let output = viewModel.transform(input: input) - - output.readyForNextButton - .withUnretained(self) - .subscribe(onNext: { owner, isActive in - isActive - ? owner.nextButton.makeCustomState(type: .login(.inactive)) - : owner.nextButton.makeCustomState(type: .login(.disabled)) }) .disposed(by: disposeBag) diff --git a/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordViewModel.swift b/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordViewModel.swift index 8a45f50..665955e 100644 --- a/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordViewModel.swift +++ b/CMC/Sources/Presenter/Auth/SignIn/FindPassword/FindPasswordViewModel.swift @@ -20,12 +20,10 @@ class FindPasswordViewModel: ViewModelType{ struct Input { let backButtonTapped: Observable - let nextButtonTapped: Observable - let nowPage: Observable } struct Output { - let readyForNextButton: Observable + let afterPage: Observable } // MARK: - Properties @@ -34,10 +32,9 @@ class FindPasswordViewModel: ViewModelType{ var disposeBag: DisposeBag = DisposeBag() weak var coordinator: AuthCoordinator? - let readyForNextButton = BehaviorRelay(value: true) - let pageAppeared = BehaviorRelay(value: 1) - let email = BehaviorRelay(value: "") + let nowPage = BehaviorRelay(value: 1) + let timerStart = PublishRelay() // MARK: - Initializers init( @@ -50,13 +47,6 @@ class FindPasswordViewModel: ViewModelType{ // MARK: - Methods func transform(input: Input) -> Output { - - input.nextButtonTapped - .withUnretained(self) - .subscribe(onNext: { owner, _ in - owner.readyForNextButton.accept(false) - }) - .disposed(by: disposeBag) input.backButtonTapped .withUnretained(self) @@ -66,15 +56,8 @@ class FindPasswordViewModel: ViewModelType{ }) .disposed(by: disposeBag) - input.nowPage - .withUnretained(self) - .subscribe(onNext: { owner, nowPage in - owner.pageAppeared.accept(nowPage) - }) - .disposed(by: disposeBag) - return Output( - readyForNextButton: readyForNextButton.asObservable() + afterPage: nowPage.asObservable() ) } } diff --git a/DesignSystem/Sources/CMCTextField+Timer.swift b/DesignSystem/Sources/CMCTextField+Timer.swift index aff0a0e..c4f2079 100644 --- a/DesignSystem/Sources/CMCTextField+Timer.swift +++ b/DesignSystem/Sources/CMCTextField+Timer.swift @@ -219,6 +219,7 @@ public final class CMCTextField_Timer: UIView { } public func resetTimer() { + timerDisposeBag = DisposeBag() // 타이머 구독 해제 timerCountRelay.accept(180) // 초기 시간을 180초로 설정 startTimer() // 타이머 다시 시작 }