diff --git a/Projects/App/Sources/Application/DI/Students/AppComponent+Students.swift b/Projects/App/Sources/Application/DI/Students/AppComponent+Students.swift index e4b7dbe6..77fff897 100644 --- a/Projects/App/Sources/Application/DI/Students/AppComponent+Students.swift +++ b/Projects/App/Sources/Application/DI/Students/AppComponent+Students.swift @@ -64,4 +64,10 @@ public extension AppComponent { ChangeProfileImageUseCaseImpl(studentsRepository: studentsRepository) } } + + var withdrawalUseCase: any WithdrawalUseCase { + shared { + WithdrawalUseCaseImpl(studentsRepository: studentsRepository) + } + } } diff --git a/Projects/App/Sources/Application/NeedleGenerated.swift b/Projects/App/Sources/Application/NeedleGenerated.swift index 35f3b4e3..018fe92a 100644 --- a/Projects/App/Sources/Application/NeedleGenerated.swift +++ b/Projects/App/Sources/Application/NeedleGenerated.swift @@ -207,6 +207,9 @@ private class MyPageDependency48d84b530313b3ee40feProvider: MyPageDependency { var logoutUseCase: any LogoutUseCase { return appComponent.logoutUseCase } + var withdrawalUseCase: any WithdrawalUseCase { + return appComponent.withdrawalUseCase + } var changeProfileComponent: ChangeProfileComponent { return appComponent.changeProfileComponent } @@ -541,6 +544,7 @@ extension AppComponent: Registration { localTable["checkExistGradeClassNumberUseCase-any CheckExistGradeClassNumberUseCase"] = { [unowned self] in self.checkExistGradeClassNumberUseCase as Any } localTable["fetchMyProfileUseCase-any FetchMyProfileUseCase"] = { [unowned self] in self.fetchMyProfileUseCase as Any } localTable["changeProfileImageUseCase-any ChangeProfileImageUseCase"] = { [unowned self] in self.changeProfileImageUseCase as Any } + localTable["withdrawalUseCase-any WithdrawalUseCase"] = { [unowned self] in self.withdrawalUseCase as Any } localTable["remoteUsersDataSource-any RemoteUsersDataSource"] = { [unowned self] in self.remoteUsersDataSource as Any } localTable["usersRepository-any UsersRepository"] = { [unowned self] in self.usersRepository as Any } localTable["changePasswordUseCase-any ChangePasswordUseCase"] = { [unowned self] in self.changePasswordUseCase as Any } @@ -634,6 +638,7 @@ extension MyPageComponent: Registration { public func registerItems() { keyPathToName[\MyPageDependency.fetchMyProfileUseCase] = "fetchMyProfileUseCase-any FetchMyProfileUseCase" keyPathToName[\MyPageDependency.logoutUseCase] = "logoutUseCase-any LogoutUseCase" + keyPathToName[\MyPageDependency.withdrawalUseCase] = "withdrawalUseCase-any WithdrawalUseCase" keyPathToName[\MyPageDependency.changeProfileComponent] = "changeProfileComponent-ChangeProfileComponent" keyPathToName[\MyPageDependency.rewardPointDetailComponent] = "rewardPointDetailComponent-RewardPointDetailComponent" keyPathToName[\MyPageDependency.checkPasswordComponent] = "checkPasswordComponent-CheckPasswordComponent" diff --git a/Projects/Features/MyPageFeature/Sources/MyPage/MyPageComponent.swift b/Projects/Features/MyPageFeature/Sources/MyPage/MyPageComponent.swift index 6283aca0..c143579c 100644 --- a/Projects/Features/MyPageFeature/Sources/MyPage/MyPageComponent.swift +++ b/Projects/Features/MyPageFeature/Sources/MyPage/MyPageComponent.swift @@ -5,6 +5,7 @@ import NeedleFoundation public protocol MyPageDependency: Dependency { var fetchMyProfileUseCase: any FetchMyProfileUseCase { get } var logoutUseCase: any LogoutUseCase { get } + var withdrawalUseCase: any WithdrawalUseCase { get } var changeProfileComponent: ChangeProfileComponent { get } var rewardPointDetailComponent: RewardPointDetailComponent { get } var checkPasswordComponent: CheckPasswordComponent { get } @@ -16,7 +17,8 @@ public final class MyPageComponent: Component { MyPageView( viewModel: .init( fetchMyProfileUseCase: self.dependency.fetchMyProfileUseCase, - logoutUseCase: self.dependency.logoutUseCase + logoutUseCase: self.dependency.logoutUseCase, + withdrawalUseCase: self.dependency.withdrawalUseCase ), changeProfileComponent: self.dependency.changeProfileComponent, rewardPointDetailComponent: self.dependency.rewardPointDetailComponent, diff --git a/Projects/Features/MyPageFeature/Sources/MyPage/MyPageView.swift b/Projects/Features/MyPageFeature/Sources/MyPage/MyPageView.swift index 35b95f07..51e9d6c4 100644 --- a/Projects/Features/MyPageFeature/Sources/MyPage/MyPageView.swift +++ b/Projects/Features/MyPageFeature/Sources/MyPage/MyPageView.swift @@ -130,6 +130,19 @@ struct MyPageView: View { .dmsShadow() } + VStack(alignment: .leading, spacing: 0) { + myPageOptionRowCardView(title: "회원 탈퇴") + .dmsFont(.body(.body2), color: .GrayScale.gray6) + .onTapGesture(perform: viewModel.withdrawalButtonDidTap) + .cornerRadius(10) + } + .frame(maxWidth: .infinity) + .background { + RoundedRectangle(cornerRadius: 10) + .fill(Color.GrayScale.gray1) + .dmsShadow(style: .surface) + } + Spacer() } .padding(.horizontal, 24) @@ -145,6 +158,13 @@ struct MyPageView: View { Text("정말 로그아웃 하시겠습니까?") .dmsFont(.body(.body2), color: .GrayScale.gray6) } + .alert("", isPresented: $viewModel.isPresentedWithdrawalAlert) { + Button("취소", role: .cancel) {} + Button("탈퇴", role: .destructive, action: viewModel.confirmWithdrawalButtonDidTap) + } message: { + Text("정말 회원 탈퇴 하시겠습니까?") + .dmsFont(.body(.body2), color: .GrayScale.gray6) + } .onChange(of: viewModel.isNavigateChangeProfile) { newValue in withAnimation { tabbarHidden.wrappedValue = newValue diff --git a/Projects/Features/MyPageFeature/Sources/MyPage/MyPageViewModel.swift b/Projects/Features/MyPageFeature/Sources/MyPage/MyPageViewModel.swift index bcd279aa..df6f7fe6 100644 --- a/Projects/Features/MyPageFeature/Sources/MyPage/MyPageViewModel.swift +++ b/Projects/Features/MyPageFeature/Sources/MyPage/MyPageViewModel.swift @@ -8,17 +8,21 @@ final class MyPageViewModel: BaseViewModel { @Published var isNavigateChangeProfile = false @Published var isNavigateChangePassword = false @Published var isNavigateRewardPointDetail = false + @Published var isPresentedWithdrawalAlert = false @Published var isSuccessLogout = false private let fetchMyProfileUseCase: any FetchMyProfileUseCase private let logoutUseCase: any LogoutUseCase + private let withdrawalUseCase: any WithdrawalUseCase public init( fetchMyProfileUseCase: any FetchMyProfileUseCase, - logoutUseCase: any LogoutUseCase + logoutUseCase: any LogoutUseCase, + withdrawalUseCase: any WithdrawalUseCase ) { self.fetchMyProfileUseCase = fetchMyProfileUseCase self.logoutUseCase = logoutUseCase + self.withdrawalUseCase = withdrawalUseCase } func onAppear() { @@ -41,4 +45,14 @@ final class MyPageViewModel: BaseViewModel { func profileImageDidTap() { isNavigateChangeProfile = true } + + func withdrawalButtonDidTap() { + isPresentedWithdrawalAlert = true + } + + func confirmWithdrawalButtonDidTap() { + addCancellable(withdrawalUseCase.execute()) { [weak self] _ in + self?.isSuccessLogout = true + } + } } diff --git a/Projects/Modules/ErrorModule/Sources/DmsError.swift b/Projects/Modules/ErrorModule/Sources/DmsError.swift index f7b09b15..4686b89b 100644 --- a/Projects/Modules/ErrorModule/Sources/DmsError.swift +++ b/Projects/Modules/ErrorModule/Sources/DmsError.swift @@ -36,6 +36,7 @@ public enum DmsError: Error { // MARK: - Users case currentPasswordMismatch case photoCapacityIsLarge + case failedToWithdrawal // MARK: - StudyRooms case seatIsAlreadyExist @@ -120,6 +121,9 @@ extension DmsError: LocalizedError { case .photoCapacityIsLarge: return "사진의 최대 용량을 초과했습니다." + case .failedToWithdrawal: + return "회원탈퇴에 실패했습니다. 잠시 후 다시 시도해주세요." + // MARK: - StudyRooms case .seatIsAlreadyExist: return "이미 신청된 자리입니다" diff --git a/Projects/Services/APIKit/Sources/StudentsAPI.swift b/Projects/Services/APIKit/Sources/StudentsAPI.swift index e35c6657..327db521 100644 --- a/Projects/Services/APIKit/Sources/StudentsAPI.swift +++ b/Projects/Services/APIKit/Sources/StudentsAPI.swift @@ -12,6 +12,7 @@ public enum StudentsAPI { case checkExistGradeClassNumber(CheckExistGradeClassNumberRequestDTO) case fetchMyProfile case changeProfileImage(url: String) + case withdrawal } extension StudentsAPI: DmsAPI { @@ -44,6 +45,9 @@ extension StudentsAPI: DmsAPI { case .changeProfileImage: return "/profile" + + case .withdrawal: + return "" } } @@ -57,6 +61,9 @@ extension StudentsAPI: DmsAPI { case .renewalPassword, .changeProfileImage: return .patch + + case .withdrawal: + return .delete } } @@ -107,7 +114,7 @@ extension StudentsAPI: DmsAPI { public var jwtTokenType: JwtTokenType { switch self { - case .fetchMyProfile, .changeProfileImage: + case .fetchMyProfile, .changeProfileImage, .withdrawal: return .accessToken default: @@ -173,6 +180,14 @@ extension StudentsAPI: DmsAPI { 401: .tokenExpired, 500: .internalServerError ] + + case .withdrawal: + return [ + 400: .badRequest, + 401: .tokenExpired, + 404: .failedToWithdrawal, + 500: .internalServerError + ] } } diff --git a/Projects/Services/DataModule/Sources/Students/Repositories/Impl/StudentsRepositoryImpl.swift b/Projects/Services/DataModule/Sources/Students/Repositories/Impl/StudentsRepositoryImpl.swift index 77f88b67..a3f12365 100644 --- a/Projects/Services/DataModule/Sources/Students/Repositories/Impl/StudentsRepositoryImpl.swift +++ b/Projects/Services/DataModule/Sources/Students/Repositories/Impl/StudentsRepositoryImpl.swift @@ -44,4 +44,8 @@ public struct StudentsRepositoryImpl: StudentsRepository { public func changeProfileImage(url: String) -> AnyPublisher { remoteStudentsDataSource.changeProfileImage(url: url) } + + public func withdrawal() -> AnyPublisher { + remoteStudentsDataSource.withdrawal() + } } diff --git a/Projects/Services/DataModule/Sources/Students/Repositories/Stub/StudentsRepositoryStub.swift b/Projects/Services/DataModule/Sources/Students/Repositories/Stub/StudentsRepositoryStub.swift index ad4cd695..e6942848 100644 --- a/Projects/Services/DataModule/Sources/Students/Repositories/Stub/StudentsRepositoryStub.swift +++ b/Projects/Services/DataModule/Sources/Students/Repositories/Stub/StudentsRepositoryStub.swift @@ -60,4 +60,10 @@ public struct StudentsRepositoryStub: StudentsRepository { .setFailureType(to: DmsError.self) .eraseToAnyPublisher() } + + public func withdrawal() -> AnyPublisher { + Just(()) + .setFailureType(to: DmsError.self) + .eraseToAnyPublisher() + } } diff --git a/Projects/Services/DataModule/Sources/Students/UseCases/Impl/WithdrawalUseCaseImpl.swift b/Projects/Services/DataModule/Sources/Students/UseCases/Impl/WithdrawalUseCaseImpl.swift new file mode 100644 index 00000000..93f17813 --- /dev/null +++ b/Projects/Services/DataModule/Sources/Students/UseCases/Impl/WithdrawalUseCaseImpl.swift @@ -0,0 +1,15 @@ +import Combine +import DomainModule +import ErrorModule + +public struct WithdrawalUseCaseImpl: WithdrawalUseCase { + private let studentsRepository: any StudentsRepository + + public init(studentsRepository: any StudentsRepository) { + self.studentsRepository = studentsRepository + } + + public func execute() -> AnyPublisher { + studentsRepository.withdrawal() + } +} diff --git a/Projects/Services/DomainModule/Sources/Students/Repository/StudentsRepository.swift b/Projects/Services/DomainModule/Sources/Students/Repository/StudentsRepository.swift index 0d42c147..f560fb9e 100644 --- a/Projects/Services/DomainModule/Sources/Students/Repository/StudentsRepository.swift +++ b/Projects/Services/DomainModule/Sources/Students/Repository/StudentsRepository.swift @@ -11,4 +11,5 @@ public protocol StudentsRepository { func checkExistGradeClassNumber(req: CheckExistGradeClassNumberRequestDTO) -> AnyPublisher func fetchMyProfile() -> AnyPublisher func changeProfileImage(url: String) -> AnyPublisher + func withdrawal() -> AnyPublisher } diff --git a/Projects/Services/DomainModule/Sources/Students/UseCases/WithdrawalUseCase.swift b/Projects/Services/DomainModule/Sources/Students/UseCases/WithdrawalUseCase.swift new file mode 100644 index 00000000..e282a079 --- /dev/null +++ b/Projects/Services/DomainModule/Sources/Students/UseCases/WithdrawalUseCase.swift @@ -0,0 +1,6 @@ +import Combine +import ErrorModule + +public protocol WithdrawalUseCase { + func execute() -> AnyPublisher +} diff --git a/Projects/Services/NetworkModule/Sources/Students/Remote/Impl/RemoteStudentsDataSourceImpl.swift b/Projects/Services/NetworkModule/Sources/Students/Remote/Impl/RemoteStudentsDataSourceImpl.swift index 7bfda7bd..de89276b 100644 --- a/Projects/Services/NetworkModule/Sources/Students/Remote/Impl/RemoteStudentsDataSourceImpl.swift +++ b/Projects/Services/NetworkModule/Sources/Students/Remote/Impl/RemoteStudentsDataSourceImpl.swift @@ -44,4 +44,8 @@ public final class RemoteStudentsDataSourceImpl: BaseRemoteDataSource AnyPublisher { request(.changeProfileImage(url: url)) } + + public func withdrawal() -> AnyPublisher { + request(.withdrawal) + } } diff --git a/Projects/Services/NetworkModule/Sources/Students/Remote/RemoteStudentsDataSource.swift b/Projects/Services/NetworkModule/Sources/Students/Remote/RemoteStudentsDataSource.swift index a24e821e..2a562c11 100644 --- a/Projects/Services/NetworkModule/Sources/Students/Remote/RemoteStudentsDataSource.swift +++ b/Projects/Services/NetworkModule/Sources/Students/Remote/RemoteStudentsDataSource.swift @@ -12,4 +12,5 @@ public protocol RemoteStudentsDataSource { func checkExistGradeClassNumber(req: CheckExistGradeClassNumberRequestDTO) -> AnyPublisher func fetchMyProfile() -> AnyPublisher func changeProfileImage(url: String) -> AnyPublisher + func withdrawal() -> AnyPublisher } diff --git a/Projects/Services/NetworkModule/Sources/Students/Remote/Stub/RemoteStudentsDataSourceStub.swift b/Projects/Services/NetworkModule/Sources/Students/Remote/Stub/RemoteStudentsDataSourceStub.swift index 4d69d17a..bfa581a3 100644 --- a/Projects/Services/NetworkModule/Sources/Students/Remote/Stub/RemoteStudentsDataSourceStub.swift +++ b/Projects/Services/NetworkModule/Sources/Students/Remote/Stub/RemoteStudentsDataSourceStub.swift @@ -59,4 +59,10 @@ public struct RemoteStudentsDataSourceStub: RemoteStudentsDataSource { .setFailureType(to: DmsError.self) .eraseToAnyPublisher() } + + public func withdrawal() -> AnyPublisher { + Just(()) + .setFailureType(to: DmsError.self) + .eraseToAnyPublisher() + } }