From d0097ceaa4e0d85b3a5231a6ae88d615a20306ac Mon Sep 17 00:00:00 2001 From: J0onYEong Date: Sun, 25 Aug 2024 16:14:14 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[IDLE-000]=20Feat,=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=ED=99=94=EB=A9=B4,=20=ED=83=88=ED=87=B4=EC=82=AC=EC=9C=A0=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WorkerSettingCoordinator.swift | 62 +++++ .../ExampleApp/Sources/SceneDelegate.swift | 2 +- .../WorkerSettingScreenCoordinator.swift | 73 ++++++ .../View/Setting/WorkerSettingVC.swift | 242 ++++++++++++++++++ .../ViewModel/Seting/WorkerSettingVM.swift | 204 +++++++++++++++ .../CenterSettingScreenCoordinatable.swift | 2 +- .../WorkerSettingScreenCoordinatable.swift | 13 + 7 files changed, 596 insertions(+), 2 deletions(-) create mode 100644 project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerSettingCoordinator.swift create mode 100644 project/Projects/Presentation/Feature/Worker/Sources/Coordinator/Setting/WorkerSettingScreenCoordinator.swift create mode 100644 project/Projects/Presentation/Feature/Worker/Sources/View/Setting/WorkerSettingVC.swift create mode 100644 project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Seting/WorkerSettingVM.swift create mode 100644 project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/WorkerSettingScreenCoordinatable.swift diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerSettingCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerSettingCoordinator.swift new file mode 100644 index 00000000..0ab3c00c --- /dev/null +++ b/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerSettingCoordinator.swift @@ -0,0 +1,62 @@ +// +// WorkerSettingCoordinator.swift +// Idle-iOS +// +// Created by choijunios on 8/25/24. +// + +import UIKit +import WorkerFeature +import RootFeature +import PresentationCore +import UseCaseInterface + +class WorkerSettingCoordinaator: WorkerSettingScreenCoordinatable { + + struct Dependency { + let parent: WorkerMainCoordinator + let injector: Injector + let navigationController: UINavigationController + } + + var childCoordinators: [any PresentationCore.Coordinator] = [] + + weak var parent: WorkerMainCoordinator? + + weak var viewControllerRef: UIViewController? + + var navigationController: UINavigationController + let injector: Injector + + init(dependency: Dependency) { + self.navigationController = dependency.navigationController + self.injector = dependency.injector + self.parent = dependency.parent + } + + public func start() { + let coordinator = WorkerSettingScreenCoordinator( + dependency: .init( + navigationController: navigationController, + settingUseCase: injector.resolve(SettingScreenUseCase.self), + centerProfileUseCase: injector.resolve(CenterProfileUseCase.self) + ) + ) + addChildCoordinator(coordinator) + coordinator.parent = self + coordinator.start() + } + + public func startRemoveWorkerAccountFlow() { + let coordinator = DeRegisterCoordinator( + dependency: .init( + userType: .center, + settingUseCase: injector.resolve(SettingScreenUseCase.self), + navigationController: navigationController + ) + ) + addChildCoordinator(coordinator) + coordinator.parent = self + coordinator.start() + } +} diff --git a/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift index 721d2aa6..d1a21d9b 100644 --- a/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Worker/ExampleApp/Sources/SceneDelegate.swift @@ -44,7 +44,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let nav = UINavigationController(rootViewController: vc) nav.setNavigationBarHidden(true, animated: false) - window?.rootViewController = nav + window?.rootViewController = WorkerSettingVC() window?.makeKeyAndVisible() } } diff --git a/project/Projects/Presentation/Feature/Worker/Sources/Coordinator/Setting/WorkerSettingScreenCoordinator.swift b/project/Projects/Presentation/Feature/Worker/Sources/Coordinator/Setting/WorkerSettingScreenCoordinator.swift new file mode 100644 index 00000000..828d360d --- /dev/null +++ b/project/Projects/Presentation/Feature/Worker/Sources/Coordinator/Setting/WorkerSettingScreenCoordinator.swift @@ -0,0 +1,73 @@ +// +// WorkerSettingScreenCoordinator.swift +// WorkerFeature +// +// Created by choijunios on 8/25/24. +// + +import UIKit +import PresentationCore +import UseCaseInterface +import Entity + +public class WorkerSettingScreenCoordinator: ChildCoordinator { + + public struct Dependency { + let navigationController: UINavigationController + let settingUseCase: SettingScreenUseCase + let centerProfileUseCase: CenterProfileUseCase + + public init(navigationController: UINavigationController, settingUseCase: SettingScreenUseCase, centerProfileUseCase: CenterProfileUseCase) { + self.navigationController = navigationController + self.settingUseCase = settingUseCase + self.centerProfileUseCase = centerProfileUseCase + } + } + + public weak var viewControllerRef: UIViewController? + public weak var parent: WorkerSettingScreenCoordinatable? + + public let navigationController: UINavigationController + let settingUseCase: SettingScreenUseCase + let centerProfileUseCase: CenterProfileUseCase + + public init( + dependency: Dependency + ) { + self.navigationController = dependency.navigationController + self.settingUseCase = dependency.settingUseCase + self.centerProfileUseCase = dependency.centerProfileUseCase + } + + deinit { + printIfDebug("\(String(describing: WorkerSettingScreenCoordinator.self))") + } + + + public func start() { + let vc = WorkerSettingVC() + let vm = WorkerSettingVM( + coordinator: self, + settingUseCase: settingUseCase, + centerProfileUseCase: centerProfileUseCase + ) + vc.bind(viewModel: vm) + viewControllerRef = vc + navigationController.pushViewController(vc, animated: false) + } + + public func coordinatorDidFinish() { + parent?.removeChildCoordinator(self) + popViewController() + } + + func popToRoot() { + + /// Root까지 네비게이션을 제거합니다. + NotificationCenter.default.post(name: .popToInitialVC, object: nil) + } + + func startRemoveWorkerAccountFlow() { + parent?.startRemoveWorkerAccountFlow() + } +} diff --git a/project/Projects/Presentation/Feature/Worker/Sources/View/Setting/WorkerSettingVC.swift b/project/Projects/Presentation/Feature/Worker/Sources/View/Setting/WorkerSettingVC.swift new file mode 100644 index 00000000..329c27d9 --- /dev/null +++ b/project/Projects/Presentation/Feature/Worker/Sources/View/Setting/WorkerSettingVC.swift @@ -0,0 +1,242 @@ +// +// WorkerSettingVC.swift +// WorkerFeature +// +// Created by choijunios on 8/25/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit + +public class WorkerSettingVC: BaseViewController { + + var viewModel: WorkerSettingVMable? + + // Init + + // View + let titleBar: IdleTitleBar = { + let bar = IdleTitleBar(titleText: "설정", innerViews: []) + return bar + }() + + let myProfileButton: FullRowButton = { + let button = FullRowButton(labelText: "내 프로필 정보") + return button + }() + + let pushNotificationAuthRow: PushNotificationAuthRow = { + let row = PushNotificationAuthRow() + return row + }() + let frequentQuestionButton: FullRowButton = { + let button = FullRowButton(labelText: "자주 묻는 질문") + return button + }() + let askButton: FullRowButton = { + let button = FullRowButton(labelText: "문의하기") + return button + }() + let applicationPolicyButton: FullRowButton = { + let button = FullRowButton(labelText: "약관및 정책") + return button + }() + let personalDataProcessingPolicyButton: FullRowButton = { + let button = FullRowButton(labelText: "개인정보 처리방침") + return button + }() + let signOutButton: IdleUnderLineLabelButton = { + let button = IdleUnderLineLabelButton(labelText: "로그아웃") + button.backgroundColor = .clear + return button + }() + let removeAccountButton: IdleUnderLineLabelButton = { + let button = IdleUnderLineLabelButton(labelText: "회원 탈퇴") + button.backgroundColor = .clear + return button + }() + + + private let disposeBag = DisposeBag() + + public init() { + super.init(nibName: nil, bundle: nil) + } + + public required init?(coder: NSCoder) { fatalError() } + + public func bind(viewModel: WorkerSettingVMable) { + + self.viewModel = viewModel + + // Input + myProfileButton.rx.tap + .bind(to: viewModel.myProfileButtonClicked) + .disposed(by: disposeBag) + + pushNotificationAuthRow.`switch`.rx.isOn + .map({ [pushNotificationAuthRow] isOn in + + // On / Off 여부는 ViewModel이 설정한다. + pushNotificationAuthRow.`switch`.setOn(false, animated: false) + + return isOn + }) + .bind(to: viewModel.approveToPushNotification) + .disposed(by: disposeBag) + + removeAccountButton.rx.tap + .bind(to: viewModel.removeAccountButtonClicked) + .disposed(by: disposeBag) + + // Output + viewModel + .pushNotificationApproveState? + .drive(onNext: { [weak self] isOn in + self?.pushNotificationAuthRow.`switch`.setOn(isOn, animated: true) + }) + .disposed(by: disposeBag) + + // MARK: 세팅화면으로 이동 + viewModel + .showSettingAlert? + .drive(onNext: { + if let settingsUrl = URL(string: UIApplication.openSettingsURLString) { + if UIApplication.shared.canOpenURL(settingsUrl) { + UIApplication.shared.open(settingsUrl, options: [:], completionHandler: nil) + } + } + }) + .disposed(by: disposeBag) + + viewModel.alert? + .drive(onNext: { [weak self] alertVO in + self?.showAlert(vo: alertVO) + }) + .disposed(by: disposeBag) + + } + + public override func viewDidLoad() { + super.viewDidLoad() + setAppearance() + setLayout() + setObservable() + } + + private func setAppearance() { + view.backgroundColor = DSColor.gray0.color + } + + private func setLayout() { + + let scrollView = UIScrollView() + scrollView.backgroundColor = DSColor.gray050.color + + let contentGuide = scrollView.contentLayoutGuide + let frameGuide = scrollView.frameLayoutGuide + + let viewList = [ + myProfileButton, + Spacer(height: 8), + pushNotificationAuthRow, + Spacer(height: 8), + frequentQuestionButton, + askButton, + Spacer(height: 8), + applicationPolicyButton, + Spacer(height: 8), + personalDataProcessingPolicyButton, + Spacer(height: 20), + HStack([Spacer(width: 20), signOutButton, Spacer()]), + Spacer(height: 24), + HStack([Spacer(width: 20), removeAccountButton, Spacer()]), + ] + + let contentView = VStack(viewList, alignment: .fill) + + scrollView.addSubview(contentView) + contentView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + + contentView.topAnchor.constraint(equalTo: contentGuide.topAnchor), + contentView.leftAnchor.constraint(equalTo: contentGuide.leftAnchor), + contentView.rightAnchor.constraint(equalTo: contentGuide.rightAnchor), + contentView.bottomAnchor.constraint(equalTo: contentGuide.bottomAnchor), + + contentView.widthAnchor.constraint(equalTo: frameGuide.widthAnchor), + ]) + + + // main view + [ + titleBar, + scrollView + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + NSLayoutConstraint.activate([ + titleBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + titleBar.leftAnchor.constraint(equalTo: view.leftAnchor), + titleBar.rightAnchor.constraint(equalTo: view.rightAnchor), + + scrollView.topAnchor.constraint(equalTo: titleBar.bottomAnchor), + scrollView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor), + scrollView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + private func setObservable() { + + // 설정화면에 종속적인 뷰들입니다. + frequentQuestionButton.rx.tap + .subscribe(onNext: { + + // 자주하는 질문뷰 + + }) + .disposed(by: disposeBag) + + askButton.rx.tap + .subscribe(onNext: { + + // 문의하기 뷰 + + }) + .disposed(by: disposeBag) + + applicationPolicyButton.rx.tap + .subscribe(onNext: { + + // 약관및 정책 + + }) + .disposed(by: disposeBag) + + personalDataProcessingPolicyButton.rx.tap + .subscribe(onNext: { + + // 개인정보 처리방침 + + }) + .disposed(by: disposeBag) + + signOutButton + .rx.tap + .subscribe(onNext: { [weak self] _ in + guard let self, let vm = viewModel else { return } + let signOutVM = vm.createSingOutVM() + showIdleModal(viewModel: signOutVM) + }) + .disposed(by: disposeBag) + } +} diff --git a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Seting/WorkerSettingVM.swift b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Seting/WorkerSettingVM.swift new file mode 100644 index 00000000..334d10b2 --- /dev/null +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Seting/WorkerSettingVM.swift @@ -0,0 +1,204 @@ +// +// WorkerSettingVM.swift +// WorkerFeature +// +// Created by choijunios on 8/25/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit +import UseCaseInterface +import UserNotifications + +public protocol WorkerSettingVMable { + + // Input + var viewWillAppear: PublishRelay { get } + var myProfileButtonClicked: PublishRelay { get } + var approveToPushNotification: PublishRelay { get } + + var signOutButtonComfirmed: PublishRelay { get } + var removeAccountButtonClicked: PublishRelay { get } + + // Output + var pushNotificationApproveState: Driver? { get } + var showSettingAlert: Driver? { get } + var alert: Driver? { get } + + // SignOut + func createSingOutVM() -> IdleAlertViewModelable +} + +public class WorkerSettingVM: WorkerSettingVMable { + + // Init + weak var coordinator: WorkerSettingScreenCoordinator? + let settingUseCase: SettingScreenUseCase + let centerProfileUseCase: CenterProfileUseCase + + public var viewWillAppear: RxRelay.PublishRelay = .init() + public var myProfileButtonClicked: RxRelay.PublishRelay = .init() + public var approveToPushNotification: RxRelay.PublishRelay = .init() + + public var signOutButtonComfirmed: RxRelay.PublishRelay = .init() + public var removeAccountButtonClicked: RxRelay.PublishRelay = .init() + + public var pushNotificationApproveState: RxCocoa.Driver? + public var showSettingAlert: Driver? + public var alert: RxCocoa.Driver? + + let disposeBag = DisposeBag() + + public init( + coordinator: WorkerSettingScreenCoordinator?, + settingUseCase: SettingScreenUseCase, + centerProfileUseCase: CenterProfileUseCase + ) + { + self.coordinator = coordinator + self.settingUseCase = settingUseCase + self.centerProfileUseCase = centerProfileUseCase + + + // 기존의 알람수신 동의 여부 확인 + // 설정화면에서 다시돌아온 경우 이벤트 수신 + let refreshNotificationStatusRequest = Observable.merge( + NotificationCenter.default.rx.notification(UIApplication.didBecomeActiveNotification).map {_ in }, + viewWillAppear.asObservable() + ) + + let currentNotificationAuthStatus = refreshNotificationStatusRequest + .flatMap { [settingUseCase] _ in + settingUseCase.checkPushNotificationApproved() + } + + let requestApproveNotification = approveToPushNotification.filter { $0 }.map { _ in () } + let requestDenyNotification = approveToPushNotification.filter { !$0 }.map { _ in () } + + let approveRequestResult = requestApproveNotification + .flatMap { [settingUseCase] _ in + settingUseCase + .requestNotificationPermission() + } + + let approveGranted = approveRequestResult.filter { $0 == .granted } + let openSettingAppToApprove = approveRequestResult.filter { $0 == .openSystemSetting }.map { _ in () } + let approveRequestError = approveRequestResult.filter { + if case let .error(message) = $0 { + printIfDebug("알림동의 실패: \(message)") + return true + } + return false + } + + // MARK: 뷰가 표시할 알람수신 동의 상태 + pushNotificationApproveState = Observable.merge( + currentNotificationAuthStatus, + approveGranted.map { _ in true } + ) + .asDriver(onErrorJustReturn: false) + + + // MARK: 세팅앱 열기 + showSettingAlert = Observable.merge( + openSettingAppToApprove, + requestDenyNotification + ) + .asDriver(onErrorJustReturn: ()) + + // MARK: 로그아웃 + let signOutRequestResult = signOutButtonComfirmed.flatMap({ [settingUseCase] _ in + settingUseCase.signoutCenterAccount() + }) + .share() + + let signOutSuccess = signOutRequestResult.compactMap { $0.value } + let signOutFailure = signOutRequestResult.compactMap { $0.error } + + signOutSuccess + .subscribe(onNext: { [weak self] _ in + + // ‼️ ‼️ 로컬에 저장된 계정 정보 삭제 ‼️ ‼️ + + self?.coordinator?.popToRoot() + }) + .disposed(by: disposeBag) + + + // MARK: 회원 탈퇴 + removeAccountButtonClicked + .subscribe(onNext: { [weak self] _ in + + self?.coordinator?.startRemoveWorkerAccountFlow() + }) + .disposed(by: disposeBag) + + + // MARK: Alert + alert = Observable.merge( + approveRequestError.map { _ in "알람수신 동의 실패" }, + signOutFailure.map { $0.message } + ) + .map({ message in + AlertWithCompletionVO( + title: "환경설정 오류", + message: message + ) + }) + .asDriver(onErrorJustReturn: .default) + } + + public func createSingOutVM() -> any DSKit.IdleAlertViewModelable { + let viewModel = CenterSingOutVM( + title: "로그아웃하시겠어요?", + description: "", + acceptButtonLabelText: "로그아웃", + cancelButtonLabelText: "취소하기" + ) + + viewModel + .acceptButtonClicked + .bind(to: signOutButtonComfirmed) + .disposed(by: disposeBag) + + return viewModel + } +} + +class CenterSingOutVM: IdleAlertViewModelable { + + var acceptButtonLabelText: String + var cancelButtonLabelText: String + + var acceptButtonClicked: RxRelay.PublishRelay = .init() + var cancelButtonClicked: RxRelay.PublishRelay = .init() + + var dismiss: RxCocoa.Driver? + + var title: String + var description: String + + init( + title: String, + description: String, + acceptButtonLabelText: String, + cancelButtonLabelText: String + ) { + self.title = title + self.description = description + self.acceptButtonLabelText = acceptButtonLabelText + self.cancelButtonLabelText = cancelButtonLabelText + + dismiss = Observable + .merge( + acceptButtonClicked.asObservable(), + cancelButtonClicked.asObservable() + ) + .asDriver(onErrorDriveWith: .never()) + } +} diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/CenterSettingScreenCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/CenterSettingScreenCoordinatable.swift index d1222f61..7c85b61c 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/CenterSettingScreenCoordinatable.swift +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/CenterSettingScreenCoordinatable.swift @@ -1,5 +1,5 @@ // -// asd.swift +// CenterSettingScreenCoordinatable.swift // PresentationCore // // Created by choijunios on 8/25/24. diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/WorkerSettingScreenCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/WorkerSettingScreenCoordinatable.swift new file mode 100644 index 00000000..9bbf7d77 --- /dev/null +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/WorkerSettingScreenCoordinatable.swift @@ -0,0 +1,13 @@ +// +// WorkerSettingScreenCoordinatable.swift +// PresentationCore +// +// Created by choijunios on 8/25/24. +// + +import Foundation + +public protocol WorkerSettingScreenCoordinatable: ParentCoordinator { + /// 요양보호사 계정을 지우는 작업을 시작합니다. + func startRemoveWorkerAccountFlow() +} From 53c06bef34de4227ad3f99583dfc349096788247 Mon Sep 17 00:00:00 2001 From: J0onYEong Date: Sun, 25 Aug 2024 18:53:44 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[IDLE-000]=20Feat,=20=EB=A7=88=EC=A7=80?= =?UTF-8?q?=EB=A7=89=20=ED=9C=B4=EB=8C=80=EC=A0=84=ED=99=94=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CenterSettingCoordinator.swift | 1 + .../WorkerSettingCoordinator.swift | 3 +- .../Main/Worker/WorkerMainCoordinator.swift | 8 +- .../RootCoordinator/RootCoordinator.swift | 2 +- .../Auth/DefaultAuthRepository.swift | 21 +- .../Data/NetworkDataSource/API/AuthAPI.swift | 14 + .../Setting/DefaultSettingUseCase.swift | 10 + .../Auth/Login/AuthRepository.swift | 2 + .../Setting/SettingScreenUseCase .swift | 8 + .../Navigation/IdleNavigationBar.swift | 2 +- .../ValidatePhoneNumberViewController.swift | 11 +- .../AuthInOutStreamManager+Name.swift | 2 +- .../AuthInOutStreamManager+PhoneNumber.swift | 8 +- .../PasswordForDeregisterCoordinator.swift | 7 +- .../Setting/PasswordForDeregisterVC.swift | 9 +- .../Setting/PasswordForDeregisterVM.swift | 17 +- .../CenterDeregisterReasonsVM.swift | 0 .../Coordinator/DeRegisterCoordinator.swift | 22 +- .../Sub/SelectReasonCoordinator.swift | 6 +- ...erValidationForDeregisterCoordinator.swift | 78 +++++ ...PhoneNumberValidationForDeregisterVC.swift | 309 ++++++++++++++++++ ...PhoneNumberValidationForDeregisterVM.swift | 116 +++++++ .../Worker/WorkerDeregisterReasonsVM.swift | 35 ++ .../ViewModel/Seting/WorkerSettingVM.swift | 2 +- .../Deregister/DeregisterCoordinatable.swift | 3 + 25 files changed, 662 insertions(+), 34 deletions(-) rename project/Projects/Presentation/Feature/Root/Sources/Screen/{Common/Deregister/ViewModel => Center}/CenterDeregisterReasonsVM.swift (100%) create mode 100644 project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/PhoneNumberValidationForDeregisterCoordinator.swift create mode 100644 project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/PhoneNumberValidationForDeregisterVC.swift create mode 100644 project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/PhoneNumberValidationForDeregisterVM.swift create mode 100644 project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/WorkerDeregisterReasonsVM.swift diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Center /SubCoordinator/CenterSettingCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Center /SubCoordinator/CenterSettingCoordinator.swift index 8142e34a..c667b908 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Center /SubCoordinator/CenterSettingCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Center /SubCoordinator/CenterSettingCoordinator.swift @@ -52,6 +52,7 @@ class CenterSettingCoordinator: CenterSettingScreenCoordinatable { dependency: .init( userType: .center, settingUseCase: injector.resolve(SettingScreenUseCase.self), + inputValidationUseCase: injector.resolve(AuthInputValidationUseCase.self), navigationController: navigationController ) ) diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerSettingCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerSettingCoordinator.swift index 0ab3c00c..8d68e1c0 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerSettingCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerSettingCoordinator.swift @@ -50,8 +50,9 @@ class WorkerSettingCoordinaator: WorkerSettingScreenCoordinatable { public func startRemoveWorkerAccountFlow() { let coordinator = DeRegisterCoordinator( dependency: .init( - userType: .center, + userType: .worker, settingUseCase: injector.resolve(SettingScreenUseCase.self), + inputValidationUseCase: injector.resolve(AuthInputValidationUseCase.self), navigationController: navigationController ) ) diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Worker/WorkerMainCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Worker/WorkerMainCoordinator.swift index e78acdde..34d4670d 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Worker/WorkerMainCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Worker/WorkerMainCoordinator.swift @@ -87,7 +87,13 @@ class WorkerMainCoordinator: ParentCoordinator { ) ) case .setting: - fatalError() + coordinator = WorkerSettingCoordinaator( + dependency: .init( + parent: self, + injector: injector, + navigationController: navigationController + ) + ) } addChildCoordinator(coordinator) diff --git a/project/Projects/App/Sources/RootCoordinator/RootCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/RootCoordinator.swift index e2db0068..669e35ca 100644 --- a/project/Projects/App/Sources/RootCoordinator/RootCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/RootCoordinator.swift @@ -37,7 +37,7 @@ class RootCoordinator { navigationController.pushViewController(vc, animated: false) - centerMain() + workerMain() } func popViewController() { diff --git a/project/Projects/Data/ConcreteRepository/Auth/DefaultAuthRepository.swift b/project/Projects/Data/ConcreteRepository/Auth/DefaultAuthRepository.swift index d214d2fe..95b54dc4 100644 --- a/project/Projects/Data/ConcreteRepository/Auth/DefaultAuthRepository.swift +++ b/project/Projects/Data/ConcreteRepository/Auth/DefaultAuthRepository.swift @@ -12,7 +12,7 @@ import NetworkDataSource import Entity public class DefaultAuthRepository: AuthRepository { - + let networkService = AuthService() public init() { } @@ -94,6 +94,25 @@ public extension DefaultAuthRepository { return networkService.requestDecodable(api: .workerLogin(phoneNumber: phoneNumber, verificationNumber: authNumber), with: .plain) .flatMap { [unowned self] in saveTokenToStore(token: $0) } } + + func signoutWorkerAccount() -> RxSwift.Single { + networkService + .request(api: .signoutWorkerAccount, with: .withToken) + .map { _ in } + } + + func deregisterWorkerAccount(reasons: [Entity.DeregisterReasonVO]) -> RxSwift.Single { + let reasonString = reasons.map { $0.reasonText }.joined(separator: "|") + + return networkService + .request( + api: .deregisterWorkerAccount( + reason: reasonString + ), + with: .withToken + ) + .map { _ in } + } } // MARK: Token management diff --git a/project/Projects/Data/NetworkDataSource/API/AuthAPI.swift b/project/Projects/Data/NetworkDataSource/API/AuthAPI.swift index cdc430c7..f1ccfecd 100644 --- a/project/Projects/Data/NetworkDataSource/API/AuthAPI.swift +++ b/project/Projects/Data/NetworkDataSource/API/AuthAPI.swift @@ -27,6 +27,8 @@ public enum AuthAPI { // Worker case registerWorkerAccount(data: Data) case workerLogin(phoneNumber: String, verificationNumber: String) + case signoutWorkerAccount + case deregisterWorkerAccount(reason: String) } extension AuthAPI: BaseAPI { @@ -58,6 +60,10 @@ extension AuthAPI: BaseAPI { return .post case .workerLogin: return .post + case .signoutWorkerAccount: + return .post + case .deregisterWorkerAccount: + return .post } } @@ -91,6 +97,10 @@ extension AuthAPI: BaseAPI { "carer/join" case .workerLogin: "carer/login" + case .signoutWorkerAccount: + "carer/logout" + case .deregisterWorkerAccount: + "carer/withdraw" } } @@ -113,6 +123,8 @@ extension AuthAPI: BaseAPI { case .workerLogin(let phoneNumber, let verificationNumber): params["phoneNumber"] = phoneNumber params["verificationNumber"] = verificationNumber + case .deregisterWorkerAccount(let reason): + params["reason"] = reason default: break } @@ -148,6 +160,8 @@ extension AuthAPI: BaseAPI { return .requestData(data) case .workerLogin: return .requestParameters(parameters: bodyParameters ?? [:], encoding: parameterEncoding) + case .deregisterWorkerAccount: + return .requestParameters(parameters: bodyParameters ?? [:], encoding: parameterEncoding) default: return .requestPlain } diff --git a/project/Projects/Domain/ConcreteUseCase/Setting/DefaultSettingUseCase.swift b/project/Projects/Domain/ConcreteUseCase/Setting/DefaultSettingUseCase.swift index dcaa9115..01aea216 100644 --- a/project/Projects/Domain/ConcreteUseCase/Setting/DefaultSettingUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/Setting/DefaultSettingUseCase.swift @@ -91,4 +91,14 @@ public class DefaultSettingUseCase: SettingScreenUseCase { task: repository.signoutCenterAccount() ) } + + // 요양보호사 회원탈퇴 + public func deregisterWorkerAccount(reasons: [Entity.DeregisterReasonVO]) -> RxSwift.Single> { + convert(task: repository.deregisterWorkerAccount(reasons: reasons)) + } + + // 요양보호사 로그아웃 + public func signoutWorkerAccount() -> RxSwift.Single> { + convert(task: repository.signoutWorkerAccount()) + } } diff --git a/project/Projects/Domain/RepositoryInterface/Auth/Login/AuthRepository.swift b/project/Projects/Domain/RepositoryInterface/Auth/Login/AuthRepository.swift index cd37c419..30929ae1 100644 --- a/project/Projects/Domain/RepositoryInterface/Auth/Login/AuthRepository.swift +++ b/project/Projects/Domain/RepositoryInterface/Auth/Login/AuthRepository.swift @@ -19,4 +19,6 @@ public protocol AuthRepository: RepositoryBase { // MARK: Worker func requestRegisterWorkerAccount(registerState: WorkerRegisterState) -> Single func requestWorkerLogin(phoneNumber: String, authNumber: String) -> Single + func signoutWorkerAccount() -> Single + func deregisterWorkerAccount(reasons: [DeregisterReasonVO]) -> Single } diff --git a/project/Projects/Domain/UseCaseInterface/Setting/SettingScreenUseCase .swift b/project/Projects/Domain/UseCaseInterface/Setting/SettingScreenUseCase .swift index 6d3942ca..314c551b 100644 --- a/project/Projects/Domain/UseCaseInterface/Setting/SettingScreenUseCase .swift +++ b/project/Projects/Domain/UseCaseInterface/Setting/SettingScreenUseCase .swift @@ -29,6 +29,14 @@ public protocol SettingScreenUseCase: UseCaseBase { /// 어플리케이션 이용약관을 가져옵니다. func getApplicationPolicyUrl() -> URL + /// 요양보호사 회원 탈퇴 + func deregisterWorkerAccount( + reasons: [DeregisterReasonVO] + ) -> Single> + + /// 요양보호사 로그아웃 + func signoutWorkerAccount() -> Single> + /// 센터 회원 탈퇴 func deregisterCenterAccount( reasons: [DeregisterReasonVO], diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Navigation/IdleNavigationBar.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Navigation/IdleNavigationBar.swift index 73cdefee..2c7c2b94 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Navigation/IdleNavigationBar.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Navigation/IdleNavigationBar.swift @@ -38,7 +38,7 @@ public class IdleNavigationBar: UIView { public init( titleText: String = "", - innerViews: [UIView] + innerViews: [UIView] = [] ) { super.init(frame: .zero) 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 f199b1ed..99cbbcee 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 @@ -104,15 +104,12 @@ where return textField }() - private let authSuccessText: ResizableUILabel = { - - let label = ResizableUILabel() - label.font = DSKitFontFamily.Pretendard.medium.font(size: 12) - label.text = "인증이 완료되었습니다." + private let authSuccessText: IdleLabel = { + let label = IdleLabel(typography: .caption) + label.textString = "* 인증이 완료되었습니다." label.textAlignment = .left - label.textColor = DSKitAsset.Colors.gray300.color + label.attrTextColor = DSKitAsset.Colors.gray300.color label.isHidden = true - return label }() 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 5db247e6..dc5e1fc6 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 @@ -12,7 +12,7 @@ import PresentationCore import UseCaseInterface import Entity -enum AuthInOutStreamManager { } +public enum AuthInOutStreamManager { } extension AuthInOutStreamManager { 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 1655fcee..92c9bc1c 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 @@ -12,7 +12,7 @@ import PresentationCore import UseCaseInterface import Entity -extension AuthInOutStreamManager { +public extension AuthInOutStreamManager { static func validatePhoneNumberInOut( input: AuthPhoneNumberInputable & AnyObject, @@ -28,7 +28,7 @@ extension AuthInOutStreamManager { output.canSubmitPhoneNumber = input .editingPhoneNumber .map({ [unowned useCase] phoneNumber in - printIfDebug("[CenterRegisterViewModel] 전달받은 전화번호: \(phoneNumber)") + printIfDebug("전달받은 전화번호: \(phoneNumber)") return useCase.checkPhoneNumberIsValid(phoneNumber: phoneNumber) }) .asDriver(onErrorJustReturn: false) @@ -37,7 +37,7 @@ extension AuthInOutStreamManager { .editingAuthNumber .compactMap({ $0 }) .map { authNumber in - printIfDebug("[CenterRegisterViewModel] 전달받은 인증번호: \(authNumber)") + printIfDebug("전달받은 인증번호: \(authNumber)") return authNumber.count == 6 } @@ -45,7 +45,7 @@ extension AuthInOutStreamManager { let phoneNumberAuthRequestResult = input .requestAuthForPhoneNumber - .flatMap { [unowned useCase, input] _ in + .flatMap { [useCase, input] _ in let formatted = Self.formatPhoneNumber(phoneNumber: input.editingPhoneNumber.value) #if DEBUG diff --git a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/Setting/PasswordForDeregisterCoordinator.swift b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/Setting/PasswordForDeregisterCoordinator.swift index 537121cb..a235be57 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/Setting/PasswordForDeregisterCoordinator.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/Setting/PasswordForDeregisterCoordinator.swift @@ -61,8 +61,11 @@ public class PasswordForDeregisterCoordinator: ChildCoordinator { } public func popToRoot() { - - /// Root까지 네비게이션을 제거합니다. + parent?.popToRoot() + } + + public func cancelDeregister() { + parent?.cancelDeregister() } } diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/Setting/PasswordForDeregisterVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/Setting/PasswordForDeregisterVC.swift index d0ab375e..b54e9b16 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/Setting/PasswordForDeregisterVC.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/Setting/PasswordForDeregisterVC.swift @@ -162,10 +162,13 @@ public class PasswordForDeregisterVC: BaseViewController { .withLatestFrom(passwordField.eventPublisher) .bind(to: viewModel.deregisterButtonClicked) .disposed(by: disposeBag) + + cancelButton.rx.tap + .bind(to: viewModel.cancelButtonClicked) + .disposed(by: disposeBag) - navigationBar - .backButton.rx.tap - .bind(to: viewModel.exitButtonClicked) + navigationBar.backButton.rx.tap + .bind(to: viewModel.backButtonClicked) .disposed(by: disposeBag) // Output diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Setting/PasswordForDeregisterVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Setting/PasswordForDeregisterVM.swift index 531638c7..12f98c74 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Setting/PasswordForDeregisterVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Setting/PasswordForDeregisterVM.swift @@ -16,7 +16,8 @@ public class PasswordForDeregisterVM: DefaultAlertOutputable { public weak var coordinator: PasswordForDeregisterCoordinator? public let deregisterButtonClicked: PublishRelay = .init() - public let exitButtonClicked: PublishRelay = .init() + public let backButtonClicked: PublishRelay = .init() + public let cancelButtonClicked: PublishRelay = .init() public var alert: RxCocoa.Driver? let settingUseCase: SettingScreenUseCase @@ -33,10 +34,7 @@ public class PasswordForDeregisterVM: DefaultAlertOutputable { let deregisterResult = deregisterButtonClicked .flatMap { [settingUseCase] password in - settingUseCase.deregisterCenterAccount( - reasons: deregisterReasons, - password: password - ) + settingUseCase.deregisterWorkerAccount(reasons: deregisterReasons) } .share() @@ -54,13 +52,20 @@ public class PasswordForDeregisterVM: DefaultAlertOutputable { }) .disposed(by: disposeBag) - exitButtonClicked + backButtonClicked .observe(on: MainScheduler.instance) .subscribe(onNext: { [weak self] _ in self?.coordinator?.coordinatorDidFinish() }) .disposed(by: disposeBag) + cancelButtonClicked + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] _ in + self?.coordinator?.cancelDeregister() + }) + .disposed(by: disposeBag) + alert = deregisterFailure .map { error in DefaultAlertContentVO( diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/ViewModel/CenterDeregisterReasonsVM.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/CenterDeregisterReasonsVM.swift similarity index 100% rename from project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/ViewModel/CenterDeregisterReasonsVM.swift rename to project/Projects/Presentation/Feature/Root/Sources/Screen/Center/CenterDeregisterReasonsVM.swift diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/Coordinator/DeRegisterCoordinator.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/Coordinator/DeRegisterCoordinator.swift index cd97ef53..ad1a8a9c 100644 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/Coordinator/DeRegisterCoordinator.swift +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/Coordinator/DeRegisterCoordinator.swift @@ -16,11 +16,13 @@ public class DeRegisterCoordinator: DeregisterCoordinatable { public struct Dependency { let userType: UserType let settingUseCase: SettingScreenUseCase + let inputValidationUseCase: AuthInputValidationUseCase let navigationController: UINavigationController - public init(userType: UserType, settingUseCase: SettingScreenUseCase, navigationController: UINavigationController) { + public init(userType: UserType, settingUseCase: SettingScreenUseCase, inputValidationUseCase: AuthInputValidationUseCase, navigationController: UINavigationController) { self.userType = userType self.settingUseCase = settingUseCase + self.inputValidationUseCase = inputValidationUseCase self.navigationController = navigationController } } @@ -34,10 +36,12 @@ public class DeRegisterCoordinator: DeregisterCoordinatable { var viewControllerRef: UIViewController? let userType: UserType let settingUseCase: SettingScreenUseCase + let inputValidationUseCase: AuthInputValidationUseCase public init(dependency: Dependency) { self.userType = dependency.userType self.settingUseCase = dependency.settingUseCase + self.inputValidationUseCase = dependency.inputValidationUseCase self.navigationController = dependency.navigationController } @@ -73,11 +77,25 @@ public class DeRegisterCoordinator: DeregisterCoordinatable { } public func showFinalPhoneAuthScreen(reasons: [Entity.DeregisterReasonVO]) { - + let coordinator = PhoneNumberValidationForDeregisterCoordinator( + dependency: .init( + settingUseCase: settingUseCase, + inputValidationUseCase: inputValidationUseCase, + reasons: reasons, + navigationController: navigationController + ) + ) + addChildCoordinator(coordinator) + coordinator.parent = self + coordinator.start() } public func cancelDeregister() { clearChildren() parent?.removeChildCoordinator(self) } + + public func popToRoot() { + NotificationCenter.default.post(name: .popToInitialVC, object: nil) + } } diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/Coordinator/Sub/SelectReasonCoordinator.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/Coordinator/Sub/SelectReasonCoordinator.swift index 189f0d1f..b69db6c1 100644 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/Coordinator/Sub/SelectReasonCoordinator.swift +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/Coordinator/Sub/SelectReasonCoordinator.swift @@ -47,7 +47,7 @@ public class SelectReasonCoordinator: ChildCoordinator { case .center: vm = CenterDeregisterReasonsVM(coordinator: self) case .worker: - fatalError() + vm = WorkerDeregisterReasonsVM(coordinator: self) } let vc = DeregisterReasonVC() @@ -65,8 +65,8 @@ public class SelectReasonCoordinator: ChildCoordinator { parent?.showFinalPasswordScreen(reasons: reasons) } - public func showPhoneNumberAuthScreen() { - + public func showPhoneNumberAuthScreen(reasons: [DeregisterReasonVO]) { + parent?.showFinalPhoneAuthScreen(reasons: reasons) } } diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/PhoneNumberValidationForDeregisterCoordinator.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/PhoneNumberValidationForDeregisterCoordinator.swift new file mode 100644 index 00000000..72b5aaf3 --- /dev/null +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/PhoneNumberValidationForDeregisterCoordinator.swift @@ -0,0 +1,78 @@ +// +// PhoneNumberValidationForDeregisterCoordinator.swift +// RootFeature +// +// Created by choijunios on 8/25/24. +// + +import UIKit +import PresentationCore +import UseCaseInterface +import Entity + +class PhoneNumberValidationForDeregisterCoordinator: ChildCoordinator { + + struct Dependency { + let settingUseCase: SettingScreenUseCase + let inputValidationUseCase: AuthInputValidationUseCase + let reasons: [DeregisterReasonVO] + let navigationController: UINavigationController + + init(settingUseCase: SettingScreenUseCase, inputValidationUseCase: AuthInputValidationUseCase, reasons: [DeregisterReasonVO], navigationController: UINavigationController) { + self.settingUseCase = settingUseCase + self.inputValidationUseCase = inputValidationUseCase + self.reasons = reasons + self.navigationController = navigationController + } + } + + let settingUseCase: SettingScreenUseCase + let inputValidationUseCase: AuthInputValidationUseCase + let reasons: [DeregisterReasonVO] + + weak var viewControllerRef: UIViewController? + weak var parent: DeregisterCoordinatable? + + let navigationController: UINavigationController + + + init( + dependency: Dependency + ) { + self.navigationController = dependency.navigationController + self.settingUseCase = dependency.settingUseCase + self.inputValidationUseCase = dependency.inputValidationUseCase + self.reasons = dependency.reasons + } + + deinit { + printIfDebug("\(String(describing: PhoneNumberValidationForDeregisterCoordinator.self))") + } + + func start() { + let vc = PhoneNumberValidationForDeregisterVC() + let vm = PhoneNumberValidationForDeregisterVM( + coordinator: self, + deregisterReasons: reasons, + inputValidationUseCase: inputValidationUseCase, + settingUseCase: settingUseCase + ) + vc.bind(viewModel: vm) + viewControllerRef = vc + navigationController.pushViewController(vc, animated: true) + } + + func coordinatorDidFinish() { + popViewController() + parent?.removeChildCoordinator(self) + } + + func popToRoot() { + parent?.popToRoot() + } + + func cancelDeregister() { + parent?.cancelDeregister() + } +} + diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/PhoneNumberValidationForDeregisterVC.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/PhoneNumberValidationForDeregisterVC.swift new file mode 100644 index 00000000..e2b28593 --- /dev/null +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/PhoneNumberValidationForDeregisterVC.swift @@ -0,0 +1,309 @@ +// +// PhoneNumberValidationForDeregisterVC.swift +// RootFeature +// +// Created by choijunios on 8/25/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit +import AuthFeature + +public class PhoneNumberValidationForDeregisterVC: BaseViewController { + + var viewModel: PhoneNumberValidationForDeregisterVMable? + + // Init + + // View + let navigationBar: IdleNavigationBar = { + let bar = IdleNavigationBar() + bar.titleLabel.textString = "계정 삭제" + return bar + }() + let titleLabel: IdleLabel = { + let label = IdleLabel(typography: .Heading1) + label.textAlignment = .left + label.numberOfLines = 2 + label.textString = "마지막으로\n전화번호를 인증해주세요" + return label + }() + + // MARK: 전화번호 입력 + private let phoneNumberLabel: IdleLabel = { + let label = IdleLabel(typography: .Subtitle4) + label.textString = "전화번호" + label.attrTextColor = DSColor.gray500.color + label.textAlignment = .left + return label + }() + private let phoneNumberField: IFType1 = { + + let textField = IFType1( + placeHolderText: "전화번호를 입력해주세요.", + submitButtonText: "인증", + keyboardType: .numberPad + ) + textField.idleTextField.isCompleteImageAvailable = false + + return textField + }() + + // MARK: 인증번호 입력 + private let authNumberLabel: IdleLabel = { + let label = IdleLabel(typography: .Subtitle4) + label.textString = "인증번호" + label.attrTextColor = DSColor.gray500.color + label.textAlignment = .left + label.alpha = 0 + return label + }() + private let authNumberField: IFType1 = { + + let textField = IFType1( + placeHolderText: "", + submitButtonText: "확인", + keyboardType: .numberPad + ) + textField.idleTextField.isCompleteImageAvailable = false + textField.alpha = 0 + return textField + }() + private let authSuccessText: IdleLabel = { + let label = IdleLabel(typography: .caption) + label.textString = "* 인증이 완료되었습니다." + label.textAlignment = .left + label.attrTextColor = DSKitAsset.Colors.gray300.color + label.alpha = 0 + return label + }() + + + let finalWarningLabel: IdleLabel = { + let label = IdleLabel(typography: .caption) + label.textString = "탈퇴 버튼 선택 시 모든 정보가 삭제되며, 되돌릴 수 없습니다." + label.attrTextColor = DSColor.red100.color + return label + }() + + let cancelButton: IdleThirdinaryButton = { + let button = IdleThirdinaryButton(level: .medium) + button.label.textString = "취소하기" + return button + }() + + let acceptDeregisterButton: IdlePrimaryButton = { + let button = IdlePrimaryButton(level: .mediumRed) + button.label.textString = "탈퇴하기" + button.setEnabled(false) + return button + }() + + // Observable + private let disposeBag = DisposeBag() + + public init() { + super.init( + nibName: nil, + bundle: nil + ) + } + + public required init?( + coder: NSCoder + ) { + fatalError() + } + + public override func viewDidLoad() { + super.viewDidLoad() + setAppearance() + setLayout() + setObservable() + } + + private func setAppearance() { + view.backgroundColor = DSColor.gray0.color + } + + private func setLayout() { + + let textFieldStack = VStack( + [ + phoneNumberLabel, + phoneNumberField, + Spacer(height: 20), + authNumberLabel, + authNumberField + ], + spacing: 6, + alignment: .fill + ) + + let buttonStack = HStack( + [ + cancelButton, + acceptDeregisterButton + ], + spacing: 8, + alignment: .center, + distribution: .fillEqually + ) + + [ + navigationBar, + titleLabel, + textFieldStack, + authSuccessText, + finalWarningLabel, + buttonStack, + + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + view.addSubview($0) + } + + NSLayoutConstraint.activate([ + navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + navigationBar.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor), + navigationBar.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor), + + titleLabel.topAnchor.constraint(equalTo: navigationBar.bottomAnchor, constant: 24), + titleLabel.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 20), + + textFieldStack.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 36), + textFieldStack.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 20), + textFieldStack.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -20), + + authSuccessText.topAnchor.constraint(equalTo: textFieldStack.bottomAnchor, constant: 2), + authSuccessText.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 20), + + finalWarningLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + finalWarningLabel.bottomAnchor.constraint(equalTo: buttonStack.topAnchor, constant: -12), + + buttonStack.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 20), + buttonStack.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -20), + buttonStack.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -14), + ]) + } + + private func setObservable() { + + } + + public func bind(viewModel: PhoneNumberValidationForDeregisterVMable) { + self.viewModel = viewModel + + // Input + cancelButton.rx.tap + .bind(to: viewModel.cancelButtonClicked) + .disposed(by: disposeBag) + + navigationBar.backButton.rx.tap + .bind(to: viewModel.backButtonClicked) + .disposed(by: disposeBag) + + acceptDeregisterButton.rx.tap + .bind(to: viewModel.deregisterButtonClicked) + .disposed(by: disposeBag) + + // 현재 입력중인 정보 전송 + phoneNumberField.idleTextField.textField.rx.text + .compactMap { $0 } + .bind(to: viewModel.editingPhoneNumber) + .disposed(by: disposeBag) + + authNumberField.idleTextField.textField.rx.text + .compactMap { $0 } + .bind(to: viewModel.editingAuthNumber) + .disposed(by: disposeBag) + + // 인증, 확인 버튼이 눌린 경우 + phoneNumberField + .eventPublisher + .map { _ in () } + .bind(to: viewModel.requestAuthForPhoneNumber) + .disposed(by: disposeBag) + + authNumberField + .eventPublisher + .map { _ in () } + .bind(to: viewModel.requestValidationForAuthNumber) + .disposed(by: disposeBag) + + // Output + + // 입력중인 전화번호가 특정 조건(ex: 입력길이)을 만족한 경우 '인증'버튼 활성화 + viewModel + .canSubmitPhoneNumber? + .compactMap { $0 } + .asDriver(onErrorJustReturn: false) + .drive(onNext: { [weak self] in self?.phoneNumberField.button.setEnabled($0) }) + .disposed(by: disposeBag) + + // 입력중인 인증번호가 특정 조건(ex: 입력길이)을 만족한 경우 '확인'버튼 활성화 + viewModel + .canSubmitAuthNumber? + .compactMap { $0 } + .asDriver(onErrorJustReturn: false) + .drive(onNext: { [weak self] in self?.authNumberField.button.setEnabled($0) }) + .disposed(by: disposeBag) + + // 휴대전화 인증의 시작 + viewModel + .phoneNumberValidation? + .asObservable() + .take(1) + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + activateAuthNumberField() + }) + .disposed(by: disposeBag) + + // 인증번호 인증 성공여부 + viewModel + .authNumberValidation? + .asObservable() + .take(1) + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + acceptDeregisterButton.setEnabled(true) + + // 입력과 관려된 필드와 버튼 비활성화 + phoneNumberField.idleTextField.setEnabled(false) + phoneNumberField.button.setEnabled(false) + authNumberField.idleTextField.setEnabled(false) + authNumberField.button.setEnabled(false) + + // 타이머 비활성화 + authNumberField.idleTextField.removeTimer() + + // 인증 완료 텍스트 + authSuccessText.alpha = 1 + }) + .disposed(by: disposeBag) + + + // Alert + viewModel + .alert? + .drive(onNext: { [weak self] vo in + self?.showAlert(vo: vo) + }) + .disposed(by: disposeBag) + } + + func activateAuthNumberField() { + authNumberField.idleTextField.createTimer() + authNumberField.idleTextField.startTimer(minute: 5, seconds: 0) + authNumberField.alpha = 1 + authNumberLabel.alpha = 1 + } +} + + diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/PhoneNumberValidationForDeregisterVM.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/PhoneNumberValidationForDeregisterVM.swift new file mode 100644 index 00000000..2d9fe27a --- /dev/null +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/PhoneNumberValidationForDeregisterVM.swift @@ -0,0 +1,116 @@ +// +// PhoneNumberValidationForDeregisterVM.swift +// RootFeature +// +// Created by choijunios on 8/25/24. +// + +import Foundation +import RxCocoa +import RxSwift +import Entity +import AuthFeature +import UseCaseInterface + + +public protocol PhoneNumberValidationForDeregisterVMable: AuthPhoneNumberInputable & AuthPhoneNumberOutputable { + + var backButtonClicked: PublishRelay { get } + var cancelButtonClicked: PublishRelay { get } + var deregisterButtonClicked: PublishRelay { get } +} + +class PhoneNumberValidationForDeregisterVM: PhoneNumberValidationForDeregisterVMable { + + // Init + weak var coordinator: PhoneNumberValidationForDeregisterCoordinator? + let inputValidationUseCase: AuthInputValidationUseCase + let settingUseCase: SettingScreenUseCase + + // Input + var editingPhoneNumber: RxRelay.BehaviorRelay = .init(value: "") + var editingAuthNumber: RxRelay.BehaviorRelay = .init(value: "") + var requestAuthForPhoneNumber: RxRelay.PublishRelay = .init() + var requestValidationForAuthNumber: RxRelay.PublishRelay = .init() + var deregisterButtonClicked: RxRelay.PublishRelay = .init() + var backButtonClicked: RxRelay.PublishRelay = .init() + var cancelButtonClicked: RxRelay.PublishRelay = .init() + + // Output + var canSubmitPhoneNumber: RxCocoa.Driver? + var canSubmitAuthNumber: RxCocoa.Driver? + var phoneNumberValidation: RxCocoa.Driver? + var authNumberValidation: RxCocoa.Driver? + var alert: RxCocoa.Driver? + + let disposeBag = DisposeBag() + + init( + coordinator: PhoneNumberValidationForDeregisterCoordinator?, + deregisterReasons: [DeregisterReasonVO], + inputValidationUseCase: AuthInputValidationUseCase, + settingUseCase: SettingScreenUseCase + ) + { + self.coordinator = coordinator + self.inputValidationUseCase = inputValidationUseCase + self.settingUseCase = settingUseCase + + // MARK: 번호인증 로직 + AuthInOutStreamManager.validatePhoneNumberInOut( + input: self, + output: self, + useCase: inputValidationUseCase) { _ in } + + let deregisterResult = deregisterButtonClicked + .flatMap { [settingUseCase] _ in + settingUseCase + .deregisterWorkerAccount(reasons: deregisterReasons) + } + .share() + + let deregisterSuccess = deregisterResult.compactMap { $0.value } + let deregisterFailure = deregisterResult.compactMap { $0.error } + + deregisterSuccess + .observe(on: MainScheduler.asyncInstance) + .subscribe(onNext: { [weak self] _ in + + // ‼️ ‼️ 로컬에 저장된 계정 정보 삭제 ‼️ ‼️ + + // RootCoordinator로 이동 + self?.coordinator?.popToRoot() + }) + .disposed(by: disposeBag) + + if let alert { + + self.alert = Observable + .merge( + alert.asObservable(), + deregisterFailure + .map { + DefaultAlertContentVO( + title: "회원탈퇴 실패", + message: $0.message + ) + } + ) + .asDriver(onErrorJustReturn: .default) + } + + backButtonClicked + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] _ in + self?.coordinator?.coordinatorDidFinish() + }) + .disposed(by: disposeBag) + + cancelButtonClicked + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] _ in + self?.coordinator?.cancelDeregister() + }) + .disposed(by: disposeBag) + } +} diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/WorkerDeregisterReasonsVM.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/WorkerDeregisterReasonsVM.swift new file mode 100644 index 00000000..381bbf70 --- /dev/null +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/WorkerDeregisterReasonsVM.swift @@ -0,0 +1,35 @@ +// +// WorkerDeregisterReasonsVM.swift +// RootFeature +// +// Created by choijunios on 8/25/24. +// + +import Entity +import RxSwift +import RxCocoa + +public class WorkerDeregisterReasonsVM: DeregisterReasonVMable { + + public weak var coordinator: SelectReasonCoordinator? + public var exitButonClicked: RxRelay.PublishRelay = .init() + public var acceptDeregisterButonClicked: PublishRelay<[DeregisterReasonVO]> = .init() + + let disposeBag = DisposeBag() + + public init(coordinator: SelectReasonCoordinator) { + self.coordinator = coordinator + + acceptDeregisterButonClicked + .subscribe(onNext: { [weak self] reasons in + self?.coordinator?.showPhoneNumberAuthScreen(reasons: reasons) + }) + .disposed(by: disposeBag) + + exitButonClicked + .subscribe(onNext: { [weak self] reasons in + self?.coordinator?.coordinatorDidFinish() + }) + .disposed(by: disposeBag) + } +} diff --git a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Seting/WorkerSettingVM.swift b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Seting/WorkerSettingVM.swift index 334d10b2..a597dc27 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Seting/WorkerSettingVM.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Seting/WorkerSettingVM.swift @@ -113,7 +113,7 @@ public class WorkerSettingVM: WorkerSettingVMable { // MARK: 로그아웃 let signOutRequestResult = signOutButtonComfirmed.flatMap({ [settingUseCase] _ in - settingUseCase.signoutCenterAccount() + settingUseCase.signoutWorkerAccount() }) .share() diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/Deregister/DeregisterCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/Deregister/DeregisterCoordinatable.swift index 08dc10a2..3a5b84df 100644 --- a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/Deregister/DeregisterCoordinatable.swift +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/Deregister/DeregisterCoordinatable.swift @@ -21,4 +21,7 @@ public protocol DeregisterCoordinatable: ParentCoordinator { /// 요양보호사: 마지막으로 전화번호를 입력합니다. func showFinalPhoneAuthScreen(reasons: [DeregisterReasonVO]) + + /// 최초화면으로 돌아갑니다. + func popToRoot() } From fecd737e83f9afa75b47d99546f9ee5242027c5f Mon Sep 17 00:00:00 2001 From: J0onYEong Date: Sun, 25 Aug 2024 18:57:32 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[IDLE-000]=20Fix,=20=EC=98=88=EC=8B=9C?= =?UTF-8?q?=EC=95=B1=20=EC=BD=94=EB=93=9C=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Center/ExampleApp/Sources/SceneDelegate.swift | 3 +-- .../Root/ExampleApp/Sources/SceneDelegate.swift | 10 ---------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift index 3a572918..a90b3163 100644 --- a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift @@ -50,11 +50,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let vc = CenterSettingVC() let vm = CenterSettingVM( coordinator: nil, - settingUseCase: DefaultSettingUseCase(), + settingUseCase: DefaultSettingUseCase(repository: DefaultAuthRepository()), centerProfileUseCase: DefaultCenterProfileUseCase( repository: DefaultUserProfileRepository() ) - ) vc.bind(viewModel: vm) diff --git a/project/Projects/Presentation/Feature/Root/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Root/ExampleApp/Sources/SceneDelegate.swift index 7349c2fa..34f74e35 100644 --- a/project/Projects/Presentation/Feature/Root/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Root/ExampleApp/Sources/SceneDelegate.swift @@ -31,16 +31,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let nav = UINavigationController() nav.setNavigationBarHidden(true, animated: false) - self.coordinator = DeRegisterCoordinator( - dependency: .init( - userType: .center, - authUseCase: DefaultAuthUseCase( - repository: DefaultAuthRepository() - ), - navigationController: nav - ) - ) - window = UIWindow(windowScene: windowScene) window?.rootViewController = nav window?.makeKeyAndVisible()