diff --git a/Projects/Core/Networking/Interface/Endpoint/DotoriRestAPIDomain.swift b/Projects/Core/Networking/Interface/Endpoint/DotoriRestAPIDomain.swift index 5a42fc01..bd8f7797 100644 --- a/Projects/Core/Networking/Interface/Endpoint/DotoriRestAPIDomain.swift +++ b/Projects/Core/Networking/Interface/Endpoint/DotoriRestAPIDomain.swift @@ -6,6 +6,7 @@ import Foundation */ public enum DotoriRestAPIDomain: String { case auth + case user = "members" case selfStudy = "dotori-role/self-study" case massage = "dotori-role/massage" case notice = "dotori-role/board" diff --git a/Projects/Core/Networking/Interface/Auth/AuthEndpoint.swift b/Projects/Domain/AuthDomain/Sources/DataSource/Remote/AuthEndpoint.swift similarity index 69% rename from Projects/Core/Networking/Interface/Auth/AuthEndpoint.swift rename to Projects/Domain/AuthDomain/Sources/DataSource/Remote/AuthEndpoint.swift index 7e29b74d..5a107adc 100644 --- a/Projects/Core/Networking/Interface/Auth/AuthEndpoint.swift +++ b/Projects/Domain/AuthDomain/Sources/DataSource/Remote/AuthEndpoint.swift @@ -1,4 +1,6 @@ +import AuthDomainInterface import Emdpoint +import NetworkingInterface public enum AuthEndpoint { case signin(email: String, password: String) @@ -42,4 +44,18 @@ extension AuthEndpoint: DotoriEndpoint { return .none } } + + public var errorMap: [Int: Error] { + switch self { + case .signin: + return [ + 400: AuthDomainError.invalidPassword, + 409: AuthDomainError.invalidPassword, + 500: AuthDomainError.unknown + ] + + default: + return [:] + } + } } diff --git a/Projects/Domain/AuthDomain/Sources/DataSource/RemoteAuthDataSourceImpl.swift b/Projects/Domain/AuthDomain/Sources/DataSource/Remote/RemoteAuthDataSourceImpl.swift similarity index 100% rename from Projects/Domain/AuthDomain/Sources/DataSource/RemoteAuthDataSourceImpl.swift rename to Projects/Domain/AuthDomain/Sources/DataSource/Remote/RemoteAuthDataSourceImpl.swift diff --git a/Projects/Domain/UserDomain/Interface/DataSource/RemoteUserDataSource.swift b/Projects/Domain/UserDomain/Interface/DataSource/RemoteUserDataSource.swift new file mode 100644 index 00000000..aab6cc98 --- /dev/null +++ b/Projects/Domain/UserDomain/Interface/DataSource/RemoteUserDataSource.swift @@ -0,0 +1,3 @@ +public protocol RemoteUserDataSource { + func withdrawal() async throws +} diff --git a/Projects/Domain/UserDomain/Interface/Repository/UserRepository.swift b/Projects/Domain/UserDomain/Interface/Repository/UserRepository.swift index 2b30f88a..f0e05e6d 100644 --- a/Projects/Domain/UserDomain/Interface/Repository/UserRepository.swift +++ b/Projects/Domain/UserDomain/Interface/Repository/UserRepository.swift @@ -4,4 +4,5 @@ import Foundation public protocol UserRepository { func loadCurrentUserRole() throws -> UserRoleType func logout() + func withdrawal() async throws } diff --git a/Projects/Domain/UserDomain/Interface/UseCase/WithdrawalUseCase.swift b/Projects/Domain/UserDomain/Interface/UseCase/WithdrawalUseCase.swift new file mode 100644 index 00000000..cfae3ce4 --- /dev/null +++ b/Projects/Domain/UserDomain/Interface/UseCase/WithdrawalUseCase.swift @@ -0,0 +1,3 @@ +public protocol WithdrawalUseCase { + func callAsFunction() async throws +} diff --git a/Projects/Domain/UserDomain/Sources/Assembly/UserDomainAssembly.swift b/Projects/Domain/UserDomain/Sources/Assembly/UserDomainAssembly.swift index 38855a83..329dc0ac 100644 --- a/Projects/Domain/UserDomain/Sources/Assembly/UserDomainAssembly.swift +++ b/Projects/Domain/UserDomain/Sources/Assembly/UserDomainAssembly.swift @@ -1,5 +1,6 @@ import JwtStoreInterface import KeyValueStoreInterface +import NetworkingInterface import Swinject import UserDomainInterface @@ -14,8 +15,15 @@ public final class UserDomainAssembly: Assembly { } .inObjectScope(.container) + container.register(RemoteUserDataSource.self) { resolver in + RemoteUserDataSourceImpl(networking: resolver.resolve(Networking.self)!) + } + container.register(UserRepository.self) { resolver in - UserRepositoryImpl(localUserDataSource: resolver.resolve(LocalUserDataSource.self)!) + UserRepositoryImpl( + localUserDataSource: resolver.resolve(LocalUserDataSource.self)!, + remoteUserDataSource: resolver.resolve(RemoteUserDataSource.self)! + ) } .inObjectScope(.container) @@ -26,5 +34,9 @@ public final class UserDomainAssembly: Assembly { container.register(LogoutUseCase.self) { resolver in LogoutUseCaseImpl(userRepository: resolver.resolve(UserRepository.self)!) } + + container.register(WithdrawalUseCase.self) { resolver in + WithdrawalUseCaseImpl(userRepository: resolver.resolve(UserRepository.self)!) + } } } diff --git a/Projects/Domain/UserDomain/Sources/DataSource/LocalUserDataSourceImpl.swift b/Projects/Domain/UserDomain/Sources/DataSource/Local/LocalUserDataSourceImpl.swift similarity index 100% rename from Projects/Domain/UserDomain/Sources/DataSource/LocalUserDataSourceImpl.swift rename to Projects/Domain/UserDomain/Sources/DataSource/Local/LocalUserDataSourceImpl.swift diff --git a/Projects/Domain/UserDomain/Sources/DataSource/Remote/RemoteUserDataSourceImpl.swift b/Projects/Domain/UserDomain/Sources/DataSource/Remote/RemoteUserDataSourceImpl.swift new file mode 100644 index 00000000..59f7b293 --- /dev/null +++ b/Projects/Domain/UserDomain/Sources/DataSource/Remote/RemoteUserDataSourceImpl.swift @@ -0,0 +1,14 @@ +import UserDomainInterface +import NetworkingInterface + +final class RemoteUserDataSourceImpl: RemoteUserDataSource { + private let networking: any Networking + + init(networking: any Networking) { + self.networking = networking + } + + func withdrawal() async throws { + try await networking.request(UserEndpoint.withdrawal) + } +} diff --git a/Projects/Domain/UserDomain/Sources/DataSource/Remote/UserEndpoint.swift b/Projects/Domain/UserDomain/Sources/DataSource/Remote/UserEndpoint.swift new file mode 100644 index 00000000..89ea5a94 --- /dev/null +++ b/Projects/Domain/UserDomain/Sources/DataSource/Remote/UserEndpoint.swift @@ -0,0 +1,40 @@ +import Emdpoint +import NetworkingInterface + +public enum UserEndpoint { + case withdrawal +} + +extension UserEndpoint: DotoriEndpoint { + public var domain: DotoriRestAPIDomain { + .user + } + + public var route: Route { + switch self { + case .withdrawal: + return .delete("/withdrawal") + } + } + + public var task: HTTPTask { + switch self { + default: + return .requestPlain + } + } + + public var jwtTokenType: JwtTokenType { + switch self { + default: + return .accessToken + } + } + + public var errorMap: [Int: Error] { + switch self { + default: + return [:] + } + } +} diff --git a/Projects/Domain/UserDomain/Sources/Repository/UserRepositoryImpl.swift b/Projects/Domain/UserDomain/Sources/Repository/UserRepositoryImpl.swift index 90816c0e..34d2a90e 100644 --- a/Projects/Domain/UserDomain/Sources/Repository/UserRepositoryImpl.swift +++ b/Projects/Domain/UserDomain/Sources/Repository/UserRepositoryImpl.swift @@ -4,9 +4,14 @@ import UserDomainInterface final class UserRepositoryImpl: UserRepository { private let localUserDataSource: any LocalUserDataSource + private let remoteUserDataSource: any RemoteUserDataSource - init(localUserDataSource: any LocalUserDataSource) { + init( + localUserDataSource: any LocalUserDataSource, + remoteUserDataSource: any RemoteUserDataSource + ) { self.localUserDataSource = localUserDataSource + self.remoteUserDataSource = remoteUserDataSource } func loadCurrentUserRole() throws -> UserRoleType { @@ -16,4 +21,8 @@ final class UserRepositoryImpl: UserRepository { func logout() { localUserDataSource.logout() } + + func withdrawal() async throws { + try await remoteUserDataSource.withdrawal() + } } diff --git a/Projects/Domain/UserDomain/Sources/UseCase/WithdrawalUseCaseImpl.swift b/Projects/Domain/UserDomain/Sources/UseCase/WithdrawalUseCaseImpl.swift new file mode 100644 index 00000000..f077858f --- /dev/null +++ b/Projects/Domain/UserDomain/Sources/UseCase/WithdrawalUseCaseImpl.swift @@ -0,0 +1,13 @@ +import UserDomainInterface + +struct WithdrawalUseCaseImpl: WithdrawalUseCase { + private let userRepository: any UserRepository + + init(userRepository: any UserRepository) { + self.userRepository = userRepository + } + + func callAsFunction() async throws { + try await userRepository.withdrawal() + } +} diff --git a/Projects/Domain/UserDomain/Testing/DataSource/RemoteUserDataSourceSpy.swift b/Projects/Domain/UserDomain/Testing/DataSource/RemoteUserDataSourceSpy.swift new file mode 100644 index 00000000..b985e915 --- /dev/null +++ b/Projects/Domain/UserDomain/Testing/DataSource/RemoteUserDataSourceSpy.swift @@ -0,0 +1,9 @@ +import Foundation +import UserDomainInterface + +final class RemoteUserDataSourceSpy: RemoteUserDataSource { + var withdrawalCallCount = 0 + func withdrawal() async throws { + withdrawalCallCount += 1 + } +} diff --git a/Projects/Domain/UserDomain/Testing/Repository/UserRepositorySpy.swift b/Projects/Domain/UserDomain/Testing/Repository/UserRepositorySpy.swift index 08c7d7a5..d12c0d91 100644 --- a/Projects/Domain/UserDomain/Testing/Repository/UserRepositorySpy.swift +++ b/Projects/Domain/UserDomain/Testing/Repository/UserRepositorySpy.swift @@ -14,4 +14,9 @@ final class UserRepositorySpy: UserRepository { func logout() { logoutCallCount += 1 } + + var withdrawalCallCount = 0 + func withdrawal() async throws { + withdrawalCallCount += 1 + } } diff --git a/Projects/Domain/UserDomain/Testing/UseCase/WithdrawalUseCaseSpy.swift b/Projects/Domain/UserDomain/Testing/UseCase/WithdrawalUseCaseSpy.swift new file mode 100644 index 00000000..5b2739a9 --- /dev/null +++ b/Projects/Domain/UserDomain/Testing/UseCase/WithdrawalUseCaseSpy.swift @@ -0,0 +1,8 @@ +import UserDomainInterface + +final class WithdrawalUseCaseSpy: WithdrawalUseCase { + var withdrawalCallCount = 0 + func callAsFunction() async throws { + withdrawalCallCount += 1 + } +} diff --git a/Projects/Domain/UserDomain/Tests/UserRepositoryTest.swift b/Projects/Domain/UserDomain/Tests/UserRepositoryTest.swift index ce01b5dd..3b34bf5f 100644 --- a/Projects/Domain/UserDomain/Tests/UserRepositoryTest.swift +++ b/Projects/Domain/UserDomain/Tests/UserRepositoryTest.swift @@ -4,17 +4,23 @@ import XCTest final class UserRepositoryTests: XCTestCase { var userRepository: UserRepositoryImpl! + var remoteUserDataSourceSpy: RemoteUserDataSourceSpy! var localUserDataSourceSpy: LocalUserDataSourceSpy! override func setUp() { super.setUp() + remoteUserDataSourceSpy = .init() localUserDataSourceSpy = LocalUserDataSourceSpy() - userRepository = UserRepositoryImpl(localUserDataSource: localUserDataSourceSpy) + userRepository = UserRepositoryImpl( + localUserDataSource: localUserDataSourceSpy, + remoteUserDataSource: remoteUserDataSourceSpy + ) } override func tearDown() { userRepository = nil localUserDataSourceSpy = nil + remoteUserDataSourceSpy = nil super.tearDown() } diff --git a/Projects/Feature/HomeFeature/Demo/Sources/AppDelegate.swift b/Projects/Feature/HomeFeature/Demo/Sources/AppDelegate.swift index a669d5a3..3d062bf3 100644 --- a/Projects/Feature/HomeFeature/Demo/Sources/AppDelegate.swift +++ b/Projects/Feature/HomeFeature/Demo/Sources/AppDelegate.swift @@ -34,6 +34,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { let cancelMassageUseCase = CancelMassageUseCaseSpy() let modifyMassagePersonnelUseCase = ModifyMassagePersonnelUseCaseSpy() let logoutUseCase = LogoutUseCaseSpy() + let withdrawalUseCase = WithdrawalUseCaseSpy() let store = HomeStore( repeatableTimer: repeatableTimerStub, fetchSelfStudyInfoUseCase: fetchSelfStudyInfoUseCase, @@ -46,7 +47,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { applyMassageUseCase: applyMassageUseCase, cancelMassageUseCase: cancelMassageUseCase, modifyMassagePersonnelUseCase: modifyMassagePersonnelUseCase, - logoutUseCase: logoutUseCase + logoutUseCase: logoutUseCase, + withdrawalUseCase: withdrawalUseCase ) let viewController = Inject.ViewControllerHost( UINavigationController(rootViewController: HomeViewController(store: store)) diff --git a/Projects/Feature/HomeFeature/Sources/Assembly/HomeAssembly.swift b/Projects/Feature/HomeFeature/Sources/Assembly/HomeAssembly.swift index df5ed404..83dea5dd 100644 --- a/Projects/Feature/HomeFeature/Sources/Assembly/HomeAssembly.swift +++ b/Projects/Feature/HomeFeature/Sources/Assembly/HomeAssembly.swift @@ -26,6 +26,7 @@ public final class HomeAssembly: Assembly { cancelMassageUseCase: resolver.resolve(CancelMassageUseCase.self)!, modifyMassagePersonnelUseCase: resolver.resolve(ModifyMassagePersonnelUseCase.self)!, logoutUseCase: resolver.resolve(LogoutUseCase.self)!, + withdrawalUseCase: resolver.resolve(WithdrawalUseCase.self)!, confirmationDialogFactory: resolver.resolve(ConfirmationDialogFactory.self)!, myViolationListFactory: resolver.resolve(MyViolationListFactory.self)!, inputDialogFactory: resolver.resolve(InputDialogFactory.self)! diff --git a/Projects/Feature/HomeFeature/Sources/Factory/HomeFactoryImpl.swift b/Projects/Feature/HomeFeature/Sources/Factory/HomeFactoryImpl.swift index f9a8ebc9..be7f8970 100644 --- a/Projects/Feature/HomeFeature/Sources/Factory/HomeFactoryImpl.swift +++ b/Projects/Feature/HomeFeature/Sources/Factory/HomeFactoryImpl.swift @@ -21,6 +21,7 @@ struct HomeFactoryImpl: HomeFactory { private let cancelMassageUseCase: any CancelMassageUseCase private let modifyMassagePersonnelUseCase: any ModifyMassagePersonnelUseCase private let logoutUseCase: any LogoutUseCase + private let withdrawalUseCase: any WithdrawalUseCase private let confirmationDialogFactory: any ConfirmationDialogFactory private let myViolationListFactory: any MyViolationListFactory private let inputDialogFactory: any InputDialogFactory @@ -38,6 +39,7 @@ struct HomeFactoryImpl: HomeFactory { cancelMassageUseCase: any CancelMassageUseCase, modifyMassagePersonnelUseCase: any ModifyMassagePersonnelUseCase, logoutUseCase: any LogoutUseCase, + withdrawalUseCase: any WithdrawalUseCase, confirmationDialogFactory: any ConfirmationDialogFactory, myViolationListFactory: any MyViolationListFactory, inputDialogFactory: any InputDialogFactory @@ -54,6 +56,7 @@ struct HomeFactoryImpl: HomeFactory { self.cancelMassageUseCase = cancelMassageUseCase self.modifyMassagePersonnelUseCase = modifyMassagePersonnelUseCase self.logoutUseCase = logoutUseCase + self.withdrawalUseCase = withdrawalUseCase self.confirmationDialogFactory = confirmationDialogFactory self.myViolationListFactory = myViolationListFactory self.inputDialogFactory = inputDialogFactory @@ -72,7 +75,8 @@ struct HomeFactoryImpl: HomeFactory { applyMassageUseCase: applyMassageUseCase, cancelMassageUseCase: cancelMassageUseCase, modifyMassagePersonnelUseCase: modifyMassagePersonnelUseCase, - logoutUseCase: logoutUseCase + logoutUseCase: logoutUseCase, + withdrawalUseCase: withdrawalUseCase ) let homeViewController = HomeViewController(store: homeStore) return HomeMoordinator( diff --git a/Projects/Feature/HomeFeature/Sources/Scene/HomeViewController.swift b/Projects/Feature/HomeFeature/Sources/Scene/HomeViewController.swift index ce740d7e..6ad67ab1 100644 --- a/Projects/Feature/HomeFeature/Sources/Scene/HomeViewController.swift +++ b/Projects/Feature/HomeFeature/Sources/Scene/HomeViewController.swift @@ -27,6 +27,8 @@ final class HomeViewController: BaseStoredViewController { maxApplyCount: 5 ) private let mealCardView = MealCardView() + private let bottomSpacerView = SpacerView(height: 40) + .set(\.backgroundColor, .clear.withAlphaComponent(0.0125)) override func setLayout() { MSGLayout.stackedScrollLayout(view) { @@ -41,7 +43,7 @@ final class HomeViewController: BaseStoredViewController { mealCardView - SpacerView(height: 32) + bottomSpacerView } .margin(.horizontal(20)) } @@ -108,6 +110,15 @@ final class HomeViewController: BaseStoredViewController { .map { Store.Action.refreshMassageButtonDidTap } .sink(receiveValue: store.send(_:)) .store(in: &subscription) + + bottomSpacerView.tapGesturePublisher() + .sink { _ in + guard let url = URL( + string: "https://apps.apple.com/kr/app/%EC%98%A4%EB%8A%98-%EB%AD%90%EC%9E%84/id1629567018" + ) else { return } + UIApplication.shared.open(url) + } + .store(in: &subscription) } override func bindState() { diff --git a/Projects/Feature/HomeFeature/Sources/Scene/Store/HomeStore.swift b/Projects/Feature/HomeFeature/Sources/Scene/Store/HomeStore.swift index 556dce31..21fa22db 100644 --- a/Projects/Feature/HomeFeature/Sources/Scene/Store/HomeStore.swift +++ b/Projects/Feature/HomeFeature/Sources/Scene/Store/HomeStore.swift @@ -35,6 +35,7 @@ final class HomeStore: BaseStore { private let cancelMassageUseCase: any CancelMassageUseCase private let modifyMassagePersonnelUseCase: any ModifyMassagePersonnelUseCase private let logoutUseCase: any LogoutUseCase + private let withdrawalUseCase: any WithdrawalUseCase init( repeatableTimer: any RepeatableTimer, @@ -48,7 +49,8 @@ final class HomeStore: BaseStore { applyMassageUseCase: any ApplyMassageUseCase, cancelMassageUseCase: any CancelMassageUseCase, modifyMassagePersonnelUseCase: any ModifyMassagePersonnelUseCase, - logoutUseCase: any LogoutUseCase + logoutUseCase: any LogoutUseCase, + withdrawalUseCase: any WithdrawalUseCase ) { self.initialState = .init() self.stateSubject = .init(initialState) @@ -64,6 +66,7 @@ final class HomeStore: BaseStore { self.cancelMassageUseCase = cancelMassageUseCase self.modifyMassagePersonnelUseCase = modifyMassagePersonnelUseCase self.logoutUseCase = logoutUseCase + self.withdrawalUseCase = withdrawalUseCase } enum Action: Equatable { @@ -177,6 +180,20 @@ private extension HomeStore { } route.send(confirmationDialogRoutePath) }, + .init(title: L10n.Home.withdrawalTitle, style: .destructive, handler: { [route, withdrawalUseCase] _ in + let confirmationDialogRoutePath = DotoriRoutePath.confirmationDialog( + title: L10n.Home.withdrawalTitle, + description: L10n.Home.reallyWithdrawalTitle + ) { + do { + try await withdrawalUseCase() + route.send(DotoriRoutePath.signin) + } catch { + await DotoriToast.makeToast(text: error.localizedDescription, style: .error) + } + } + route.send(confirmationDialogRoutePath) + }), .init(title: L10n.Global.cancelButtonTitle, style: .cancel) ]) self.route.send(alertPath) diff --git a/Projects/Feature/HomeFeature/Tests/HomeFeatureTest.swift b/Projects/Feature/HomeFeature/Tests/HomeFeatureTest.swift index 5f829fa2..b5519c5b 100644 --- a/Projects/Feature/HomeFeature/Tests/HomeFeatureTest.swift +++ b/Projects/Feature/HomeFeature/Tests/HomeFeatureTest.swift @@ -22,6 +22,7 @@ final class HomeFeatureTests: XCTestCase { var cancelMassageUseCase: CancelMassageUseCaseSpy! var modifyMassagePersonnelUseCase: ModifyMassagePersonnelUseCaseSpy! var logoutUseCase: LogoutUseCaseSpy! + var withdrawalUseCase: WithdrawalUseCaseSpy! var sut: HomeStore! var subscription: Set! @@ -38,6 +39,7 @@ final class HomeFeatureTests: XCTestCase { cancelMassageUseCase = .init() modifyMassagePersonnelUseCase = .init() logoutUseCase = .init() + withdrawalUseCase = .init() sut = .init( repeatableTimer: repeatableTimer, fetchSelfStudyInfoUseCase: fetchSelfStudyInfoUseCase, @@ -50,7 +52,8 @@ final class HomeFeatureTests: XCTestCase { applyMassageUseCase: applyMassageUseCase, cancelMassageUseCase: cancelMassageUseCase, modifyMassagePersonnelUseCase: modifyMassagePersonnelUseCase, - logoutUseCase: logoutUseCase + logoutUseCase: logoutUseCase, + withdrawalUseCase: withdrawalUseCase ) subscription = .init() } @@ -68,6 +71,7 @@ final class HomeFeatureTests: XCTestCase { cancelMassageUseCase = nil modifyMassagePersonnelUseCase = nil logoutUseCase = nil + withdrawalUseCase = nil sut = nil subscription = nil } diff --git a/Projects/UserInterface/Localization/Resources/en.lproj/Home.strings b/Projects/UserInterface/Localization/Resources/en.lproj/Home.strings index 95e222aa..51f7baa3 100644 --- a/Projects/UserInterface/Localization/Resources/en.lproj/Home.strings +++ b/Projects/UserInterface/Localization/Resources/en.lproj/Home.strings @@ -41,3 +41,5 @@ "complete_to_modify_self_study_limit_title" = "Complete to modify SelfStudy limit"; "massage_modify_limit_title" = "Modify Massage limit"; "complete_to_modify_massage_limit_title" = "Complete to modify Massage limit"; +"withdrawal_title" = "Withdrawal"; +"really_withdrawal_title" = "Are you sure you want to withdrawal Dotori?"; diff --git a/Projects/UserInterface/Localization/Resources/ko.lproj/Home.strings b/Projects/UserInterface/Localization/Resources/ko.lproj/Home.strings index b3e73e3b..99feac98 100644 --- a/Projects/UserInterface/Localization/Resources/ko.lproj/Home.strings +++ b/Projects/UserInterface/Localization/Resources/ko.lproj/Home.strings @@ -41,3 +41,5 @@ "complete_to_modify_self_study_limit_title" = "자습 인원 수정 완료"; "massage_modify_limit_title" = "안마의자 인원 수정"; "complete_to_modify_massage_limit_title" = "안마의자 인원 수정 완료"; +"withdrawal_title" = "회원탈퇴"; +"really_withdrawal_title" = "정말로 도토리를 회원탈퇴 하시겠습니까?";