diff --git a/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift b/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift index a75b3866..72a4cc75 100644 --- a/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift +++ b/project/Projects/App/Sources/DI/Assembly/DomainAssembly.swift @@ -45,5 +45,10 @@ public struct DomainAssembly: Assembly { return DefaultWorkerProfileUseCase(repository: repository) } + container.register(SettingScreenUseCase.self) { resolver in + let repository = resolver.resolve(AuthRepository.self)! + + return DefaultSettingUseCase(repository: repository) + } } } diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterMainCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterMainCoordinator.swift index ec28832f..34a1061e 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterMainCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Center /CenterMainCoordinator.swift @@ -8,6 +8,7 @@ import UIKit import DSKit import PresentationCore +import CenterFeature import RootFeature import UseCaseInterface @@ -74,15 +75,18 @@ class CenterMainCoordinator: CenterMainCoordinatable { coordinator = RecruitmentManagementCoordinator( dependency: .init( parent: self, - navigationController: navigationController, - workerProfileUseCase: injector.resolve(WorkerProfileUseCase.self), - recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) + injector: injector, + navigationController: navigationController ) ) case .setting: coordinator = CenterSettingCoordinator( - navigationController: navigationController + dependency: .init( + parent: self, + injector: injector, + navigationController: navigationController + ) ) } addChildCoordinator(coordinator) diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/RegisterPostCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/RegisterPostCoordinator.swift deleted file mode 100644 index 71de734b..00000000 --- a/project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/RegisterPostCoordinator.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// RegisterRecruitmentPostCoordinator.swift -// Idle-iOS -// -// Created by choijunios on 8/5/24. -// - -import UIKit -import DSKit -import PresentationCore -import CenterFeature -import UseCaseInterface diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/CenterProfileRegisterCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Center /SubCoordinator/CenterProfileRegisterCoordinator.swift similarity index 99% rename from project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/CenterProfileRegisterCoordinator.swift rename to project/Projects/App/Sources/RootCoordinator/Main/Center /SubCoordinator/CenterProfileRegisterCoordinator.swift index d10f28d4..d499aeab 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Center /OtherCoordinator/CenterProfileRegisterCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Center /SubCoordinator/CenterProfileRegisterCoordinator.swift @@ -45,7 +45,6 @@ class CenterProfileRegisterCoordinator: CenterProfileRegisterCoordinatable { public func registerFinished() { clearChildren() - parent?.removeChildCoordinator(self) } } diff --git a/project/Projects/App/Sources/RootCoordinator/Main/Center /SubCoordinator/CenterSettingCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Center /SubCoordinator/CenterSettingCoordinator.swift new file mode 100644 index 00000000..8142e34a --- /dev/null +++ b/project/Projects/App/Sources/RootCoordinator/Main/Center /SubCoordinator/CenterSettingCoordinator.swift @@ -0,0 +1,62 @@ +// +// CenterSettingCoordinator.swift +// Idle-iOS +// +// Created by choijunios on 8/25/24. +// + +import UIKit +import CenterFeature +import RootFeature +import PresentationCore +import UseCaseInterface + +class CenterSettingCoordinator: CenterSettingScreenCoordinatable { + + struct Dependency { + let parent: CenterMainCoordinatable + let injector: Injector + let navigationController: UINavigationController + } + + var childCoordinators: [any PresentationCore.Coordinator] = [] + + weak var parent: CenterMainCoordinatable? + + 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 = CenterSettingScreenCoordinator( + dependency: .init( + navigationController: navigationController, + settingUseCase: injector.resolve(SettingScreenUseCase.self), + centerProfileUseCase: injector.resolve(CenterProfileUseCase.self) + ) + ) + addChildCoordinator(coordinator) + coordinator.parent = self + coordinator.start() + } + + public func startRemoveCenterAccountFlow() { + 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/Root/Sources/Screen/Center/Coordinator/RecruitmentPost/RecruitmentManagementCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Center /SubCoordinator/RecruitmentManagementCoordinator.swift similarity index 66% rename from project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RecruitmentPost/RecruitmentManagementCoordinator.swift rename to project/Projects/App/Sources/RootCoordinator/Main/Center /SubCoordinator/RecruitmentManagementCoordinator.swift index 90bde115..5d41b4c0 100644 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RecruitmentPost/RecruitmentManagementCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Center /SubCoordinator/RecruitmentManagementCoordinator.swift @@ -1,11 +1,12 @@ // // RecruitmentManagementCoordinator.swift -// RootFeature +// Idle-iOS // // Created by choijunios on 7/25/24. // import UIKit +import RootFeature import CenterFeature import PresentationCore import UseCaseInterface @@ -15,21 +16,14 @@ import Entity public class RecruitmentManagementCoordinator: RecruitmentManagementCoordinatable { public struct Dependency { - weak var parent: CenterMainCoordinatable? + let parent: CenterMainCoordinatable + let injector: Injector let navigationController: UINavigationController - let workerProfileUseCase: WorkerProfileUseCase - let recruitmentPostUseCase: RecruitmentPostUseCase - public init( - parent: CenterMainCoordinatable? = nil, - navigationController: UINavigationController, - workerProfileUseCase: WorkerProfileUseCase, - recruitmentPostUseCase: RecruitmentPostUseCase - ) { + init(parent: CenterMainCoordinatable, injector: Injector, navigationController: UINavigationController) { self.parent = parent + self.injector = injector self.navigationController = navigationController - self.workerProfileUseCase = workerProfileUseCase - self.recruitmentPostUseCase = recruitmentPostUseCase } } @@ -37,28 +31,25 @@ public class RecruitmentManagementCoordinator: RecruitmentManagementCoordinatabl public weak var viewControllerRef: UIViewController? - public var navigationController: UINavigationController - public weak var parent: CenterMainCoordinatable? - - let workerProfileUseCase: WorkerProfileUseCase - let recruitmentPostUseCase: RecruitmentPostUseCase + let injector: Injector + public var navigationController: UINavigationController public init( dependency: Dependency ) { self.parent = dependency.parent + self.injector = dependency.injector self.navigationController = dependency.navigationController - self.workerProfileUseCase = dependency.workerProfileUseCase - self.recruitmentPostUseCase = dependency.recruitmentPostUseCase } public func start() { - let vc = CenterRecruitmentPostBoardVC() - let vm = CenterRecruitmentPostBoardVM(coordinator: self) - vc.bind(viewModel: vm) - viewControllerRef = vc - navigationController.pushViewController(vc, animated: false) + let coordinator = CenterRecruitmentPostBoardScreenCoordinator( + navigationController: navigationController + ) + addChildCoordinator(coordinator) + coordinator.parent = self + coordinator.start() } public func coordinatorDidFinish() { @@ -74,7 +65,7 @@ public extension RecruitmentManagementCoordinator { dependency: .init( navigationController: navigationController, centerEmployCardVO: .mock, - workerProfileUseCase: workerProfileUseCase + workerProfileUseCase: injector.resolve(WorkerProfileUseCase.self) ) ) addChildCoordinator(coordinator) @@ -89,7 +80,7 @@ public extension RecruitmentManagementCoordinator { postId: postId, applicantCount: applicantCount, navigationController: navigationController, - recruitmentPostUseCase: recruitmentPostUseCase + recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) ) ) addChildCoordinator(coordinator) @@ -101,7 +92,7 @@ public extension RecruitmentManagementCoordinator { let vm = EditPostVM( id: postId, - recruitmentPostUseCase: recruitmentPostUseCase + recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) ) let coordinator = EditPostCoordinator( dependency: .init( diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/AppliedAndLiked/AppliedAndLikedBoardCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/AppliedAndLikedBoardCoordinator.swift similarity index 60% rename from project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/AppliedAndLiked/AppliedAndLikedBoardCoordinator.swift rename to project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/AppliedAndLikedBoardCoordinator.swift index 5f6c552f..92457d08 100644 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/AppliedAndLiked/AppliedAndLikedBoardCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/AppliedAndLikedBoardCoordinator.swift @@ -12,44 +12,41 @@ import CenterFeature import PresentationCore import UseCaseInterface -public class AppliedAndLikedBoardCoordinator: WorkerRecruitmentBoardCoordinatable { +class AppliedAndLikedBoardCoordinator: WorkerRecruitmentBoardCoordinatable { - public struct Dependency { + struct Dependency { + let parent: WorkerMainCoordinator + let injector: Injector let navigationController: UINavigationController - let centerProfileUseCase: CenterProfileUseCase - let recruitmentPostUseCase: RecruitmentPostUseCase - public init(navigationController: UINavigationController, centerProfileUseCase: CenterProfileUseCase, recruitmentPostUseCase: RecruitmentPostUseCase) { + init(parent: WorkerMainCoordinator, injector: Injector, navigationController: UINavigationController) { + self.parent = parent + self.injector = injector self.navigationController = navigationController - self.centerProfileUseCase = centerProfileUseCase - self.recruitmentPostUseCase = recruitmentPostUseCase } } - public var childCoordinators: [any PresentationCore.Coordinator] = [] + var childCoordinators: [any PresentationCore.Coordinator] = [] - public weak var viewControllerRef: UIViewController? - - public var navigationController: UINavigationController + weak var viewControllerRef: UIViewController? + var navigationController: UINavigationController weak var parent: ParentCoordinator? - - let centerProfileUseCase: CenterProfileUseCase - let recruitmentPostUseCase: RecruitmentPostUseCase + let injector: Injector public init(depedency: Dependency) { + self.parent = depedency.parent self.navigationController = depedency.navigationController - self.centerProfileUseCase = depedency.centerProfileUseCase - self.recruitmentPostUseCase = depedency.recruitmentPostUseCase + self.injector = depedency.injector } public func start() { let vc = StarredAndAppliedVC() let appliedVM = AppliedPostBoardVM( - recruitmentPostUseCase: recruitmentPostUseCase + recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) ) let starredVM = StarredPostBoardVM( - recruitmentPostUseCase: recruitmentPostUseCase + recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) ) vc.bind( appliedPostVM: appliedVM, @@ -72,7 +69,7 @@ extension AppliedAndLikedBoardCoordinator { postId: postId, parent: self, navigationController: navigationController, - recruitmentPostUseCase: recruitmentPostUseCase + recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) ) ) addChildCoordinator(coodinator) @@ -82,7 +79,7 @@ extension AppliedAndLikedBoardCoordinator { let coordinator = CenterProfileCoordinator( dependency: .init( mode: .otherProfile(id: centerId), - profileUseCase: centerProfileUseCase, + profileUseCase: injector.resolve(CenterProfileUseCase.self), navigationController: navigationController ) ) diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/MainBoard/WorkerRecruitmentBoardCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerRecruitmentBoardCoordinator.swift similarity index 53% rename from project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/MainBoard/WorkerRecruitmentBoardCoordinator.swift rename to project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerRecruitmentBoardCoordinator.swift index 623d9d61..2239c985 100644 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/MainBoard/WorkerRecruitmentBoardCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Worker/SubCoordinator/WorkerRecruitmentBoardCoordinator.swift @@ -1,6 +1,6 @@ // // WorkerRecruitmentBoardCoordinator.swift -// RootFeature +// Idle-iOS // // Created by choijunios on 7/25/24. // @@ -12,52 +12,44 @@ import CenterFeature import PresentationCore import UseCaseInterface -public class WorkerRecruitmentBoardCoordinator: WorkerRecruitmentBoardCoordinatable { +class WorkerRecruitmentBoardCoordinator: WorkerRecruitmentBoardCoordinatable { - public struct Dependency { + struct Dependency { + let parent: WorkerMainCoordinator + let injector: Injector let navigationController: UINavigationController - let centerProfileUseCase: CenterProfileUseCase - let recruitmentPostUseCase: RecruitmentPostUseCase - public init(navigationController: UINavigationController, centerProfileUseCase: CenterProfileUseCase, recruitmentPostUseCase: RecruitmentPostUseCase) { + init(parent: WorkerMainCoordinator, injector: Injector, navigationController: UINavigationController) { + self.parent = parent + self.injector = injector self.navigationController = navigationController - self.centerProfileUseCase = centerProfileUseCase - self.recruitmentPostUseCase = recruitmentPostUseCase } } - public var childCoordinators: [any PresentationCore.Coordinator] = [] + var childCoordinators: [any PresentationCore.Coordinator] = [] - public weak var viewControllerRef: UIViewController? - - public var navigationController: UINavigationController + weak var viewControllerRef: UIViewController? + var navigationController: UINavigationController weak var parent: ParentCoordinator? + let injector: Injector - let centerProfileUseCase: CenterProfileUseCase - let recruitmentPostUseCase: RecruitmentPostUseCase - - public init(depedency: Dependency) { + init(depedency: Dependency) { self.navigationController = depedency.navigationController - self.centerProfileUseCase = depedency.centerProfileUseCase - self.recruitmentPostUseCase = depedency.recruitmentPostUseCase + self.parent = depedency.parent + self.injector = depedency.injector } - public func start() { + func start() { let vc = WorkerRecruitmentPostBoardVC() let vm = WorkerRecruitmentPostBoardVM( coordinator: self, - recruitmentPostUseCase: recruitmentPostUseCase + recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) ) vc.bind(viewModel: vm) viewControllerRef = vc navigationController.pushViewController(vc, animated: false) } - - public func coordinatorDidFinish() { - popViewController() - parent?.removeChildCoordinator(self) - } } extension WorkerRecruitmentBoardCoordinator { @@ -67,7 +59,7 @@ extension WorkerRecruitmentBoardCoordinator { postId: postId, parent: self, navigationController: navigationController, - recruitmentPostUseCase: recruitmentPostUseCase + recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) ) ) addChildCoordinator(coodinator) @@ -77,7 +69,7 @@ extension WorkerRecruitmentBoardCoordinator { let coordinator = CenterProfileCoordinator( dependency: .init( mode: .otherProfile(id: centerId), - profileUseCase: centerProfileUseCase, + profileUseCase: injector.resolve(CenterProfileUseCase.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 d2f7e0c3..e78acdde 100644 --- a/project/Projects/App/Sources/RootCoordinator/Main/Worker/WorkerMainCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/Main/Worker/WorkerMainCoordinator.swift @@ -73,23 +73,21 @@ class WorkerMainCoordinator: ParentCoordinator { case .home: coordinator = WorkerRecruitmentBoardCoordinator( depedency: .init( - navigationController: navigationController, - centerProfileUseCase: injector.resolve(CenterProfileUseCase.self), - recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) + parent: self, + injector: injector, + navigationController: navigationController ) ) case .preferredPost: coordinator = AppliedAndLikedBoardCoordinator( depedency: .init( - navigationController: navigationController, - centerProfileUseCase: injector.resolve(CenterProfileUseCase.self), - recruitmentPostUseCase: injector.resolve(RecruitmentPostUseCase.self) + parent: self, + injector: injector, + navigationController: navigationController ) ) case .setting: - coordinator = WorkerSettingCoordinator( - navigationController: navigationController - ) + fatalError() } addChildCoordinator(coordinator) diff --git a/project/Projects/App/Sources/RootCoordinator/RootCoordinator+Extension.swift b/project/Projects/App/Sources/RootCoordinator/RootCoordinator+Extension.swift index 3e032225..780c009d 100644 --- a/project/Projects/App/Sources/RootCoordinator/RootCoordinator+Extension.swift +++ b/project/Projects/App/Sources/RootCoordinator/RootCoordinator+Extension.swift @@ -7,8 +7,9 @@ import Foundation import AuthFeature +import PresentationCore -extension RootCoordinator { +extension RootCoordinator: RootCoorinatable { /// 로그인및 회원가입을 실행합니다. func auth() { @@ -59,4 +60,9 @@ extension RootCoordinator { coordinator.start() } + /// 루트 VC까지 모든 네비게이션을 제거합니다, 이후 코디네이터도 제거합니다. + func popToRoot() { + navigationController.popToRootViewController(animated: true) + childCoordinators.removeAll() + } } diff --git a/project/Projects/App/Sources/RootCoordinator/RootCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/RootCoordinator.swift index e0e9da8e..e2db0068 100644 --- a/project/Projects/App/Sources/RootCoordinator/RootCoordinator.swift +++ b/project/Projects/App/Sources/RootCoordinator/RootCoordinator.swift @@ -7,8 +7,9 @@ import UIKit import PresentationCore +import RootFeature -class RootCoordinator: ParentCoordinator { +class RootCoordinator { struct Dependency { let navigationController: UINavigationController @@ -28,6 +29,14 @@ class RootCoordinator: ParentCoordinator { func start() { navigationController.setNavigationBarHidden(true, animated: false) + // Root VC + let vc = InitialScreenVC() + let vm = InitialScreenVM(coordinator: self) + + vc.bind(viewModel: vm) + + navigationController.pushViewController(vc, animated: false) + centerMain() } diff --git a/project/Projects/Data/ConcreteRepository/Auth/DefaultAuthRepository.swift b/project/Projects/Data/ConcreteRepository/Auth/DefaultAuthRepository.swift index a91ee8c2..d214d2fe 100644 --- a/project/Projects/Data/ConcreteRepository/Auth/DefaultAuthRepository.swift +++ b/project/Projects/Data/ConcreteRepository/Auth/DefaultAuthRepository.swift @@ -47,6 +47,27 @@ public extension DefaultAuthRepository { return networkService.requestDecodable(api: .centerLogin(id: id, password: password), with: .plain) .flatMap { [unowned self] in saveTokenToStore(token: $0) } } + + func signoutCenterAccount() -> RxSwift.Single { + networkService + .request(api: .signoutCenterAccount, with: .withToken) + .map { _ in } + } + + func deregisterCenterAccount(reasons: [DeregisterReasonVO], password: String) -> RxSwift.Single { + + let reasonString = reasons.map { $0.reasonText }.joined(separator: "|") + + return networkService + .request( + api: .deregisterCenterAccount( + reason: reasonString, + password: password + ), + with: .withToken + ) + .map { _ in } + } } // MARK: Worker auth diff --git a/project/Projects/Data/NetworkDataSource/API/AuthAPI.swift b/project/Projects/Data/NetworkDataSource/API/AuthAPI.swift index cf891f1b..cdc430c7 100644 --- a/project/Projects/Data/NetworkDataSource/API/AuthAPI.swift +++ b/project/Projects/Data/NetworkDataSource/API/AuthAPI.swift @@ -21,6 +21,8 @@ public enum AuthAPI { case registerCenterAccount(data: Data) case centerLogin(id: String, password: String) case reissueToken(refreshToken: String) + case deregisterCenterAccount(reason: String, password: String) + case signoutCenterAccount // Worker case registerWorkerAccount(data: Data) @@ -46,6 +48,10 @@ extension AuthAPI: BaseAPI { return .post case .centerLogin: return .post + case .signoutCenterAccount: + return .post + case .deregisterCenterAccount: + return .post case .reissueToken: return .post case .registerWorkerAccount: @@ -61,20 +67,30 @@ extension AuthAPI: BaseAPI { "common/send" case .checkAuthNumber: "common/confirm" + case .reissueToken: + "common/refresh" + + case .authenticateBusinessNumber(let businessNumber): "center/authentication/\(businessNumber)" case .checkIdDuplication(id: let id): "center/validation/\(id)" + + case .registerCenterAccount: "center/join" case .centerLogin: "center/login" - case .reissueToken: - "center/refresh" + case .signoutCenterAccount: + "center/logout" + case .deregisterCenterAccount: + "center/withdraw" + + case .registerWorkerAccount: - "auth/carer/join" + "carer/join" case .workerLogin: - "auth/carer/login" + "carer/login" } } @@ -89,6 +105,9 @@ extension AuthAPI: BaseAPI { case .centerLogin(let id, let password): params["identifier"] = id params["password"] = password + case .deregisterCenterAccount(let reason, let password): + params["reason"] = reason + params["password"] = password case .reissueToken(let refreshToken): params["refreshToken"] = refreshToken case .workerLogin(let phoneNumber, let verificationNumber): @@ -107,6 +126,10 @@ extension AuthAPI: BaseAPI { } } + public var validationType: ValidationType { + .successCodes + } + public var task: Task { switch self { case .startPhoneNumberAuth: @@ -117,6 +140,8 @@ extension AuthAPI: BaseAPI { return .requestData(data) case .centerLogin: return .requestParameters(parameters: bodyParameters ?? [:], encoding: parameterEncoding) + case .deregisterCenterAccount: + return .requestParameters(parameters: bodyParameters ?? [:], encoding: parameterEncoding) case .reissueToken: return .requestParameters(parameters: bodyParameters ?? [:], encoding: parameterEncoding) case .registerWorkerAccount(let data): diff --git a/project/Projects/Data/NetworkDataSource/Service/BaseNetworkService.swift b/project/Projects/Data/NetworkDataSource/Service/BaseNetworkService.swift index d9d0f9b6..4a863302 100644 --- a/project/Projects/Data/NetworkDataSource/Service/BaseNetworkService.swift +++ b/project/Projects/Data/NetworkDataSource/Service/BaseNetworkService.swift @@ -96,9 +96,11 @@ public class BaseNetworkService { if let httpResponse = request.response { - if httpResponse.statusCode == 401 { + if httpResponse.statusCode == 401, request.retryCount < 1 { - guard let self else { fatalError() } + guard let self else { + return completion(.doNotRetry) + } guard let refreshToken = self.keyValueStore.getAuthToken()?.refreshToken else { completion(.doNotRetry) @@ -114,6 +116,11 @@ public class BaseNetworkService { provider.rx .request(.reissueToken(refreshToken: refreshToken)) + .catch({ error in + // 토큰 리프래쉬 실패 -> 재로그인 필요 + completion(.doNotRetryWithError(error)) + return .error(error) + }) .subscribe { [weak self] response in guard let self else { fatalError() } @@ -141,7 +148,7 @@ public class BaseNetworkService { .disposed(by: self.disposeBag) } else { - completion(.doNotRetry) + completion(.doNotRetryWithError(error)) } } } diff --git a/project/Projects/Data/NetworkDataSource/Service/JWTError.swift b/project/Projects/Data/NetworkDataSource/Service/JWTError.swift new file mode 100644 index 00000000..abe74c41 --- /dev/null +++ b/project/Projects/Data/NetworkDataSource/Service/JWTError.swift @@ -0,0 +1,31 @@ +// +// JWTError.swift +// NetworkDataSource +// +// Created by choijunios on 8/21/24. +// + +import Foundation + +enum JWTError: String, Error { + case tokenDecodeException = "JWT-001" + case tokenNotValid = "JWT-002" + case tokenExpiredException = "JWT-003" + case tokenNotFound = "JWT-004" + case notSupportUserTokenType = "JWT-005" + + var message: String { + switch self { + case .tokenDecodeException: + return "유효하지 않은 토큰, 토큰을 디코딩할 때, JWT의 형식에 맞지 않는 경우 발생합니다." + case .tokenNotValid: + return "유효하지 않은 토큰, 토큰 내 값 검증에 실패한 경우 발생합니다. (ex. 알고리즘, 서명)" + case .tokenExpiredException: + return "토큰이 만료된 경우 발생합니다. 재 로그인이 필요합니다." + case .tokenNotFound: + return "토큰을 찾을 수 없는 경우 발생합니다." + case .notSupportUserTokenType: + return "지원하지 않는 유저 토큰 타입을 사용한 경우 발생할 수 있습니다. (carer, center 제외)" + } + } +} diff --git a/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthInputValidationUseCase.swift b/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthInputValidationUseCase.swift index 9603fb55..7998fb36 100644 --- a/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthInputValidationUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthInputValidationUseCase.swift @@ -26,7 +26,7 @@ public class DefaultAuthInputValidationUseCase: AuthInputValidationUseCase { // MARK: 전화번호 인증 - public func requestPhoneNumberAuthentication(phoneNumber: String) -> Single> { + public func requestPhoneNumberAuthentication(phoneNumber: String) -> Single> { convert(task: self.repository .requestPhoneNumberAuthentication(phoneNumber: phoneNumber) .map { _ in phoneNumber } @@ -40,7 +40,7 @@ public class DefaultAuthInputValidationUseCase: AuthInputValidationUseCase { return predicate.evaluate(with: phoneNumber) } - public func authenticateAuthNumber(phoneNumber: String, authNumber: String) -> Single> { + public func authenticateAuthNumber(phoneNumber: String, authNumber: String) -> Single> { convert(task: repository .authenticateAuthNumber(phoneNumber: phoneNumber, authNumber: authNumber) .map({ _ in phoneNumber }) @@ -48,7 +48,7 @@ public class DefaultAuthInputValidationUseCase: AuthInputValidationUseCase { } // MARK: 사업자 번호 인증 - public func requestBusinessNumberAuthentication(businessNumber: String) -> Single> { + public func requestBusinessNumberAuthentication(businessNumber: String) -> Single> { convert(task: repository .requestBusinessNumberAuthentication(businessNumber: businessNumber) .map({ vo in (businessNumber, vo) }) @@ -70,7 +70,7 @@ public class DefaultAuthInputValidationUseCase: AuthInputValidationUseCase { return predicate.evaluate(with: id) } - public func requestCheckingIdDuplication(id: String) -> Single> { + public func requestCheckingIdDuplication(id: String) -> Single> { convert(task: repository .requestCheckingIdDuplication(id: id) .map({ _ in id }) diff --git a/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthUseCase.swift b/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthUseCase.swift index 65c7f854..cbb6ca45 100644 --- a/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/Auth/DefaultAuthUseCase.swift @@ -12,15 +12,15 @@ import RxSwift import Entity public class DefaultAuthUseCase: AuthUseCase { - + let repository: AuthRepository public init(repository: AuthRepository) { self.repository = repository } - /// 센터 회원가입 실행 - public func registerCenterAccount(registerState: CenterRegisterState) -> Single> { + // 센터 회원가입 실행 + public func registerCenterAccount(registerState: CenterRegisterState) -> Single> { convert( task: repository.requestRegisterCenterAccount( managerName: registerState.name, @@ -31,19 +31,19 @@ public class DefaultAuthUseCase: AuthUseCase { )) } - /// 센터 로그인 실행 - public func loginCenterAccount(id: String, password: String) -> Single> { + // 센터 로그인 실행 + public func loginCenterAccount(id: String, password: String) -> Single> { convert(task: repository.requestCenterLogin(id: id, password: password)) } - /// 요양 보호사 회원가입 실행 - public func registerWorkerAccount(registerState: WorkerRegisterState) -> Single> { + // 요양 보호사 회원가입 실행 + public func registerWorkerAccount(registerState: WorkerRegisterState) -> Single> { convert( task: repository.requestRegisterWorkerAccount(registerState: registerState)) } - /// 요양 보호사 로그인 실행 - public func loginWorkerAccount(phoneNumber: String, authNumber: String) -> Single> { + // 요양 보호사 로그인 실행 + public func loginWorkerAccount(phoneNumber: String, authNumber: String) -> Single> { convert( task: repository.requestWorkerLogin(phoneNumber: phoneNumber, authNumber: authNumber)) } diff --git a/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift b/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift index d78b8046..295653ae 100644 --- a/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift @@ -19,7 +19,7 @@ public class DefaultRecruitmentPostUseCase: RecruitmentPostUseCase { self.repository = repository } - public func registerRecruitmentPost(inputs: RegisterRecruitmentPostBundle) -> Single> { + public func registerRecruitmentPost(inputs: RegisterRecruitmentPostBundle) -> Single> { // 마감기간이 지정되지 않는 경우 현재로 부터 한달 후로 설정 if inputs.applicationDetail.applyDeadlineType == .untilApplicationFinished { @@ -34,7 +34,7 @@ public class DefaultRecruitmentPostUseCase: RecruitmentPostUseCase { ) } - public func editRecruitmentPost(id: String, inputs: Entity.RegisterRecruitmentPostBundle) -> RxSwift.Single> { + public func editRecruitmentPost(id: String, inputs: Entity.RegisterRecruitmentPostBundle) -> RxSwift.Single> { if inputs.applicationDetail.applyDeadlineType == .untilApplicationFinished { let oneMonthLater = Calendar.current.date(byAdding: .month, value: 1, to: Date()) @@ -49,15 +49,15 @@ public class DefaultRecruitmentPostUseCase: RecruitmentPostUseCase { ) } - public func getPostDetailForCenter(id: String) -> RxSwift.Single> { + public func getPostDetailForCenter(id: String) -> RxSwift.Single> { convert(task: repository.getPostDetailForCenter(id: id)) } - public func getPostDetailForWorker(id: String) -> RxSwift.Single> { + public func getPostDetailForWorker(id: String) -> RxSwift.Single> { convert(task: repository.getPostDetailForWorker(id: id)) } - public func getPostListForWorker(request: PostPagingRequestForWorker, postCount: Int) -> Single> { + public func getPostListForWorker(request: PostPagingRequestForWorker, postCount: Int) -> Single> { let stream: Single! diff --git a/project/Projects/Domain/ConcreteUseCase/Setting/DefaultSettingUseCase.swift b/project/Projects/Domain/ConcreteUseCase/Setting/DefaultSettingUseCase.swift new file mode 100644 index 00000000..dcaa9115 --- /dev/null +++ b/project/Projects/Domain/ConcreteUseCase/Setting/DefaultSettingUseCase.swift @@ -0,0 +1,94 @@ +// +// DefaultSettingUseCase.swift +// ConcreteUseCase +// +// Created by choijunios on 8/19/24. +// + +import Foundation +import RxSwift +import UserNotifications +import UseCaseInterface +import RepositoryInterface +import Entity + +public class DefaultSettingUseCase: SettingScreenUseCase { + + let repository: AuthRepository + + public init(repository: AuthRepository) { + self.repository = repository + } + + public func checkPushNotificationApproved() -> Single { + Single.create { single in + let center = UNUserNotificationCenter.current() + center.getNotificationSettings { settings in + switch settings.authorizationStatus { + case .notDetermined, .denied: + single(.success(false)) + case .authorized, .provisional, .ephemeral: + single(.success(true)) + @unknown default: + single(.success(false)) + } + } + + return Disposables.create { } + } + } + + public func requestNotificationPermission() -> Maybe { + Maybe.create { maybe in + + let current = UNUserNotificationCenter.current() + + current.getNotificationSettings { [maybe] settings in + switch settings.authorizationStatus { + case .notDetermined: + // Request permission since the user hasn't decided yet. + current.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in + if error != nil { + maybe(.success(.error(message: "알람동의를 수행할 수 없습니다."))) + } else { + maybe(.success(.granted)) + } + } + case .denied: + // 사용자가 요청을 거부했던 상태로 설정앱을 엽니다. + maybe(.success(.openSystemSetting)) + return + case .authorized, .provisional, .ephemeral: + maybe(.success(.granted)) + default: + maybe(.completed) + break + } + } + + return Disposables.create { } + } + } + + public func getPersonalDataUsageDescriptionUrl() -> URL { + URL(string: "")! + } + + public func getApplicationPolicyUrl() -> URL { + URL(string: "")! + } + + // 센터 회원탈퇴 + public func deregisterCenterAccount(reasons: [Entity.DeregisterReasonVO], password: String) -> RxSwift.Single> { + convert( + task: repository.deregisterCenterAccount(reasons: reasons, password: password) + ) + } + + // 센터 로그아웃 + public func signoutCenterAccount() -> RxSwift.Single> { + convert( + task: repository.signoutCenterAccount() + ) + } +} diff --git a/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift index 5d1fe9ef..5ab4355b 100644 --- a/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultCenterProfileUseCase.swift @@ -19,11 +19,11 @@ public class DefaultCenterProfileUseCase: CenterProfileUseCase { self.repository = repository } - public func getProfile(mode: ProfileMode) -> Single> { + public func getProfile(mode: ProfileMode) -> Single> { convert(task: repository.getCenterProfile(mode: mode)) } - public func updateProfile(phoneNumber: String?, introduction: String?, imageInfo: ImageUploadInfo?) -> Single> { + public func updateProfile(phoneNumber: String?, introduction: String?, imageInfo: ImageUploadInfo?) -> Single> { var updateText: Single! var updateImage: Single! @@ -85,7 +85,7 @@ public class DefaultCenterProfileUseCase: CenterProfileUseCase { return convert(task: task) } - public func registerCenterProfile(state: CenterProfileRegisterState) -> Single> { + public func registerCenterProfile(state: CenterProfileRegisterState) -> Single> { var registerImage: Single! diff --git a/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultWorkerProfileUseCase.swift b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultWorkerProfileUseCase.swift index faa488b3..9207dd2c 100644 --- a/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultWorkerProfileUseCase.swift +++ b/project/Projects/Domain/ConcreteUseCase/UserInfo/DefaultWorkerProfileUseCase.swift @@ -19,11 +19,11 @@ public class DefaultWorkerProfileUseCase: WorkerProfileUseCase { self.repository = repository } - public func getProfile(mode: ProfileMode) -> Single> { + public func getProfile(mode: ProfileMode) -> Single> { convert(task: repository.getWorkerProfile(mode: mode)) } - public func updateProfile(stateObject: WorkerProfileStateObject, imageInfo: ImageUploadInfo?) -> Single> { + public func updateProfile(stateObject: WorkerProfileStateObject, imageInfo: ImageUploadInfo?) -> Single> { var updateText: Single! var updateImage: Single! diff --git a/project/Projects/Domain/Entity/Error/Auth/AuthError.swift b/project/Projects/Domain/Entity/Error/Auth/AuthError.swift deleted file mode 100644 index 8f77bc6f..00000000 --- a/project/Projects/Domain/Entity/Error/Auth/AuthError.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// AuthError.swift -// Entity -// -// Created by choijunios on 7/10/24. -// - -import Foundation - -public enum AuthError: String, DomainError { - - case accountAlreadyExist="CENTER-002" - - // undefinedError - case undefinedError="Err-000" - - public var message: String { - switch self { - case .accountAlreadyExist: - "이미 존재하는 계정입니다." - case .undefinedError: - "❌ 정의되지 않은 에러타입입니다. ❌" - } - } -} diff --git a/project/Projects/Domain/Entity/Error/Auth/InputValidationError.swift b/project/Projects/Domain/Entity/Error/Auth/InputValidationError.swift deleted file mode 100644 index 151b312d..00000000 --- a/project/Projects/Domain/Entity/Error/Auth/InputValidationError.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// InputValidationError.swift -// Entity -// -// Created by choijunios on 7/10/24. -// - -import Foundation - -public enum InputValidationError: String, DomainError { - - case InvalidSmsVerificationNumber="SMS-001" - case SmsVerificationNumberNotFound="SMS-002" - case ClientException="SMS-003" - - case ExternalApiException="CLIENT-001" - case CompanyNotFoundException="CLIENT-002" - - // undefinedError - case undefinedError="Err-000" - - public var message: String { - switch self { - case .InvalidSmsVerificationNumber: - "전화번호 인증 시, 잘못된 인증번호를 입력한 경우 발생합니다." - case .SmsVerificationNumberNotFound: - "전화번호 인증 시, 인증번호가 만료되었거나 존재하지 않는 경우 발생합니다." - case .ClientException: - "SMS 문자 발송에 실패한 경우 발생합니다." - case .ExternalApiException: - "외부 API에서 알 수 없는 문제가 발생한 경우를 모두 포함합니다." - case .CompanyNotFoundException: - "사업자 등록번호의 조회 결과가 없는 경우 발생합니다." - // MARK: undefinedError - case .undefinedError: - "❌ 정의되지 않은 에러타입입니다. ❌" - } - } -} diff --git a/project/Projects/Domain/Entity/Error/DomainError.swift b/project/Projects/Domain/Entity/Error/DomainError.swift new file mode 100644 index 00000000..c465cd9b --- /dev/null +++ b/project/Projects/Domain/Entity/Error/DomainError.swift @@ -0,0 +1,203 @@ +// +// DomainError.swift +// Entity +// +// Created by choijunios on 8/25/24. +// + +import Foundation + +public enum DomainError: Error { + + // API + case invalidParameter + + // SECURITY + case unAuthorizedRequest + case invalidLoginRequest + case invalidPassword + case unregisteredUser + + // System + case internalServerError + + // JWT + case tokenDecodeException + /// 리프래쉬필요 + case tokenNotValid + /// 재로그인 필요 + case tokenExpiredException + case tokenNotFound + case notSupportUserTokenType + + // User + case invalidVerificationNumber + case verificationNumberNotFound + case imageUploadNotCompleted + + // Center + case duplicateIdentifier + case alreadyExistCenterManager + case alreadyExistCenter + case centerNotFoundException + + // Carer + case alreadyExistCarer + + // Persistence + case resourceNotFound + + // SMS + case clientException + + // Business Registration + case businessCodeNotFound + + // Geocoding + case geoCodingFailure + + // undefinedError + case undefinedCode + case undefinedError + + public init(code: String) { + switch code { + case "API-001": + self = .invalidParameter + + case "SECURITY-001": + self = .unAuthorizedRequest + case "SECURITY-002": + self = .invalidLoginRequest + case "SECURITY-003": + self = .invalidPassword + case "SECURITY-004": + self = .unregisteredUser + + case "SYSTEM-001": + self = .internalServerError + + case "JWT-001": + self = .tokenDecodeException + case "JWT-002": + self = .tokenNotValid + case "JWT-003": + self = .tokenExpiredException + case "JWT-004": + self = .tokenNotFound + case "JWT-005": + self = .notSupportUserTokenType + + case "USER-001": + self = .invalidVerificationNumber + case "USER-002": + self = .verificationNumberNotFound + case "USER-003": + self = .imageUploadNotCompleted + + case "CENTER-001": + self = .duplicateIdentifier + case "CENTER-002": + self = .alreadyExistCenterManager + case "CENTER-003": + self = .alreadyExistCenter + case "CENTER-004": + self = .centerNotFoundException + + case "CARER-001": + self = .alreadyExistCarer + + case "PERSISTENCE-001": + self = .resourceNotFound + + case "SMS-001": + self = .clientException + + case "BUSINESS-REGISTRATION-001": + self = .businessCodeNotFound + + case "GeoCode-001": + self = .geoCodingFailure + + default: + self = .undefinedError + } + } + + // MARK: 오류 메세지 + public var message: String { + switch self { + case .invalidParameter: + return "요청하신 API에서 잘못된 파라미터가 입력되었습니다. 입력값을 다시 확인해주세요." + + case .unAuthorizedRequest: + return "접근 권한이 없습니다. 로그인이 필요한 API에 접근하려면 로그인을 먼저 해주세요." + + case .invalidLoginRequest: + return "로그인 실패: 입력하신 ID 또는 비밀번호가 잘못되었습니다. 존재하지 않는 ID로 로그인 시도 시에도 발생할 수 있습니다." + + case .invalidPassword: + return "비밀번호가 일치하지 않습니다. 정확한 비밀번호를 입력해주세요. (예: 회원 탈퇴 시 비밀번호 입력 단계에서 발생)" + + case .unregisteredUser: + return "등록되지 않은 사용자입니다. 회원가입이 필요합니다." + + case .internalServerError: + return "서버 내부에서 문제가 발생했습니다. 잠시 후 다시 시도해주세요." + + case .tokenDecodeException: + return "토큰 해석에 실패했습니다. 토큰의 형식이 올바른지 확인해주세요." + + case .tokenNotValid: + return "유효하지 않은 토큰입니다. 토큰의 값이 올바른지 다시 확인해주세요." + + case .tokenExpiredException: + return "토큰이 만료되었습니다. 다시 로그인해주세요." + + case .tokenNotFound: + return "토큰을 찾을 수 없습니다. 요청을 다시 확인해주세요." + + case .notSupportUserTokenType: + return "지원되지 않는 사용자 토큰 유형입니다. 사용 가능한 토큰 유형을 확인해주세요." + + case .invalidVerificationNumber: + return "잘못된 인증번호입니다. 다시 입력해주세요." + + case .verificationNumberNotFound: + return "인증번호가 만료되었거나 존재하지 않습니다. 새로운 인증번호를 요청해주세요." + + case .imageUploadNotCompleted: + return "이미지 업로드가 완료되지 않았습니다. 다시 시도해주세요." + + case .duplicateIdentifier: + return "이미 사용 중인 센터 ID입니다. 다른 ID를 사용해주세요." + + case .alreadyExistCenterManager: + return "해당 센터 관리자 계정이 이미 존재합니다." + + case .alreadyExistCenter: + return "이미 등록된 센터입니다. 다른 정보를 입력해주세요." + + case .centerNotFoundException: + return "해당 센터를 찾을 수 없습니다. 조회 조건을 확인해주세요." + + case .alreadyExistCarer: + return "이미 가입된 요양 보호사 정보가 존재합니다. 다른 정보를 입력해주세요." + + case .resourceNotFound: + return "요청하신 리소스를 찾을 수 없습니다. 요청이 올바른지 다시 확인해주세요." + + case .clientException: + return "SMS 발송에 실패했습니다. 입력된 정보를 다시 확인하거나 나중에 다시 시도해주세요." + + case .businessCodeNotFound: + return "사업자 등록번호를 찾을 수 없습니다. 정확한 정보를 입력했는지 확인해주세요." + + case .geoCodingFailure: + return "입력된 주소로 지리 정보를 찾을 수 없습니다. 주소를 다시 확인해주세요." + + case .undefinedCode, .undefinedError: + return "예기치 않은 오류가 발생했습니다. 잠시 후 다시 시도해주세요." + } + } +} diff --git a/project/Projects/Domain/Entity/Error/StoreError.swift b/project/Projects/Domain/Entity/Error/Etc/StoreError.swift similarity index 100% rename from project/Projects/Domain/Entity/Error/StoreError.swift rename to project/Projects/Domain/Entity/Error/Etc/StoreError.swift diff --git a/project/Projects/Domain/Entity/Error/Interface/CustomError.swift b/project/Projects/Domain/Entity/Error/Interface/CustomError.swift deleted file mode 100644 index 751afc15..00000000 --- a/project/Projects/Domain/Entity/Error/Interface/CustomError.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// DomainError.swift -// Entity -// -// Created by choijunios on 7/14/24. -// - -import Foundation - -public protocol DomainError: RawRepresentable, Error where RawValue == String { - - var message: String { get } - - static var undefinedError: Self { get } -} diff --git a/project/Projects/Domain/Entity/Error/RecruitmentPost/RecruitmentPostError.swift b/project/Projects/Domain/Entity/Error/RecruitmentPost/RecruitmentPostError.swift deleted file mode 100644 index 6dc45d1b..00000000 --- a/project/Projects/Domain/Entity/Error/RecruitmentPost/RecruitmentPostError.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// RecruitmentPostError.swift -// Entity -// -// Created by choijunios on 8/9/24. -// - -import Foundation - -public enum RecruitmentPostError: String, DomainError { - - // undefinedError - case undefinedError="Err-000" - - public var message: String { - switch self { - case .undefinedError: - "알 수 없는 오류가 발생했습니다." - } - } -} diff --git a/project/Projects/Domain/Entity/Error/UserInfo/UserInfoError.swift b/project/Projects/Domain/Entity/Error/UserInfo/UserInfoError.swift deleted file mode 100644 index 137dee3c..00000000 --- a/project/Projects/Domain/Entity/Error/UserInfo/UserInfoError.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// UserInfoError.swift -// Entity -// -// Created by choijunios on 7/20/24. -// - -import Foundation - -public enum UserInfoError: String, DomainError { - - case textUpdateFailed = "Err-001" - case imageUpdateFailed = "Err-002" - - // undefinedError - case undefinedError="Err-000" - - public var message: String { - switch self { - case .undefinedError: - "❌ \(String(describing: Self.self)) 정의되지 않은 에러타입입니다. ❌" - case .imageUpdateFailed: - "이미지 업로드에 실패했습니다." - case .textUpdateFailed: - "프로필 업로드에 실패했습니다." - } - } -} diff --git a/project/Projects/Domain/Entity/VO/Deregister/DeregisterReasonVO.swift b/project/Projects/Domain/Entity/VO/Deregister/DeregisterReasonVO.swift new file mode 100644 index 00000000..43745b69 --- /dev/null +++ b/project/Projects/Domain/Entity/VO/Deregister/DeregisterReasonVO.swift @@ -0,0 +1,37 @@ +// +// DeregisterReasonVO.swift +// Entity +// +// Created by choijunios on 8/21/24. +// + +import Foundation + +public enum DeregisterReasonVO: CaseIterable { + case matchingIssues + case inconvenienceUsingPlatform + case noReasonToContinueUsing + case usingOtherPlatform + case lackOfDesiredFeatures + case privacyConcerns + case noLongerOperatingCenter + + public var reasonText: String { + switch self { + case .matchingIssues: + return "매칭이 잘 이루어지지 않음" + case .inconvenienceUsingPlatform: + return "플랫폼 사용에 불편함을 느낌" + case .noReasonToContinueUsing: + return "플랫폼을 더 이상 사용할 이유가 없음" + case .usingOtherPlatform: + return "다른 플랫폼을 이용하고 있음" + case .lackOfDesiredFeatures: + return "원하는 기능의 부재" + case .privacyConcerns: + return "개인정보 보호 문제" + case .noLongerOperatingCenter: + return "더 이상 센터를 운영하지 않음" + } + } +} diff --git a/project/Projects/Domain/Entity/Error/RecruitmentPost/CenterEmployCardVO.swift b/project/Projects/Domain/Entity/VO/Employ/CenterEmployCardVO.swift similarity index 100% rename from project/Projects/Domain/Entity/Error/RecruitmentPost/CenterEmployCardVO.swift rename to project/Projects/Domain/Entity/VO/Employ/CenterEmployCardVO.swift diff --git a/project/Projects/Domain/RepositoryInterface/Auth/Login/AuthRepository.swift b/project/Projects/Domain/RepositoryInterface/Auth/Login/AuthRepository.swift index f73ac6da..cd37c419 100644 --- a/project/Projects/Domain/RepositoryInterface/Auth/Login/AuthRepository.swift +++ b/project/Projects/Domain/RepositoryInterface/Auth/Login/AuthRepository.swift @@ -9,10 +9,12 @@ import RxSwift import Entity public protocol AuthRepository: RepositoryBase { - + // MARK: Center func requestRegisterCenterAccount(managerName: String, phoneNumber: String, businessNumber: String, id: String, password: String) -> Single func requestCenterLogin(id: String, password: String) -> Single + func signoutCenterAccount() -> Single + func deregisterCenterAccount(reasons: [DeregisterReasonVO], password: String) -> Single // MARK: Worker func requestRegisterWorkerAccount(registerState: WorkerRegisterState) -> Single diff --git a/project/Projects/Domain/UseCaseInterface/Auth/AuthInputValidationUseCase.swift b/project/Projects/Domain/UseCaseInterface/Auth/AuthInputValidationUseCase.swift index 72119d3a..2917d6c4 100644 --- a/project/Projects/Domain/UseCaseInterface/Auth/AuthInputValidationUseCase.swift +++ b/project/Projects/Domain/UseCaseInterface/Auth/AuthInputValidationUseCase.swift @@ -27,7 +27,7 @@ public protocol AuthInputValidationUseCase: UseCaseBase { /// - PhoneNumber : "000-0000-0000" /// - returns: /// - Observable - func requestPhoneNumberAuthentication(phoneNumber: String) -> Single> + func requestPhoneNumberAuthentication(phoneNumber: String) -> Single> // #2. /// 전화번호 유효성 로직 @@ -44,7 +44,7 @@ public protocol AuthInputValidationUseCase: UseCaseBase { /// - authNumber : String 예시 "000000" /// - returns: /// - Observable - func authenticateAuthNumber(phoneNumber: String, authNumber: String) -> Single> + func authenticateAuthNumber(phoneNumber: String, authNumber: String) -> Single> // #4. /// 사업자 번호로 해당 사업장 정보 조회 @@ -54,7 +54,7 @@ public protocol AuthInputValidationUseCase: UseCaseBase { /// - Observable // MARK: 사업자 번호 조회 - func requestBusinessNumberAuthentication(businessNumber: String) -> Single> + func requestBusinessNumberAuthentication(businessNumber: String) -> Single> // #5. /// 사업자 번호 유효성 로직 @@ -78,7 +78,7 @@ public protocol AuthInputValidationUseCase: UseCaseBase { /// - id : "idle1234" /// - returns: /// - Bool, true: 가능, flase: 증복 - func requestCheckingIdDuplication(id: String) -> Single> + func requestCheckingIdDuplication(id: String) -> Single> // #8. /// 아이디 유효성확인 로직 diff --git a/project/Projects/Domain/UseCaseInterface/Auth/AuthUseCase.swift b/project/Projects/Domain/UseCaseInterface/Auth/AuthUseCase.swift index 604ab016..69e985bb 100644 --- a/project/Projects/Domain/UseCaseInterface/Auth/AuthUseCase.swift +++ b/project/Projects/Domain/UseCaseInterface/Auth/AuthUseCase.swift @@ -13,10 +13,12 @@ import Entity /// - #1. 센터 회원가입 실행 /// - #2. 센터 로그인 실행 /// - #3. 샌터 회원 탈퇴 +/// - #4. 샌터 회원 로그아웃 /// -/// - #4. 요양보호사 회원가입 실행 -/// - #5. 요양보호사 로그인 실행 -/// - #5. 요양보호사 회원탈퇴 실행 +/// - #5. 요양보호사 회원가입 실행 +/// - #6. 요양보호사 로그인 실행 +/// - #7. 요양보호사 회원탈퇴 실행 +/// - #8. 요양보호사 로그아웃 public protocol AuthUseCase: UseCaseBase { @@ -26,7 +28,7 @@ public protocol AuthUseCase: UseCaseBase { /// - registerState: CenterRegisterState func registerCenterAccount( registerState: CenterRegisterState - ) -> Single> + ) -> Single> // #2. /// 센터 로그인 실행 @@ -36,18 +38,17 @@ public protocol AuthUseCase: UseCaseBase { func loginCenterAccount( id: String, password: String - ) -> Single> + ) -> Single> - - // #4 + // #5 /// 요양 보호사 회원가입 실행 func registerWorkerAccount( registerState: WorkerRegisterState - ) -> Single> - // #5 + ) -> Single> + // #6 /// 요양 보호사 로그인 실행 func loginWorkerAccount( phoneNumber: String, authNumber: String - ) -> Single> + ) -> Single> } diff --git a/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift b/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift index e782bb3c..05d4ae56 100644 --- a/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift +++ b/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift @@ -14,13 +14,13 @@ public protocol RecruitmentPostUseCase: UseCaseBase { // MARK: Center /// 센터측이 공고를 등록하는 액션입니다. - func registerRecruitmentPost(inputs: RegisterRecruitmentPostBundle) -> Single> + func registerRecruitmentPost(inputs: RegisterRecruitmentPostBundle) -> Single> /// 센터측이 공고를 수정하는 액션입니다. - func editRecruitmentPost(id: String, inputs: RegisterRecruitmentPostBundle) -> Single> + func editRecruitmentPost(id: String, inputs: RegisterRecruitmentPostBundle) -> Single> /// 센터측이 공고를 조회하는 액션입니다. - func getPostDetailForCenter(id: String) -> Single> + func getPostDetailForCenter(id: String) -> Single> // MARK: Worker @@ -30,8 +30,8 @@ public protocol RecruitmentPostUseCase: UseCaseBase { /// - 공고상세정보(센터와 달리 고객 이름 배제) /// - 근무지 위치(위경도) /// - 센터정보(센터 id, 이름, 도로명 주소) - func getPostDetailForWorker(id: String) -> Single> + func getPostDetailForWorker(id: String) -> Single> /// 요양보호사가 메인화면에 사용할 공고리스트를 호출합니다. - func getPostListForWorker(request: PostPagingRequestForWorker, postCount: Int) -> Single> + func getPostListForWorker(request: PostPagingRequestForWorker, postCount: Int) -> Single> } diff --git a/project/Projects/Domain/UseCaseInterface/Setting/SettingScreenUseCase .swift b/project/Projects/Domain/UseCaseInterface/Setting/SettingScreenUseCase .swift new file mode 100644 index 00000000..6d3942ca --- /dev/null +++ b/project/Projects/Domain/UseCaseInterface/Setting/SettingScreenUseCase .swift @@ -0,0 +1,40 @@ +// +// SettingScreenUseCase .swift +// UseCaseInterface +// +// Created by choijunios on 8/19/24. +// + +import Foundation +import RxSwift +import Entity + +public enum NotificationApproveAction: Equatable { + case openSystemSetting + case granted + case error(message: String) +} + +public protocol SettingScreenUseCase: UseCaseBase { + + /// 현재 알람수신 동의 여부를 확인합니다. + func checkPushNotificationApproved() -> Single + + /// 알림동의를 요청합니다. + func requestNotificationPermission() -> Maybe + + /// 개인정보 처리방침 웹 URL을 가져옵니다. + func getPersonalDataUsageDescriptionUrl() -> URL + + /// 어플리케이션 이용약관을 가져옵니다. + func getApplicationPolicyUrl() -> URL + + /// 센터 회원 탈퇴 + func deregisterCenterAccount( + reasons: [DeregisterReasonVO], + password: String + ) -> Single> + + /// 센터 로그아웃 + func signoutCenterAccount() -> Single> +} diff --git a/project/Projects/Domain/UseCaseInterface/UseCaseBase.swift b/project/Projects/Domain/UseCaseInterface/UseCaseBase.swift index 8f644b70..26ca6512 100644 --- a/project/Projects/Domain/UseCaseInterface/UseCaseBase.swift +++ b/project/Projects/Domain/UseCaseInterface/UseCaseBase.swift @@ -14,7 +14,7 @@ public protocol UseCaseBase: AnyObject { } public extension UseCaseBase { /// Repository로 부터 전달받은 언어레벨의 에러를 도메인 특화 에러로 변경하고, error를 Result의 Failure로, 성공을 Success로 변경합니다. - func convert(task: Single) -> Single> { + func convert(task: Single) -> Single> { Single.create { single in let disposable = task .subscribe { success in @@ -27,13 +27,21 @@ public extension UseCaseBase { } // MARK: InputValidationError - private func toDomainError(error: Error) -> T { + private func toDomainError(error: Error) -> DomainError { if let httpError = error as? HTTPResponseException { if let code = httpError.rawCode { - return T.init(rawValue: code) ?? T.undefinedError + let domainError = DomainError(code: code) + + if domainError == .undefinedCode { +#if DEBUG + print("‼️ 정의되지 않은 에러코드가 발견되었습니다. 노션을 확인해주세요") +#endif + } + + return domainError } #if DEBUG @@ -41,6 +49,6 @@ public extension UseCaseBase { #endif } - return T.undefinedError + return DomainError.undefinedError } } diff --git a/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift b/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift index d6544ec9..42cbf4c2 100644 --- a/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift +++ b/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift @@ -20,13 +20,13 @@ public protocol CenterProfileUseCase: UseCaseBase { /// 1. 나의 센터/다른 센터 프로필 정보 조회 /// 6. 특정 센터의 프로필 불러오기 - func getProfile(mode: ProfileMode) -> Single> + func getProfile(mode: ProfileMode) -> Single> /// 2. 센터 프로필 정보 업데이트(전화번호, 센터소개글) /// 3. 센터 프로필 정보 업데이트(이미지, pre-signed-url) /// 4. 센터 프로필 정보 업데이트(이미지, pre-signed-url-callback) - func updateProfile(phoneNumber: String?, introduction: String?, imageInfo: ImageUploadInfo?) -> Single> + func updateProfile(phoneNumber: String?, introduction: String?, imageInfo: ImageUploadInfo?) -> Single> /// 5. 센터 프로필 최초 등록 - func registerCenterProfile(state: CenterProfileRegisterState) -> Single> + func registerCenterProfile(state: CenterProfileRegisterState) -> Single> } diff --git a/project/Projects/Domain/UseCaseInterface/UserInfo/WorkerProfileUseCase.swift b/project/Projects/Domain/UseCaseInterface/UserInfo/WorkerProfileUseCase.swift index 86672f4c..86c4ee25 100644 --- a/project/Projects/Domain/UseCaseInterface/UserInfo/WorkerProfileUseCase.swift +++ b/project/Projects/Domain/UseCaseInterface/UserInfo/WorkerProfileUseCase.swift @@ -19,12 +19,12 @@ public protocol WorkerProfileUseCase: UseCaseBase { /// 1. 나의(요보) 프로필 정보 조회 /// 5. 특정 요양보호사의 프로필 불러오기 - func getProfile(mode: ProfileMode) -> Single> + func getProfile(mode: ProfileMode) -> Single> /// 2. 나의(요보) 프로필 정보 업데이트(텍스트 데이터) /// 3. 나의(요보) 프로필 정보 업데이트(이미지, pre-signed-url) /// 4. 나의(요보) 프로필 정보 업데이트(이미지, pre-signed-url-callback) - func updateProfile(stateObject: WorkerProfileStateObject, imageInfo: ImageUploadInfo?) -> Single> + func updateProfile(stateObject: WorkerProfileStateObject, imageInfo: ImageUploadInfo?) -> Single> } diff --git a/project/Projects/Presentation/DSKit/ExampleApp/Sources/AlertViewController.swift b/project/Projects/Presentation/DSKit/ExampleApp/Sources/AlertViewController.swift index 87ebb128..03282175 100644 --- a/project/Projects/Presentation/DSKit/ExampleApp/Sources/AlertViewController.swift +++ b/project/Projects/Presentation/DSKit/ExampleApp/Sources/AlertViewController.swift @@ -18,17 +18,7 @@ class AlertViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { let alertVC = IdleSmallAlertController( - titleText: "정말 탈퇴하시겠어요?", - button1Info: .init( - text: "취소하기", - backgroundColor: .white, - accentColor: DSKitAsset.Colors.gray100.color - ), - button2Info: .init( - text: "탈퇴하기", - backgroundColor: DSKitAsset.Colors.orange500.color, - accentColor: DSKitAsset.Colors.orange700.color - ) + titleText: "정말 탈퇴하시겠어요?" ) alertVC.modalPresentationStyle = .fullScreen diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/check_box_icon.imageset/Contents.json b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/check_box_icon.imageset/Contents.json new file mode 100644 index 00000000..6510b0a1 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/check_box_icon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "check_box_icon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/check_box_icon.imageset/check_box_icon.svg b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/check_box_icon.imageset/check_box_icon.svg new file mode 100644 index 00000000..1c9d0052 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Resources/Icons.xcassets/check_box_icon.imageset/check_box_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleBigAlertController.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleBigAlertController.swift index bf565e09..1abc7052 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleBigAlertController.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleBigAlertController.swift @@ -12,59 +12,31 @@ import Entity public protocol IdleAlertViewModelable { - var button1Tapped: PublishRelay { get } - var button2Tapped: PublishRelay { get } + var acceptButtonClicked: PublishRelay { get } + var cancelButtonClicked: PublishRelay { get } + var acceptButtonLabelText: String { get } + var cancelButtonLabelText: String { get } + var dismiss: Driver? { get } + var title: String { get } + var description: String { get } } public class IdleBigAlertController: UIViewController { - public struct ButtonInfo { - let text: String - let backgroundColor: UIColor - let accentColor: UIColor - - public init(text: String, backgroundColor: UIColor, accentColor: UIColor) { - self.text = text - self.backgroundColor = backgroundColor - self.accentColor = accentColor - } - } - - // Init values - let titleText: String - let descriptionText: String - let button1Info: ButtonInfo - let button2Info: ButtonInfo - - public init(titleText: String, descriptionText: String, button1Info: ButtonInfo, button2Info: ButtonInfo) { - self.titleText = titleText - self.descriptionText = descriptionText - self.button1Info = button1Info - self.button2Info = button2Info - - super.init(nibName: nil, bundle: nil) - - setAppearance() - setAutoLayout() - } - - public required init?(coder: NSCoder) { fatalError() } + let customTranstionDelegate = CustomTransitionDelegate() // Not init - private var viewModel: IdleAlertViewModelable? private let disposeBag = DisposeBag() // View - private(set) lazy var titleLabel: IdleLabel = { + let titleLabel: IdleLabel = { let label = IdleLabel(typography: .Subtitle1) - label.textString = titleText label.textAlignment = .center return label }() - private(set) lazy var descriptionLabel: IdleLabel = { + let descriptionLabel: IdleLabel = { let label = IdleLabel(typography: .Body3) - label.textString = descriptionText label.attrTextColor = DSKitAsset.Colors.gray500.color label.lineBreakMode = .byWordWrapping label.textAlignment = .center @@ -72,27 +44,29 @@ public class IdleBigAlertController: UIViewController { return label }() - private(set) lazy var button1: TextButtonType1 = { - let btn = TextButtonType1( - labelText: button1Info.text, - originBackground: button1Info.backgroundColor, - accentBackgroundColor: button1Info.accentColor - ) - btn.layer.borderColor = DSKitAsset.Colors.gray100.color.cgColor - btn.layer.borderWidth = 1 - btn.label.typography = .Heading4 - return btn + public let cancelButton: IdleThirdinaryButton = { + let button = IdleThirdinaryButton(level: .medium) + button.label.textString = "" + return button }() - private(set) lazy var button2: TextButtonType1 = { - let btn = TextButtonType1( - labelText: button2Info.text, - originBackground: button2Info.backgroundColor, - accentBackgroundColor: button2Info.accentColor - ) - btn.label.typography = .Heading4 - return btn + public let acceptButton: IdlePrimaryButton = { + let button = IdlePrimaryButton(level: .mediumRed) + button.label.textString = "" + return button }() + public init() { + + super.init(nibName: nil, bundle: nil) + + self.transitioningDelegate = customTranstionDelegate + + setAppearance() + setAutoLayout() + } + + public required init?(coder: NSCoder) { fatalError() } + private func setAppearance() { view.backgroundColor = DSKitAsset.Colors.gray500.color.withAlphaComponent(0.5) @@ -103,29 +77,20 @@ public class IdleBigAlertController: UIViewController { private func setAutoLayout() { // 라벨 스택 - let labels = [ - titleLabel, - descriptionLabel - ] let textStack = VStack( - labels, + [ + Spacer(height: 8), + titleLabel + ], spacing: 8, alignment: .center ) - labels.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } - NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: textStack.topAnchor, constant: 8), - descriptionLabel.bottomAnchor.constraint(equalTo: textStack.bottomAnchor, constant: -8), - - descriptionLabel.leftAnchor.constraint(equalTo: textStack.leftAnchor, constant: 38), - descriptionLabel.rightAnchor.constraint(equalTo: textStack.rightAnchor, constant: -38), - ]) // 버튼 스택 let buttonStack = HStack( [ - button1, - button2 + cancelButton, + acceptButton ], spacing: 8, alignment: .fill, @@ -135,10 +100,14 @@ public class IdleBigAlertController: UIViewController { // 라벨 + 버튼 스택 let alertContentsStack = VStack( [ - textStack, + HStack([ + Spacer(width: 41.5), + textStack, + Spacer(width: 41.5) + ], alignment: .fill), buttonStack ], - spacing: 16, + spacing: 24, alignment: .fill ) @@ -154,7 +123,7 @@ public class IdleBigAlertController: UIViewController { alertContainer.layer.cornerRadius = 12 alertContainer.clipsToBounds = true - alertContainer.layoutMargins = .init(top: 12, left: 12, bottom: 12, right: 12) + alertContainer.layoutMargins = .init(top: 20, left: 12, bottom: 12, right: 12) [ alertContentsStack @@ -186,19 +155,100 @@ public class IdleBigAlertController: UIViewController { } public func bind(viewModel vm: IdleAlertViewModelable) { - // RC=1 - self.viewModel = vm - button1 - .eventPublisher - .map { _ in () } - .bind(to: vm.button1Tapped) + titleLabel.textString = vm.title + descriptionLabel.textString = vm.description + + acceptButton.label.textString = vm.acceptButtonLabelText + cancelButton.label.textString = vm.cancelButtonLabelText + + vm.dismiss? + .drive(onNext: { [weak self] in + + guard let self else { return } + modalPresentationStyle = .custom + dismiss(animated: true) + }) + .disposed(by: disposeBag) + + acceptButton.rx.tap + .bind(to: vm.acceptButtonClicked) .disposed(by: disposeBag) - button2 - .eventPublisher - .map { _ in () } - .bind(to: vm.button2Tapped) + cancelButton + .rx.tap + .bind(to: vm.cancelButtonClicked) .disposed(by: disposeBag) } } + +class FadeInAnimator: NSObject, UIViewControllerAnimatedTransitioning { + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.35 // 애니메이션 지속 시간 + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + guard let fromView = transitionContext.view(forKey: .from) else { return } + + let containerView = transitionContext.containerView + containerView.addSubview(fromView) + + // 애니메이션 시작 상태 설정 + fromView.alpha = 1.0 + + let duration = transitionDuration(using: transitionContext) + UIView.animate(withDuration: duration, animations: { + // 애니메이션 적용 + fromView.alpha = 0.0 + }) { _ in + // 애니메이션 완료 후 처리 + fromView.removeFromSuperview() + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + } + } +} + +class FadeOutAnimator: NSObject, UIViewControllerAnimatedTransitioning { + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.35 // 애니메이션 지속 시간 + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + guard let toView = transitionContext.view(forKey: .to) else { return } + + let containerView = transitionContext.containerView + containerView.addSubview(toView) + + // 애니메이션 시작 상태 설정 + toView.alpha = 0.0 + + let duration = transitionDuration(using: transitionContext) + UIView.animate(withDuration: duration, animations: { + // 애니메이션 적용 + toView.alpha = 1.0 + }) { _ in + // 애니메이션 완료 후 처리 + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + } + } +} + +class CustomTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate { + + func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return FadeInAnimator() // 우리가 만든 사용자 정의 애니메이터를 반환 + } + + func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> (any UIViewControllerAnimatedTransitioning)? { + return FadeOutAnimator() + } +} + + +@available(iOS 17.0, *) +#Preview("Preview", traits: .defaultLayout) { + + IdleBigAlertController() +} diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleSmallAlertController.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleSmallAlertController.swift index 0fffb44a..dcc0c84f 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleSmallAlertController.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleSmallAlertController.swift @@ -12,27 +12,11 @@ import Entity public class IdleSmallAlertController: UIViewController { - public struct ButtonInfo { - let text: String - let backgroundColor: UIColor - let accentColor: UIColor - - public init(text: String, backgroundColor: UIColor, accentColor: UIColor) { - self.text = text - self.backgroundColor = backgroundColor - self.accentColor = accentColor - } - } - // Init values let titleText: String - let button1Info: ButtonInfo - let button2Info: ButtonInfo - public init(titleText: String, button1Info: ButtonInfo, button2Info: ButtonInfo) { + public init(titleText: String) { self.titleText = titleText - self.button1Info = button1Info - self.button2Info = button2Info super.init(nibName: nil, bundle: nil) @@ -43,7 +27,6 @@ public class IdleSmallAlertController: UIViewController { public required init?(coder: NSCoder) { fatalError() } // Not init - private var viewModel: IdleAlertViewModelable? private let disposeBag = DisposeBag() // View @@ -54,26 +37,15 @@ public class IdleSmallAlertController: UIViewController { return label }() - - private(set) lazy var button1: TextButtonType1 = { - let btn = TextButtonType1( - labelText: button1Info.text, - originBackground: button1Info.backgroundColor, - accentBackgroundColor: button1Info.accentColor - ) - btn.layer.borderColor = DSKitAsset.Colors.gray100.color.cgColor - btn.layer.borderWidth = 1 - btn.label.typography = .Heading4 - return btn + private(set) lazy var cancelButton: IdleThirdinaryButton = { + let button = IdleThirdinaryButton(level: .medium) + button.label.textString = "" + return button }() - private(set) lazy var button2: TextButtonType1 = { - let btn = TextButtonType1( - labelText: button2Info.text, - originBackground: button2Info.backgroundColor, - accentBackgroundColor: button2Info.accentColor - ) - btn.label.typography = .Heading4 - return btn + private(set) lazy var acceptButton: IdlePrimaryButton = { + let button = IdlePrimaryButton(level: .mediumRed) + button.label.textString = "" + return button }() private func setAppearance() { @@ -89,8 +61,8 @@ public class IdleSmallAlertController: UIViewController { // 버튼 스택 let buttonStack = HStack( [ - button1, - button2 + cancelButton, + acceptButton ], spacing: 8, alignment: .fill, @@ -149,21 +121,4 @@ public class IdleSmallAlertController: UIViewController { alertContainer.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), ]) } - - public func bind(viewModel vm: IdleAlertViewModelable) { - // RC=1 - self.viewModel = vm - - button1 - .eventPublisher - .map { _ in () } - .bind(to: vm.button1Tapped) - .disposed(by: disposeBag) - - button2 - .eventPublisher - .map { _ in () } - .bind(to: vm.button2Tapped) - .disposed(by: disposeBag) - } } diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Button/FullRowButton.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Button/FullRowButton.swift new file mode 100644 index 00000000..f1005010 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Button/FullRowButton.swift @@ -0,0 +1,81 @@ +// +// FullRowButton.swift +// DSKit +// +// Created by choijunios on 8/19/24. +// + +import UIKit +import RxSwift +import RxCocoa + +public class FullRowButton: TappableUIView { + + let label: IdleLabel = { + let label = IdleLabel(typography: .Body2) + label.attrTextColor = DSColor.gray500.color + return label + }() + + let disposeBag = DisposeBag() + + public init(labelText: String) { + self.label.textString = labelText + super.init() + setApearance() + setLayout() + setObservable() + } + required init?(coder: NSCoder) { return nil } + + private func setApearance() { + self.backgroundColor = DSColor.gray0.color + } + private func setLayout() { + + self.layoutMargins = .init( + top: 12, + left: 20, + bottom: 12, + right: 20 + ) + + let chevronImage = DSIcon.chevronRight.image.toView() + chevronImage.tintColor = DSColor.gray200.color + let mainStack = HStack([ + label, + Spacer(), + chevronImage + ], alignment: .center, distribution: .fill) + + [ + mainStack + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) + } + + NSLayoutConstraint.activate([ + mainStack.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor), + mainStack.leftAnchor.constraint(equalTo: self.layoutMarginsGuide.leftAnchor), + mainStack.rightAnchor.constraint(equalTo: self.layoutMarginsGuide.rightAnchor), + mainStack.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor), + ]) + } + private func setObservable() { + self.rx.tap + .subscribe(onNext: { [weak self] _ in + self?.backgroundColor = DSColor.gray050.color + UIView.animate(withDuration: 0.35) { + self?.backgroundColor = DSColor.gray0.color + } + }) + .disposed(by: disposeBag) + } +} + +@available(iOS 17.0, *) +#Preview("Preview", traits: .defaultLayout) { + + FullRowButton(labelText: "내 프로필") +} diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Button/IdleUnderLineLabelButton.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Button/IdleUnderLineLabelButton.swift new file mode 100644 index 00000000..ac7142fe --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Button/IdleUnderLineLabelButton.swift @@ -0,0 +1,67 @@ +// +// IdleUnderLineLabelButton.swift +// DSKit +// +// Created by choijunios on 8/19/24. +// + +import UIKit +import RxSwift +import RxCocoa + +public class IdleUnderLineLabelButton: TappableUIView { + + let label: IdleLabel = { + let label = IdleLabel(typography: .Body2) + label.attrTextColor = DSColor.gray300.color + label.setAttr(attr: .underlineStyle, value: NSUnderlineStyle.single.rawValue) + return label + }() + + let disposeBag = DisposeBag() + + public init(labelText: String) { + self.label.textString = labelText + super.init() + setApearance() + setLayout() + setObservable() + } + required init?(coder: NSCoder) { return nil } + + private func setApearance() { + self.backgroundColor = DSColor.gray0.color + } + private func setLayout() { + + [ + label + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) + } + + NSLayoutConstraint.activate([ + label.topAnchor.constraint(equalTo: self.topAnchor), + label.leftAnchor.constraint(equalTo: self.leftAnchor), + label.rightAnchor.constraint(equalTo: self.rightAnchor), + label.bottomAnchor.constraint(equalTo: self.bottomAnchor), + ]) + } + private func setObservable() { + self.rx.tap + .subscribe(onNext: { [weak self] _ in + self?.label.alpha = 0.5 + UIView.animate(withDuration: 0.35) { + self?.label.alpha = 1.0 + } + }) + .disposed(by: disposeBag) + } +} + +@available(iOS 17.0, *) +#Preview("Preview", traits: .defaultLayout) { + + IdleUnderLineLabelButton(labelText: "로그아웃") +} diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Profile/Center/CenterInfoCardView.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Profile/Center/CenterInfoCardView.swift index 1f6422f5..e1186f0b 100644 --- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Profile/Center/CenterInfoCardView.swift +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Card/Profile/Center/CenterInfoCardView.swift @@ -45,10 +45,17 @@ public class CenterInfoCardView: TappableUIView { } + public let chevronLeftImage: UIImageView = { + let view = DSKitAsset.Icons.chevronRight.image.toView() + view.tintColor = DSKitAsset.Colors.gray200.color + return view + }() + private func setLayout() { let locationImageView = DSKitAsset.Icons.location.image.toView() locationImageView.tintColor = DSColor.gray400.color + let locationStack = HStack( [ locationImageView, @@ -59,9 +66,6 @@ public class CenterInfoCardView: TappableUIView { let labelStack = VStack([nameLabel,locationStack], spacing: 4, alignment: .leading) - let chevronLeftImage = DSKitAsset.Icons.chevronRight.image.toView() - chevronLeftImage.tintColor = DSKitAsset.Colors.gray200.color - NSLayoutConstraint.activate([ locationImageView.widthAnchor.constraint(equalToConstant: 20), locationImageView.heightAnchor.constraint(equalTo: locationImageView.widthAnchor), diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/CheckBox/CheckBoxWithLabelView.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/CheckBox/CheckBoxWithLabelView.swift new file mode 100644 index 00000000..caef8207 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/CheckBox/CheckBoxWithLabelView.swift @@ -0,0 +1,136 @@ +// +// CheckBoxWithLabelView.swift +// DSKit +// +// Created by choijunios on 8/21/24. +// + +import UIKit +import RxSwift +import RxCocoa + +public class CheckBoxWithLabelView: HStack { + + public enum State { + case idle(item: Item) + case checked(item: Item) + } + var currentState: State? + + // Init + let item: Item + let labelText: String + + // View + let label: IdleLabel = { + let label = IdleLabel(typography: .Body2) + return label + }() + + let checkIconView = DSIcon.checkBoxIcon.image.toView() + private lazy var checkBox: TappableUIView = { + let view = TappableUIView() + view.layer.cornerRadius = 2 + view.clipsToBounds = true + + view.addSubview(checkIconView) + checkIconView.translatesAutoresizingMaskIntoConstraints = false + checkIconView.tintColor = DSColor.gray0.color + view.layoutMargins = .init(top: 6, left: 4, bottom: 5, right: 3) + NSLayoutConstraint.activate([ + checkIconView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), + checkIconView.leftAnchor.constraint(equalTo: view.layoutMarginsGuide.leftAnchor), + checkIconView.rightAnchor.constraint(equalTo: view.layoutMarginsGuide.rightAnchor), + checkIconView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor), + ]) + + // border설정 + view.layer.borderWidth = 1 + + return view + }() + + // Observable + public lazy var opTap: PublishRelay = .init() + let disposeBag = DisposeBag() + + public init(item: Item, labelText: String) { + self.item = item + self.labelText = labelText + self.label.textString = labelText + super.init([], spacing: 12, alignment: .center) + setAppearance() + setLayout() + setObservable() + + // 초기상태 + currentState = .idle(item: item) + toIdle() + } + public required init(coder: NSCoder) { fatalError() } + + func toIdle() { + checkBox.layer.borderColor = DSColor.gray100.color.cgColor + checkBox.backgroundColor = DSColor.gray0.color + checkIconView.isHidden = true + } + + func toChecked() { + checkBox.layer.borderColor = DSColor.orange500.color.cgColor + checkBox.backgroundColor = DSColor.orange500.color + checkIconView.isHidden = false + } + + func setAppearance() { + + } + + func setLayout() { + + [ + checkBox, + label, + Spacer() + ].forEach { + self.addArrangedSubview($0) + } + NSLayoutConstraint.activate([ + checkBox.widthAnchor.constraint(equalToConstant: 19), + checkBox.heightAnchor.constraint(equalToConstant: 19), + ]) + } + + func setObservable() { + checkBox + .rx.tap + .observe(on: MainScheduler.instance) + .compactMap { [weak self] _ -> State? in + guard let self else { return nil } + + UIView.animate(withDuration: 0.2) { + if let currentState = self.currentState { + switch currentState { + case .idle: + self.currentState = .checked(item: self.item) + self.toChecked() + case .checked: + self.currentState = .idle(item: self.item) + self.toIdle() + } + } + } + return self.currentState + } + .bind(to: opTap) + .disposed(by: disposeBag) + } +} + +@available(iOS 17.0, *) +#Preview("Preview", traits: .defaultLayout) { + + CheckBoxWithLabelView( + item: "", + labelText: "매칭매칭매칭매칭매칭매칭매칭" + ) +} diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Navigation/IdleTitleBar.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Navigation/IdleTitleBar.swift new file mode 100644 index 00000000..207a17d0 --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Navigation/IdleTitleBar.swift @@ -0,0 +1,79 @@ +// +// IdleTitleBar.swift +// DSKit +// +// Created by choijunios on 8/19/24. +// + +import UIKit +import RxSwift +import RxCocoa + +public class IdleTitleBar: UIView { + + // Init parameters + + // View + public lazy var titleLabel: IdleLabel = { + let label = IdleLabel(typography: .Heading1) + label.textAlignment = .left + return label + }() + + private let disposeBag = DisposeBag() + + public init( + titleText: String = "", + innerViews: [UIView] + ) { + super.init(frame: .zero) + + self.titleLabel.textString = titleText + + setApearance() + setAutoLayout(innerViews: innerViews) + } + + public required init(coder: NSCoder) { fatalError() } + + func setApearance() { + + } + + private func setAutoLayout(innerViews: [UIView]) { + + self.layoutMargins = .init( + top: 20.43, + left: 20, + bottom: 12, + right: 20 + ) + + let mainStack = HStack( + [ + [ + titleLabel, + Spacer(), + ], + innerViews + ].flatMap { $0 }, + alignment: .center, + distribution: .fill + ) + + [ + mainStack + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) + } + + NSLayoutConstraint.activate([ + mainStack.leftAnchor.constraint(equalTo: self.layoutMarginsGuide.leftAnchor), + mainStack.rightAnchor.constraint(equalTo: self.layoutMarginsGuide.rightAnchor), + mainStack.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor), + mainStack.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor), + ]) + + } +} diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/SettingComponent/PushNotificationAuthRow.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/SettingComponent/PushNotificationAuthRow.swift new file mode 100644 index 00000000..e1a0752d --- /dev/null +++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/SettingComponent/PushNotificationAuthRow.swift @@ -0,0 +1,91 @@ +// +// PushNotificationAuthRow.swift +// DSKit +// +// Created by choijunios on 8/19/24. +// + +import UIKit +import RxSwift +import RxCocoa + +public class PushNotificationAuthRow: UIView { + + let label: IdleLabel = { + let label = IdleLabel(typography: .Body2) + label.attrTextColor = DSColor.gray500.color + label.textString = "알림 설정" + return label + }() + + public let `switch`: UISwitch = { + let view = UISwitch() + view.onTintColor = DSColor.orange500.color + view.isOn = false + return view + }() + + private var onFirstLoad: Bool = true + + public init() { + super.init(frame: .zero) + setApearance() + setLayout() + setObservable() + } + required init?(coder: NSCoder) { return nil } + + private func setApearance() { + self.backgroundColor = DSColor.gray0.color + } + private func setLayout() { + + self.layoutMargins = .init( + top: 12, + left: 20, + bottom: 12, + right: 20 + ) + + let mainStack = HStack([ + label, + Spacer(), + `switch` + ], alignment: .center, distribution: .fill) + + [ + mainStack + ].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + self.addSubview($0) + } + + NSLayoutConstraint.activate([ + mainStack.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor), + mainStack.leftAnchor.constraint(equalTo: self.layoutMarginsGuide.leftAnchor), + mainStack.rightAnchor.constraint(equalTo: self.layoutMarginsGuide.rightAnchor), + mainStack.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor), + ]) + } + private func setObservable() { + + } + + public override func layoutSubviews() { + super.layoutSubviews() + } + + public override func draw(_ rect: CGRect) { + if onFirstLoad, `switch`.bounds.height > label.bounds.height { + onFirstLoad = false + let per = label.bounds.height / `switch`.bounds.height + `switch`.transform = `switch`.transform.scaledBy(x: per, y: per) + } + } +} + +@available(iOS 17.0, *) +#Preview("Preview", traits: .defaultLayout) { + + PushNotificationAuthRow() +} diff --git a/project/Projects/Presentation/DSKit/Sources/Component/TextField/IdleOneLineInputField.swift b/project/Projects/Presentation/DSKit/Sources/Component/TextField/IdleOneLineInputField.swift index 94510a33..dcd7715a 100644 --- a/project/Projects/Presentation/DSKit/Sources/Component/TextField/IdleOneLineInputField.swift +++ b/project/Projects/Presentation/DSKit/Sources/Component/TextField/IdleOneLineInputField.swift @@ -375,3 +375,9 @@ public extension IdleOneLineInputField { } extension IdleOneLineInputField: IdleKeyboardAvoidable { } + +@available(iOS 17.0, *) +#Preview("Preview", traits: .defaultLayout) { + + IdleOneLineInputField(placeHolderText: "비밀번호를 입력해주세요") +} diff --git a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+IdPassword.swift b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+IdPassword.swift index e58fb4da..f7180156 100644 --- a/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+IdPassword.swift +++ b/project/Projects/Presentation/Feature/Auth/Sources/ViewModel/Center/AuthInOutStreamManager/AuthInOutStreamManager+IdPassword.swift @@ -43,7 +43,7 @@ extension AuthInOutStreamManager { print("✅ 디버그모드에서 아이디 중복검사 미실시") // ☑️ 상태추적 ☑️ stateTracker(id) - return Single.just(Result.success(id)) + return Single.just(Result.success(id)) #endif return useCase.requestCheckingIdDuplication(id: id) 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 5b58f8f0..1655fcee 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 @@ -50,7 +50,7 @@ extension AuthInOutStreamManager { let formatted = Self.formatPhoneNumber(phoneNumber: input.editingPhoneNumber.value) #if DEBUG print("✅ 디버그모드에서 번호인증 요청 무조건 통과") - return Single.just(Result.success(formatted)) + return Single.just(Result.success(formatted)) #endif return useCase.requestPhoneNumberAuthentication(phoneNumber: formatted) } @@ -111,7 +111,7 @@ extension AuthInOutStreamManager { #if DEBUG // 디버그시 인증번호 무조건 통과 print("✅ 디버그모드에서 번호인증 무조건 통과") - return Single.just(Result.success(phoneNumber)) + return Single.just(Result.success(phoneNumber)) #endif return useCase.authenticateAuthNumber(phoneNumber: phoneNumber, authNumber: authNumber) @@ -141,7 +141,7 @@ extension AuthInOutStreamManager { #if DEBUG // 디버그시 인증번호 무조건 통과 print("✅ 디버그모드에서 번호인증 무조건 통과") - return Single.just(Result.success(phoneNumber)) + return Single.just(Result.success(phoneNumber)) #endif return useCase.authenticateAuthNumber(phoneNumber: phoneNumber, authNumber: authNumber) diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/Base/BaseViewController.swift b/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/Base/BaseViewController.swift index 9e4810ca..b6bf9b04 100644 --- a/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/Base/BaseViewController.swift +++ b/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/Base/BaseViewController.swift @@ -7,6 +7,7 @@ import UIKit import Entity +import DSKit open class BaseViewController: UIViewController { } @@ -14,23 +15,32 @@ open class BaseViewController: UIViewController { } public extension BaseViewController { func showAlert(vo: DefaultAlertContentVO, onClose: (() -> ())? = nil) { - let alret = UIAlertController(title: vo.title, message: vo.message, preferredStyle: .alert) + let alert = UIAlertController(title: vo.title, message: vo.message, preferredStyle: .alert) let close = UIAlertAction(title: "닫기", style: .default) { _ in onClose?() } - alret.addAction(close) - present(alret, animated: true, completion: nil) + alert.addAction(close) + present(alert, animated: true, completion: nil) } func showAlert(vo: AlertWithCompletionVO) { - let alret = UIAlertController(title: vo.title, message: vo.message, preferredStyle: .alert) + let alert = UIAlertController(title: vo.title, message: vo.message, preferredStyle: .alert) vo.buttonInfo.forEach { (buttonTitle: String, completion: AlertWithCompletionVO.AlertCompletion?) in let button = UIAlertAction(title: buttonTitle, style: .default) { _ in completion?() } - alret.addAction(button) + alert.addAction(button) } - present(alret, animated: true, completion: nil) + present(alert, animated: true, completion: nil) + } + + func showIdleModal( + viewModel: IdleAlertViewModelable + ) { + let alertVC = IdleBigAlertController() + alertVC.bind(viewModel: viewModel) + alertVC.modalPresentationStyle = .custom + present(alertVC, animated: true, completion: nil) } } diff --git a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift index a4ea3d36..3a572918 100644 --- a/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Center/ExampleApp/Sources/SceneDelegate.swift @@ -47,9 +47,17 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // ) // ) // - let vc = CenterRecruitmentPostBoardVC() + let vc = CenterSettingVC() + let vm = CenterSettingVM( + coordinator: nil, + settingUseCase: DefaultSettingUseCase(), + centerProfileUseCase: DefaultCenterProfileUseCase( + repository: DefaultUserProfileRepository() + ) + + ) -// vc.bind(viewModel: vm) + vc.bind(viewModel: vm) window = UIWindow(windowScene: windowScene) window?.rootViewController = vc diff --git a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/CenterRecruitmentPostBoardScreenCoordinator.swift b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/CenterRecruitmentPostBoardScreenCoordinator.swift new file mode 100644 index 00000000..29439471 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/CenterRecruitmentPostBoardScreenCoordinator.swift @@ -0,0 +1,54 @@ +// +// CenterRecruitmentPostBoardScreenCoordinator.swift +// CenterFeature +// +// Created by choijunios on 8/25/24. +// + +import UIKit +import PresentationCore +import UseCaseInterface +import Entity + +public class CenterRecruitmentPostBoardScreenCoordinator: ChildCoordinator { + + public weak var viewControllerRef: UIViewController? + public weak var parent: RecruitmentManagementCoordinatable? + public let navigationController: UINavigationController + + public init( + navigationController: UINavigationController + ) { + self.navigationController = navigationController + } + + deinit { + printIfDebug("\(String(describing: RegisterRecruitmentCoordinator.self))") + } + + public func start() { + let vc = CenterRecruitmentPostBoardVC() + let vm = CenterRecruitmentPostBoardVM(coordinator: self) + vc.bind(viewModel: vm) + viewControllerRef = vc + navigationController.pushViewController(vc, animated: false) + } + + public func coordinatorDidFinish() { + popViewController() + parent?.removeChildCoordinator(self) + } + + public func showCheckingApplicantScreen(postId: String) { + parent?.showCheckingApplicantScreen(postId: postId) + } + + public func showPostDetailScreenForCenter(postId: String, applicantCount: Int?) { + parent?.showPostDetailScreenForCenter(postId: postId, applicantCount: applicantCount) + } + + public func showEditScreen(postId: String) { + parent?.showEditScreen(postId: postId) + } +} + diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RecruitmentPost/RegisterRecruitmentPostCoordinator.swift b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/RegisterRecruitmentPostCoordinator.swift similarity index 97% rename from project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RecruitmentPost/RegisterRecruitmentPostCoordinator.swift rename to project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/RegisterRecruitmentPostCoordinator.swift index ca84ed61..bb0a87db 100644 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RecruitmentPost/RegisterRecruitmentPostCoordinator.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/RecruitmentPost/RegisterRecruitmentPostCoordinator.swift @@ -1,6 +1,6 @@ // -// asd.swift -// RootFeature +// RegisterRecruitmentPostCoordinator.swift +// CenterFeature // // Created by choijunios on 8/14/24. // @@ -9,8 +9,6 @@ import UIKit import PresentationCore import UseCaseInterface import Entity -import CenterFeature -import WorkerFeature import BaseFeature diff --git a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/Setting/CenterSettingScreenCoordinator.swift b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/Setting/CenterSettingScreenCoordinator.swift new file mode 100644 index 00000000..cf5c6e39 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/Setting/CenterSettingScreenCoordinator.swift @@ -0,0 +1,73 @@ +// +// CenterSettingScreenCoordinator.swift +// CenterFeature +// +// Created by choijunios on 8/25/24. +// + +import UIKit +import PresentationCore +import UseCaseInterface +import Entity + +public class CenterSettingScreenCoordinator: 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: CenterSettingScreenCoordinatable? + + 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: CenterSettingScreenCoordinator.self))") + } + + public func start() { + let vc = CenterSettingVC() + let vm = CenterSettingVM( + coordinator: self, + settingUseCase: settingUseCase, + centerProfileUseCase: centerProfileUseCase + ) + vc.bind(viewModel: vm) + viewControllerRef = vc + navigationController.pushViewController(vc, animated: false) + } + + public func coordinatorDidFinish() { + parent?.removeChildCoordinator(self) + popViewController() + } + + public func popToRoot() { + + /// Root까지 네비게이션을 제거합니다. + NotificationCenter.default.post(name: .popToInitialVC, object: nil) + } + + public func startRemoveCenterAccountFlow() { + parent?.startRemoveCenterAccountFlow() + } +} + diff --git a/project/Projects/Presentation/Feature/Center/Sources/Coordinator/Setting/PasswordForDeregisterCoordinator.swift b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/Setting/PasswordForDeregisterCoordinator.swift new file mode 100644 index 00000000..537121cb --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/Coordinator/Setting/PasswordForDeregisterCoordinator.swift @@ -0,0 +1,68 @@ +// +// PasswordForDeregisterCoordinator.swift +// CenterFeature +// +// Created by choijunios on 8/21/24. +// + +import UIKit +import PresentationCore +import UseCaseInterface +import Entity + +public class PasswordForDeregisterCoordinator: ChildCoordinator { + + public struct Dependency { + let settingUseCase: SettingScreenUseCase + let reasons: [DeregisterReasonVO] + let navigationController: UINavigationController + + public init(settingUseCase: SettingScreenUseCase, reasons: [DeregisterReasonVO], navigationController: UINavigationController) { + self.settingUseCase = settingUseCase + self.reasons = reasons + self.navigationController = navigationController + } + } + + public weak var viewControllerRef: UIViewController? + public weak var parent: DeregisterCoordinatable? + + public let navigationController: UINavigationController + let settingUseCase: SettingScreenUseCase + let reasons: [DeregisterReasonVO] + + public init( + dependency: Dependency + ) { + self.settingUseCase = dependency.settingUseCase + self.reasons = dependency.reasons + self.navigationController = dependency.navigationController + } + + deinit { + printIfDebug("\(String(describing: PasswordForDeregisterCoordinator.self))") + } + + public func start() { + let vc = PasswordForDeregisterVC() + let vm = PasswordForDeregisterVM( + deregisterReasons: reasons, + coordinator: self, + settingUseCase: settingUseCase + ) + vc.bind(viewModel: vm) + viewControllerRef = vc + navigationController.pushViewController(vc, animated: true) + } + + public func coordinatorDidFinish() { + popViewController() + parent?.removeChildCoordinator(self) + } + + public func popToRoot() { + + /// Root까지 네비게이션을 제거합니다. + } +} + diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/CheckApplicant/CheckApplicantVM.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/CheckApplicant/CheckApplicantVM.swift index e514744a..73e916b8 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/CheckApplicant/CheckApplicantVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/Board/CheckApplicant/CheckApplicantVM.swift @@ -77,7 +77,7 @@ public class CheckApplicantVM: CheckApplicantViewModelable { .init(vo: vo, coordinator: coorindator) } - func publishPostApplicantVOMocks() -> Single> { + func publishPostApplicantVOMocks() -> Single> { .just(.success((0...10).map { _ in PostApplicantVO.mock })) } diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/DetailVC/PostDetailForCenterVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/DetailVC/PostDetailForCenterVC.swift index bd4283e4..5f610202 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/DetailVC/PostDetailForCenterVC.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/View/RecruitmentPost/DetailVC/PostDetailForCenterVC.swift @@ -241,10 +241,7 @@ public class PostDetailForCenterVC: BaseViewController { } private func setObservable() { - // 지도뷰 풀스크린 - // 재사용률이 떨어져 ViewController에 직접 삽입합니다. - let fullMapVC = WorkPlaceAndWorkerLocationFullVC() - navigationController?.pushViewController(fullMapVC, animated: true) + } public func bind(viewModel: PostDetailViewModelable) { diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/Setting/CenterSettingVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/Setting/CenterSettingVC.swift new file mode 100644 index 00000000..98b19f48 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/View/Setting/CenterSettingVC.swift @@ -0,0 +1,275 @@ +// +// CenterSettingVC.swift +// CenterFeature +// +// Created by choijunios on 8/19/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit + +public class CenterSettingVC: BaseViewController { + + var viewModel: CenterSettingVMable? + + // Init + + // View + let titleBar: IdleTitleBar = { + let bar = IdleTitleBar(titleText: "설정", innerViews: []) + return bar + }() + + let myCenterInfoButton: FullRowButton = { + let button = FullRowButton(labelText: "내 센터 정보") + return button + }() + let centerInfoCard: CenterInfoCardView = { + let view = CenterInfoCardView() + view.chevronLeftImage.isHidden = true + return view + }() + 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: CenterSettingVMable) { + + self.viewModel = viewModel + + // Input + Observable.merge( + myCenterInfoButton.rx.tap.asObservable(), + centerInfoCard.rx.tap.asObservable() + ) + .bind(to: viewModel.myCenterProfileButtonClicked) + .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) + + viewModel + .centerInfo? + .drive(onNext: { [weak self] info in + self?.centerInfoCard.bind(nameText: info.name, locationText: info.location) + }) + .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 myCenterInfoContainer = VStack([ + myCenterInfoButton, + HStack([ + Spacer(width: 20), + centerInfoCard, + Spacer(width: 20), + ]), + Spacer(height: 12), + ], alignment: .fill) + + myCenterInfoContainer.backgroundColor = DSColor.gray0.color + + let viewList = [ + myCenterInfoContainer, + 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) + } +} + + +@available(iOS 17.0, *) +#Preview("Preview", traits: .defaultLayout) { + + CenterSettingVC() +} diff --git a/project/Projects/Presentation/Feature/Center/Sources/View/Setting/PasswordForDeregisterVC.swift b/project/Projects/Presentation/Feature/Center/Sources/View/Setting/PasswordForDeregisterVC.swift new file mode 100644 index 00000000..d0ab375e --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/View/Setting/PasswordForDeregisterVC.swift @@ -0,0 +1,180 @@ +// +// PasswordForDeregisterVC.swift +// CenterFeature +// +// Created by choijunios on 8/21/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import UseCaseInterface +import RxCocoa +import RxSwift +import Entity +import DSKit + +public class PasswordForDeregisterVC: BaseViewController { + + // Init + + // Not init + var viewModel: PasswordForDeregisterVM? + + // View + let navigationBar: IdleNavigationBar = { + let bar = IdleNavigationBar(innerViews: []) + bar.titleLabel.textString = "계정 삭제" + return bar + }() + let titleLabel: IdleLabel = { + let label = IdleLabel(typography: .Heading1) + label.textAlignment = .left + label.numberOfLines = 2 + label.textString = "마지막으로\n비밀번호를 입력해주세요" + return label + }() + + let passwordField: IdleOneLineInputField = { + let field = IdleOneLineInputField(placeHolderText: "비밀번호를 입력해주세요") + return field + }() + + 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 passwordLabel = IdleLabel(typography: .Subtitle4) + passwordLabel.textString = "비밀번호" + passwordLabel.textColor = DSColor.gray500.color + passwordLabel.textAlignment = .left + + let textFieldStack = VStack( + [ + passwordLabel, + passwordField, + ], + spacing: 6, + alignment: .fill + ) + + let buttonStack = HStack( + [ + cancelButton, + acceptDeregisterButton + ], + spacing: 8, + alignment: .center, + distribution: .fillEqually + ) + + [ + navigationBar, + titleLabel, + textFieldStack, + 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), + + 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() { + passwordField + .eventPublisher + .map { password in + password.count > 0 + } + .observe(on: MainScheduler.asyncInstance) + .subscribe(onNext: { [weak self] isValid in + self?.acceptDeregisterButton.setEnabled(isValid) + }) + .disposed(by: disposeBag) + } + + public func bind(viewModel: PasswordForDeregisterVM) { + + self.viewModel = viewModel + + // Input + acceptDeregisterButton + .rx.tap + .withLatestFrom(passwordField.eventPublisher) + .bind(to: viewModel.deregisterButtonClicked) + .disposed(by: disposeBag) + + navigationBar + .backButton.rx.tap + .bind(to: viewModel.exitButtonClicked) + .disposed(by: disposeBag) + + // Output + viewModel + .alert? + .drive(onNext: { [weak self] alertVO in + self?.showAlert(vo: alertVO) + }) + .disposed(by: disposeBag) + } +} + diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/CenterRecruitmentPostBoardVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/CenterRecruitmentPostBoardVM.swift index b92b49d6..da6b6ead 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/CenterRecruitmentPostBoardVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/CenterRecruitmentPostBoardVM.swift @@ -21,7 +21,7 @@ public protocol CenterRecruitmentPostBoardViewModelable: OnGoingPostViewModelabl public class CenterRecruitmentPostBoardVM: CenterRecruitmentPostBoardViewModelable { - weak var coordinator: RecruitmentManagementCoordinatable? + weak var coordinator: CenterRecruitmentPostBoardScreenCoordinator? public var requestOngoingPost: PublishRelay = .init() public var requestClosedPost: PublishRelay = .init() @@ -31,7 +31,7 @@ public class CenterRecruitmentPostBoardVM: CenterRecruitmentPostBoardViewModelab public var alert: Driver? - public init(coordinator: RecruitmentManagementCoordinatable?) { + public init(coordinator: CenterRecruitmentPostBoardScreenCoordinator?) { self.coordinator = coordinator let requestOngoingPostResult = requestOngoingPost @@ -69,11 +69,11 @@ public class CenterRecruitmentPostBoardVM: CenterRecruitmentPostBoardViewModelab .asDriver(onErrorJustReturn: .default) } - func publishOngoingPostMocks() -> Single> { + func publishOngoingPostMocks() -> Single> { return .just(.success((0...10).map { _ in CenterEmployCardVO.mock })) } - func publishClosedPostMocks() -> Single> { + func publishClosedPostMocks() -> Single> { return .just(.success((0...10).map { _ in CenterEmployCardVO.mock })) } @@ -88,7 +88,7 @@ public class CenterRecruitmentPostBoardVM: CenterRecruitmentPostBoardViewModelab // MARK: 카드 뷰에 사용될 ViewModel class CenterEmployCardVM: CenterEmployCardViewModelable { - weak var coordinator: RecruitmentManagementCoordinatable? + weak var coordinator: CenterRecruitmentPostBoardScreenCoordinator? // Init let id: String @@ -104,7 +104,7 @@ class CenterEmployCardVM: CenterEmployCardViewModelable { let disposeBag = DisposeBag() - init(vo: CenterEmployCardVO, coordinator: RecruitmentManagementCoordinatable? = nil) { + init(vo: CenterEmployCardVO, coordinator: CenterRecruitmentPostBoardScreenCoordinator?) { self.id = vo.postId self.coordinator = coordinator @@ -119,7 +119,7 @@ class CenterEmployCardVM: CenterEmployCardViewModelable { .subscribe(onNext: { [weak self] _ in guard let self else { return } - coordinator?.showCheckingApplicantScreen(postId: id) + self.coordinator?.showCheckingApplicantScreen(postId: id) }) .disposed(by: disposeBag) @@ -127,7 +127,7 @@ class CenterEmployCardVM: CenterEmployCardViewModelable { .subscribe(onNext: { [weak self] _ in guard let self else { return } - coordinator?.showPostDetailScreenForCenter(postId: id, applicantCount: vo.applicantCount) + self.coordinator?.showPostDetailScreenForCenter(postId: id, applicantCount: vo.applicantCount) }) .disposed(by: disposeBag) @@ -135,7 +135,7 @@ class CenterEmployCardVM: CenterEmployCardViewModelable { .subscribe(onNext: { [weak self] _ in guard let self else { return } - coordinator?.showEditScreen(postId: id) + self.coordinator?.showEditScreen(postId: id) }) .disposed(by: disposeBag) } diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/EditPostVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/EditPostVM.swift index 189fdcf1..f8f797f8 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/EditPostVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/EditPostVM.swift @@ -389,7 +389,7 @@ public class EditPostVM: EditPostViewModelable { let inputValidationFailure = inputValidationResult.compactMap { $0 } let editingRequestResult = inputValidationSuccess - .flatMap { [weak self] _ -> Single> in + .flatMap { [weak self] _ -> Single> in guard let self else { return .never() } return recruitmentPostUseCase.editRecruitmentPost( diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/RegisterRecruitmentPostVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/RegisterRecruitmentPostVM.swift index b64439ec..c63f41b6 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/RegisterRecruitmentPostVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RecruitmentPost/RegisterRecruitmentPostVM.swift @@ -460,7 +460,7 @@ public class RegisterRecruitmentPostVM: RegisterRecruitmentPostViewModelable { // MARK: ----------------- let registerPostResult = registerButtonClicked - .flatMap { [weak self] _ -> Single> in + .flatMap { [weak self] _ -> Single> in guard let self else { return .never() } // 공고를 등록합니다. diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RegisterCenterInfo/RegisterCenterInfoVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RegisterCenterInfo/RegisterCenterInfoVM.swift index bb02cf59..7a8e43da 100644 --- a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RegisterCenterInfo/RegisterCenterInfoVM.swift +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/RegisterCenterInfo/RegisterCenterInfoVM.swift @@ -115,7 +115,7 @@ public class RegisterCenterInfoVM: RegisterCenterInfoViewModelable { let profileRegisterResult = self.completeButtonPressed .flatMap { [useCase, stateObject] _ in #if DEBUG - return Single>.just(.success(())) + return Single>.just(.success(())) #endif return useCase.registerCenterProfile(state: stateObject) } diff --git a/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Setting/CenterSettingVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Setting/CenterSettingVM.swift new file mode 100644 index 00000000..a92db919 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Setting/CenterSettingVM.swift @@ -0,0 +1,226 @@ +// +// CenterSettingVM.swift +// CenterFeature +// +// Created by choijunios on 8/19/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit +import UseCaseInterface +import UserNotifications + +public protocol CenterSettingVMable { + + // Input + var viewWillAppear: PublishRelay { get } + var myCenterProfileButtonClicked: PublishRelay { get } + var approveToPushNotification: PublishRelay { get } + + var signOutButtonComfirmed: PublishRelay { get } + var removeAccountButtonClicked: PublishRelay { get } + + // Output + var pushNotificationApproveState: Driver? { get } + var showSettingAlert: Driver? { get } + var centerInfo: Driver<(name: String, location: String)>? { get } + var alert: Driver? { get } + + // SignOut + func createSingOutVM() -> IdleAlertViewModelable +} + +public class CenterSettingVM: CenterSettingVMable { + + // Init + weak var coordinator: CenterSettingScreenCoordinator? + let settingUseCase: SettingScreenUseCase + let centerProfileUseCase: CenterProfileUseCase + + public var viewWillAppear: RxRelay.PublishRelay = .init() + public var myCenterProfileButtonClicked: 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 centerInfo: RxCocoa.Driver<(name: String, location: String)>? + public var showSettingAlert: Driver? + public var alert: RxCocoa.Driver? + + let disposeBag = DisposeBag() + + public init( + coordinator: CenterSettingScreenCoordinator?, + 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 fetchCenterProfileResult = viewWillAppear + .flatMap { [centerProfileUseCase] _ in + centerProfileUseCase.getProfile(mode: .myProfile) + } + + let fetchCenterProfileSuccess = fetchCenterProfileResult.compactMap { $0.value } + let fetchCenterProfileFailure = fetchCenterProfileResult.compactMap { $0.error } + + centerInfo = fetchCenterProfileSuccess + .map { centerProfileVO in + ( + centerProfileVO.centerName, + centerProfileVO.roadNameAddress + ) + } + .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?.startRemoveCenterAccountFlow() + }) + .disposed(by: disposeBag) + + + // MARK: Alert + alert = Observable.merge( + approveRequestError.map { _ in "알람수신 동의 실패" }, + fetchCenterProfileFailure.map { $0.message }, + 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/Feature/Center/Sources/ViewModel/Setting/PasswordForDeregisterVM.swift b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Setting/PasswordForDeregisterVM.swift new file mode 100644 index 00000000..531638c7 --- /dev/null +++ b/project/Projects/Presentation/Feature/Center/Sources/ViewModel/Setting/PasswordForDeregisterVM.swift @@ -0,0 +1,73 @@ +// +// PasswordForDeregisterVM.swift +// CenterFeature +// +// Created by choijunios on 8/21/24. +// + +import BaseFeature +import UseCaseInterface +import RxCocoa +import RxSwift +import Entity + +public class PasswordForDeregisterVM: DefaultAlertOutputable { + + public weak var coordinator: PasswordForDeregisterCoordinator? + + public let deregisterButtonClicked: PublishRelay = .init() + public let exitButtonClicked: PublishRelay = .init() + public var alert: RxCocoa.Driver? + + let settingUseCase: SettingScreenUseCase + + let disposeBag = DisposeBag() + + public init( + deregisterReasons: [DeregisterReasonVO], + coordinator: PasswordForDeregisterCoordinator, + settingUseCase: SettingScreenUseCase + ) { + self.coordinator = coordinator + self.settingUseCase = settingUseCase + + let deregisterResult = deregisterButtonClicked + .flatMap { [settingUseCase] password in + settingUseCase.deregisterCenterAccount( + reasons: deregisterReasons, + password: password + ) + } + .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) + + exitButtonClicked + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] _ in + self?.coordinator?.coordinatorDidFinish() + }) + .disposed(by: disposeBag) + + alert = deregisterFailure + .map { error in + DefaultAlertContentVO( + title: "회원탈퇴 실패", + message: error.message + ) + } + .asDriver(onErrorJustReturn: .default) + } +} diff --git a/project/Projects/Presentation/Feature/Root/ExampleApp/Sources/SceneDelegate.swift b/project/Projects/Presentation/Feature/Root/ExampleApp/Sources/SceneDelegate.swift index 71ab8589..7349c2fa 100644 --- a/project/Projects/Presentation/Feature/Root/ExampleApp/Sources/SceneDelegate.swift +++ b/project/Projects/Presentation/Feature/Root/ExampleApp/Sources/SceneDelegate.swift @@ -15,7 +15,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - var coordinator: RegisterRecruitmentPostCoordinator! + var coordinator: DeRegisterCoordinator! func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { @@ -31,12 +31,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let nav = UINavigationController() nav.setNavigationBarHidden(true, animated: false) - self.coordinator = RegisterRecruitmentPostCoordinator( + self.coordinator = DeRegisterCoordinator( dependency: .init( - navigationController: nav, - recruitmentPostUseCase: DefaultRecruitmentPostUseCase( - repository: DefaultRecruitmentPostRepository(store) - ) + userType: .center, + authUseCase: DefaultAuthUseCase( + repository: DefaultAuthRepository() + ), + navigationController: nav ) ) diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RecruitmentPost/CheckApplicantCoordinator.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/CheckApplicantCoordinator.swift similarity index 96% rename from project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RecruitmentPost/CheckApplicantCoordinator.swift rename to project/Projects/Presentation/Feature/Root/Sources/Screen/Center/CheckApplicantCoordinator.swift index e1cde0fa..9f99a774 100644 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/RecruitmentPost/CheckApplicantCoordinator.swift +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/CheckApplicantCoordinator.swift @@ -8,9 +8,9 @@ import UIKit import PresentationCore import UseCaseInterface -import Entity import CenterFeature import WorkerFeature +import Entity import BaseFeature public class CheckApplicantCoordinator: CheckApplicantCoordinatable { @@ -45,7 +45,7 @@ public class CheckApplicantCoordinator: CheckApplicantCoordinatable { } deinit { - printIfDebug("\(String(describing: RegisterRecruitmentCoordinator.self))") + printIfDebug("\(String(describing: CheckApplicantCoordinator.self))") } public func start() { diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/Setting/CenterSettingCoordinator.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/Setting/CenterSettingCoordinator.swift deleted file mode 100644 index 7a0eaa67..00000000 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/Coordinator/Setting/CenterSettingCoordinator.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// CenterSettingCoordinator.swift -// RootFeature -// -// Created by choijunios on 8/19/24. -// - -import UIKit -import PresentationCore - -public class CenterSettingCoordinator: ChildCoordinator { - - public weak var viewControllerRef: UIViewController? - - public var navigationController: UINavigationController - - public init(navigationController: UINavigationController) { - self.navigationController = navigationController - } - - public func start() { - let vc = TestSettingVC() - - navigationController.pushViewController(vc, animated: false) - } - - public func coordinatorDidFinish() { - - } -} diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/View/RecuitmentManagementVC.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/View/RecuitmentManagementVC.swift deleted file mode 100644 index 4532a107..00000000 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Center/View/RecuitmentManagementVC.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// RecuitmentManagementVC.swift -// RootFeature -// -// Created by choijunios on 7/25/24. -// - -import UIKit -import RxCocoa -import RxSwift - -public class RecuitmentManagementVC: UIViewController { - - weak var coordinator: RecruitmentManagementCoordinator? - - public init(coordinator: RecruitmentManagementCoordinator) { - self.coordinator = coordinator - - super.init(nibName: nil, bundle: nil) - - setAppearacne() - } - - public required init?(coder: NSCoder) { fatalError() } - - let dispoesBag = DisposeBag() - - private func setAppearacne() { - view.backgroundColor = .white - - let label = UILabel() - label.text = "공고 관리" - - let button1 = UIButton() - button1.setTitle("센터정보 등록", for: .normal) - button1.setTitleColor(.black, for: .normal) - button1.isUserInteractionEnabled = true - - let button2 = UIButton() - button2.setTitle("공고 등록", for: .normal) - button2.setTitleColor(.black, for: .normal) - button2.isUserInteractionEnabled = true - - [ - label, - button1, - button2, - ].forEach { - $0.translatesAutoresizingMaskIntoConstraints = false - view.addSubview($0) - } - - NSLayoutConstraint.activate([ - label.centerXAnchor.constraint(equalTo: view.centerXAnchor), - label.centerYAnchor.constraint(equalTo: view.centerYAnchor), - - button1.centerXAnchor.constraint(equalTo: view.centerXAnchor), - button1.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 15), - - button2.centerXAnchor.constraint(equalTo: view.centerXAnchor), - button2.topAnchor.constraint(equalTo: button1.bottomAnchor, constant: 15), - ]) - } -} 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 new file mode 100644 index 00000000..cd97ef53 --- /dev/null +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/Coordinator/DeRegisterCoordinator.swift @@ -0,0 +1,83 @@ +// +// DeRegisterCoordinator.swift +// RootFeature +// +// Created by choijunios on 8/21/24. +// + +import UIKit +import Entity +import PresentationCore +import CenterFeature +import UseCaseInterface + +public class DeRegisterCoordinator: DeregisterCoordinatable { + + public struct Dependency { + let userType: UserType + let settingUseCase: SettingScreenUseCase + let navigationController: UINavigationController + + public init(userType: UserType, settingUseCase: SettingScreenUseCase, navigationController: UINavigationController) { + self.userType = userType + self.settingUseCase = settingUseCase + self.navigationController = navigationController + } + } + + public var childCoordinators: [any Coordinator] = [] + + public var navigationController: UINavigationController + + public var parent: ParentCoordinator? + + var viewControllerRef: UIViewController? + let userType: UserType + let settingUseCase: SettingScreenUseCase + + public init(dependency: Dependency) { + self.userType = dependency.userType + self.settingUseCase = dependency.settingUseCase + self.navigationController = dependency.navigationController + } + + public func start() { + showSelectReasonScreen() + } + + public func showSelectReasonScreen() { + let coordinator: SelectReasonCoordinator = .init( + dependency: .init( + userType: userType, + settingUseCase: settingUseCase, + navigationController: navigationController + ) + ) + addChildCoordinator(coordinator) + coordinator.parent = self + coordinator.start() + } + + public func showFinalPasswordScreen(reasons: [Entity.DeregisterReasonVO]) { + + let coordinator = PasswordForDeregisterCoordinator( + dependency: .init( + settingUseCase: settingUseCase, + reasons: reasons, + navigationController: navigationController + ) + ) + addChildCoordinator(coordinator) + coordinator.parent = self + coordinator.start() + } + + public func showFinalPhoneAuthScreen(reasons: [Entity.DeregisterReasonVO]) { + + } + + public func cancelDeregister() { + clearChildren() + parent?.removeChildCoordinator(self) + } +} 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 new file mode 100644 index 00000000..189f0d1f --- /dev/null +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/Coordinator/Sub/SelectReasonCoordinator.swift @@ -0,0 +1,72 @@ +// +// SelectReasonCoordinator.swift +// RootFeature +// +// Created by choijunios on 8/21/24. +// + +import UIKit +import PresentationCore +import UseCaseInterface +import Entity + +public class SelectReasonCoordinator: ChildCoordinator { + + public struct Dependency { + let userType: UserType + let settingUseCase: SettingScreenUseCase + let navigationController: UINavigationController + + public init(userType: UserType, settingUseCase: SettingScreenUseCase, navigationController: UINavigationController) { + self.userType = userType + self.settingUseCase = settingUseCase + self.navigationController = navigationController + } + } + + public weak var viewControllerRef: UIViewController? + public var navigationController: UINavigationController + public weak var parent: DeregisterCoordinatable? + + let userType: UserType + let settingUseCase: SettingScreenUseCase + + public init(dependency: Dependency) { + self.userType = dependency.userType + self.settingUseCase = dependency.settingUseCase + self.navigationController = dependency.navigationController + } + + deinit { + printIfDebug("\(String(describing: SelectReasonCoordinator.self))") + } + + public func start() { + var vm: DeregisterReasonVMable! + switch userType { + case .center: + vm = CenterDeregisterReasonsVM(coordinator: self) + case .worker: + fatalError() + } + + let vc = DeregisterReasonVC() + vc.bind(viewModel: vm) + viewControllerRef = vc + navigationController.pushViewController(vc, animated: true) + } + + public func coordinatorDidFinish() { + popViewController() + parent?.removeChildCoordinator(self) + } + + public func showPasswordAuthScreen(reasons: [DeregisterReasonVO]) { + parent?.showFinalPasswordScreen(reasons: reasons) + } + + public func showPhoneNumberAuthScreen() { + + } +} + diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/View/DeregisterReasonVC.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/View/DeregisterReasonVC.swift new file mode 100644 index 00000000..01fecf65 --- /dev/null +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/View/DeregisterReasonVC.swift @@ -0,0 +1,222 @@ +// +// DeregisterReasonVC.swift +// RootFeature +// +// Created by choijunios on 8/21/24. +// + +import UIKit +import PresentationCore +import BaseFeature +import RxCocoa +import RxSwift +import Entity +import DSKit +import Entity + +public protocol DeregisterReasonVMable { + var coordinator: SelectReasonCoordinator? { get } + var exitButonClicked: PublishRelay { get } + var acceptDeregisterButonClicked: PublishRelay<[DeregisterReasonVO]> { get } +} + +public class DeregisterReasonVC: BaseViewController { + + // Init + + // Not init + var viewModel: DeregisterReasonVMable? + + // View + let navigationBar: IdleNavigationBar = { + let bar = IdleNavigationBar(innerViews: []) + bar.titleLabel.textString = "계정 삭제" + return bar + }() + let titleLabel: IdleLabel = { + let label = IdleLabel(typography: .Heading1) + label.textString = "정말 탈퇴하시겠어요?" + return label + }() + let subTitleLabel: IdleLabel = { + let label = IdleLabel(typography: .Body3) + label.numberOfLines = 2 + label.textAlignment = .left + label.textString = "계정을 삭제하시려는 이유를 알려주세요.\n소중한 피드백을 받아 더 나은 서비스로 보답하겠습니다." + label.attrTextColor = DSColor.gray300.color + return label + }() + var itemViewList: [CheckBoxWithLabelView] = { + DeregisterReasonVO.allCases.map { vo in + return CheckBoxWithLabelView( + item: vo, + labelText: vo.reasonText + ) + } + }() + + 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 var selectedReasons: [DeregisterReasonVO: Bool] = { + var dict: [DeregisterReasonVO: Bool] = [:] + DeregisterReasonVO.allCases.forEach { vo in + dict[vo] = false + } + return dict + }() + + 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 checkListScrollView = UIScrollView() + checkListScrollView.contentInset.top = 32 + let contentGuide = checkListScrollView.contentLayoutGuide + let frameGuide = checkListScrollView.frameLayoutGuide + let contentStack = VStack(itemViewList, spacing: 16, alignment: .fill) + checkListScrollView.addSubview(contentStack) + contentStack.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + contentStack.topAnchor.constraint(equalTo: contentGuide.topAnchor), + contentStack.bottomAnchor.constraint(equalTo: contentGuide.bottomAnchor, constant: -20), + + contentStack.leftAnchor.constraint(equalTo: frameGuide.leftAnchor, constant: 20), + contentStack.rightAnchor.constraint(equalTo: frameGuide.rightAnchor, constant: 20), + ]) + + let titleLabelStack = VStack([ + titleLabel, + subTitleLabel + ], spacing: 8, alignment: .leading) + + let buttonStack = HStack( + [ + cancelButton, + acceptDeregisterButton + ], + spacing: 8, + alignment: .center, + distribution: .fillEqually + ) + + [ + navigationBar, + titleLabelStack, + checkListScrollView, + 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), + + titleLabelStack.topAnchor.constraint(equalTo: navigationBar.bottomAnchor, constant: 24), + titleLabelStack.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 20), + titleLabelStack.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -20), + + checkListScrollView.topAnchor.constraint(equalTo: titleLabelStack.bottomAnchor), + checkListScrollView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor), + checkListScrollView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor), + checkListScrollView.bottomAnchor.constraint(equalTo: finalWarningLabel.bottomAnchor), + + 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() { + + itemViewList.forEach { itemView in + + itemView + .opTap + .observe(on: MainScheduler.instance) + .map({ [weak self] state in + + switch state { + case .idle(let item): + self?.selectedReasons[item] = false + case .checked(let item): + self?.selectedReasons[item] = true + } + return state + }) + .subscribe(onNext: { [weak self] _ in + guard let self else { return } + let selectCount = selectedReasons.values.filter { $0 }.count + acceptDeregisterButton.setEnabled(selectCount > 0) + }) + .disposed(by: disposeBag) + } + } + + public func bind(viewModel: DeregisterReasonVMable) { + + self.viewModel = viewModel + + acceptDeregisterButton + .rx.tap + .map { [weak self] _ in + let reasons = self?.selectedReasons.filter({ (reason, isActive) in isActive}).map { (key, _) in + key + } + return reasons ?? [] + } + .bind(to: viewModel.acceptDeregisterButonClicked) + .disposed(by: disposeBag) + + Observable + .merge( + cancelButton.rx.tap.asObservable(), + navigationBar.backButton.rx.tap.asObservable() + ) + .bind(to: viewModel.exitButonClicked) + .disposed(by: disposeBag) + } +} + diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/ViewModel/CenterDeregisterReasonsVM.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/ViewModel/CenterDeregisterReasonsVM.swift new file mode 100644 index 00000000..8e068553 --- /dev/null +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/Deregister/ViewModel/CenterDeregisterReasonsVM.swift @@ -0,0 +1,35 @@ +// +// CenterDeregisterReasonsVM.swift +// RootFeature +// +// Created by choijunios on 8/21/24. +// + +import Entity +import RxSwift +import RxCocoa + +public class CenterDeregisterReasonsVM: 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?.showPasswordAuthScreen(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/Root/Sources/Screen/InitialScreen/InitialScreenVC.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/InitialScreen/InitialScreenVC.swift new file mode 100644 index 00000000..b6f52bc5 --- /dev/null +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/InitialScreen/InitialScreenVC.swift @@ -0,0 +1,49 @@ +// +// InitialScreenVC.swift +// RootFeature +// +// Created by choijunios on 8/25/24. +// + +import UIKit +import BaseFeature +import PresentationCore +import RxCocoa +import RxSwift +import Entity +import DSKit + +public class InitialScreenVC: BaseViewController { + + var viewModel: InitialScreenVM? + + // Init + + // View + + // Observable + private let disposeBag = DisposeBag() + + public init() { + super.init(nibName: nil, bundle: nil) + } + + public required init?(coder: NSCoder) { fatalError() } + + private func setAppearance() { + view.backgroundColor = DSColor.gray0.color + } + + private func setLayout() { + + } + + private func setObservable() { + } + + public func bind(viewModel: InitialScreenVM) { + self.viewModel = viewModel + + } +} + diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/InitialScreen/InitialScreenVM.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/InitialScreen/InitialScreenVM.swift new file mode 100644 index 00000000..b6aaab8c --- /dev/null +++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/InitialScreen/InitialScreenVM.swift @@ -0,0 +1,33 @@ +// +// InitialScreenVM.swift +// RootFeature +// +// Created by choijunios on 8/25/24. +// + +import RxSwift +import Foundation +import PresentationCore + +public class InitialScreenVM { + + weak var coordinator: RootCoorinatable? + + let disposeBag = DisposeBag() + + public init(coordinator: RootCoorinatable? = nil) { + self.coordinator = coordinator + + + // MARK: 로그아웃, 회원탈퇴시 + NotificationCenter.default.rx.notification(.popToInitialVC) + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] _ in + + guard let self else { return } + + self.coordinator?.popToRoot() + }) + .disposed(by: disposeBag) + } +} diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/Setting/WorkerSettingCoordinator.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/Setting/WorkerSettingCoordinator.swift deleted file mode 100644 index 8d7d493c..00000000 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/Coordinator/Setting/WorkerSettingCoordinator.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// WorkerSettingCoordinator.swift -// RootFeature -// -// Created by choijunios on 8/19/24. -// - -import UIKit -import PresentationCore - -public class WorkerSettingCoordinator: ChildCoordinator { - - public weak var viewControllerRef: UIViewController? - - public var navigationController: UINavigationController - - public init(navigationController: UINavigationController) { - self.navigationController = navigationController - } - - public func start() { - let vc = TestSettingVC() - - navigationController.pushViewController(vc, animated: false) - } - - public func coordinatorDidFinish() { - - } -} diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/MainBoard/ RecruitmentBoardVC.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/MainBoard/ RecruitmentBoardVC.swift deleted file mode 100644 index be5270f5..00000000 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/MainBoard/ RecruitmentBoardVC.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// RecruitmentBoardVC.swift -// RootFeature -// -// Created by choijunios on 7/25/24. -// - -import UIKit - -public class RecruitmentBoardVC: UIViewController { - - public init() { - super.init(nibName: nil, bundle: nil) - - setAppearacne() - } - - public required init?(coder: NSCoder) { fatalError() } - - private func setAppearacne() { - view.backgroundColor = .white - - let label = UILabel() - label.text = "채용공고 화면" - - label.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(label) - - NSLayoutConstraint.activate([ - label.centerXAnchor.constraint(equalTo: view.centerXAnchor), - label.centerYAnchor.constraint(equalTo: view.centerYAnchor), - ]) - } -} diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/MainBoard/ApplyManagementVC.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/MainBoard/ApplyManagementVC.swift deleted file mode 100644 index 240da75b..00000000 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/MainBoard/ApplyManagementVC.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// ApplyManagementVC.swift -// RootFeature -// -// Created by choijunios on 7/25/24. -// - -import UIKit - -public class ApplyManagementVC: UIViewController { - - public init() { - super.init(nibName: nil, bundle: nil) - - setAppearacne() - } - - public required init?(coder: NSCoder) { fatalError() } - - private func setAppearacne() { - view.backgroundColor = .white - - let label = UILabel() - label.text = "공고 관리 화면" - - label.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(label) - - NSLayoutConstraint.activate([ - label.centerXAnchor.constraint(equalTo: view.centerXAnchor), - label.centerYAnchor.constraint(equalTo: view.centerYAnchor), - ]) - } -} diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/SettingVC.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/SettingVC.swift deleted file mode 100644 index c067e316..00000000 --- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Worker/View/SettingVC.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// SettingVC.swift -// RootFeature -// -// Created by choijunios on 7/25/24. -// - -import UIKit - -public class TestSettingVC: UIViewController { - - public init() { - super.init(nibName: nil, bundle: nil) - - setAppearacne() - } - - public required init?(coder: NSCoder) { fatalError() } - - private func setAppearacne() { - view.backgroundColor = .white - - let label = UILabel() - label.text = "세팅 화면" - - label.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(label) - - NSLayoutConstraint.activate([ - label.centerXAnchor.constraint(equalTo: view.centerXAnchor), - label.centerYAnchor.constraint(equalTo: view.centerYAnchor), - ]) - } -} diff --git a/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/LikedAndApplied/StarredAndAppliedVC.swift b/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/LikedAndApplied/StarredAndAppliedVC.swift index 70c620a3..c0e7857a 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/LikedAndApplied/StarredAndAppliedVC.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/View/RecruitmentPost/LikedAndApplied/StarredAndAppliedVC.swift @@ -12,7 +12,6 @@ import RxCocoa import RxSwift import Entity import DSKit -import CenterFeature public protocol WorkerStaticPostBoardVMable { diff --git a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Profile/WorkerMyProfileViewModel.swift b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Profile/WorkerMyProfileViewModel.swift index 5cb5a7d3..04c4dff9 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Profile/WorkerMyProfileViewModel.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Profile/WorkerMyProfileViewModel.swift @@ -188,12 +188,12 @@ public class WorkerMyProfileViewModel: WorkerProfileEditViewModelable { profileRenderObject = rederingState.asDriver(onErrorRecover: { _ in fatalError() }) } - private func fetchProfileVO() -> Single> { + private func fetchProfileVO() -> Single> { workerProfileUseCase .getProfile(mode: .myProfile) } - public func requestUpload(editObject: WorkerProfileStateObject) -> Single> { + public func requestUpload(editObject: WorkerProfileStateObject) -> Single> { var submitObject: WorkerProfileStateObject = .init() diff --git a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Profile/WorkerProfileViewModel.swift b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Profile/WorkerProfileViewModel.swift index 0a07f9be..51770537 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Profile/WorkerProfileViewModel.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/Profile/WorkerProfileViewModel.swift @@ -106,7 +106,7 @@ public class WorkerProfileViewModel: OtherWorkerProfileViewModelable { profileRenderObject = rederingState.asDriver(onErrorRecover: { _ in fatalError() }) } - private func fetchProfileVO() -> Single> { + private func fetchProfileVO() -> Single> { workerProfileUseCase .getProfile(mode: .otherProfile(id: self.workerId)) } diff --git a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/AppliedPostBoardVM.swift b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/AppliedPostBoardVM.swift index 0cd453e6..e0b4a477 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/AppliedPostBoardVM.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/AppliedPostBoardVM.swift @@ -70,7 +70,7 @@ public class AppliedPostBoardVM: WorkerStaticPostBoardVMable { } - func publishAppliedPostMocks() -> Single> { + func publishAppliedPostMocks() -> Single> { return .just(.success((0..<10).map { _ in .mock })) } } diff --git a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/StarredPostBoardVM.swift b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/StarredPostBoardVM.swift index 4070ff96..ab4c359f 100644 --- a/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/StarredPostBoardVM.swift +++ b/project/Projects/Presentation/Feature/Worker/Sources/ViewModel/RecruitmentPost/StarredPostBoardVM.swift @@ -69,7 +69,7 @@ public class StarredPostBoardVM: WorkerStaticPostBoardVMable { } - func publishStarredPostMocks() -> Single> { + func publishStarredPostMocks() -> Single> { return .just(.success((0..<10).map { _ in .mock })) } } diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Root/RootCoorinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Root/RootCoorinatable.swift new file mode 100644 index 00000000..060707e1 --- /dev/null +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Root/RootCoorinatable.swift @@ -0,0 +1,15 @@ +// +// asd.swift +// PresentationCore +// +// Created by choijunios on 8/25/24. +// + +import Foundation + +public protocol RootCoorinatable: ParentCoordinator { + func auth() + func workerMain() + func centerMain() + func popToRoot() +} diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/CenterSettingScreenCoordinatable.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/CenterSettingScreenCoordinatable.swift new file mode 100644 index 00000000..d1222f61 --- /dev/null +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/CenterSettingScreenCoordinatable.swift @@ -0,0 +1,13 @@ +// +// asd.swift +// PresentationCore +// +// Created by choijunios on 8/25/24. +// + +import Foundation + +public protocol CenterSettingScreenCoordinatable: ParentCoordinator { + /// 시설 관리자 계정을 지우는 작업을 시작합니다. + func startRemoveCenterAccountFlow() +} 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 new file mode 100644 index 00000000..08dc10a2 --- /dev/null +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Interface/Setting/Deregister/DeregisterCoordinatable.swift @@ -0,0 +1,24 @@ +// +// asd.swift +// PresentationCore +// +// Created by choijunios on 8/25/24. +// + +import Foundation +import Entity + +public protocol DeregisterCoordinatable: ParentCoordinator { + + /// 공통: 탈퇴 이유를 선택합니다. + func showSelectReasonScreen() + + /// 공통: 탈퇴를 취소합니다. + func cancelDeregister() + + /// 센터관리자: 마지막으로 비밀번호를 입력합니다. + func showFinalPasswordScreen(reasons: [DeregisterReasonVO]) + + /// 요양보호사: 마지막으로 전화번호를 입력합니다. + func showFinalPhoneAuthScreen(reasons: [DeregisterReasonVO]) +} diff --git a/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Notification/CoordinatingNotifications.swift b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Notification/CoordinatingNotifications.swift new file mode 100644 index 00000000..a10550e3 --- /dev/null +++ b/project/Projects/Presentation/PresentationCore/Sources/ScreenCoordinating/Notification/CoordinatingNotifications.swift @@ -0,0 +1,13 @@ +// +// CoordinatingNotifications.swift +// PresentationCore +// +// Created by choijunios on 8/25/24. +// + +import Foundation + +public extension Notification.Name { + + static let popToInitialVC: Notification.Name = .init("popToInitialVC") +}