회원탈퇴 페이지 추가
This commit is contained in:
		| @@ -37,4 +37,6 @@ enum AppStep { | |||||||
|     case privacy |     case privacy | ||||||
|      |      | ||||||
|     case notificationSettings |     case notificationSettings | ||||||
|  |      | ||||||
|  |     case signOut | ||||||
| } | } | ||||||
|   | |||||||
| @@ -59,6 +59,9 @@ struct ContentView: View { | |||||||
|             case .notificationSettings: |             case .notificationSettings: | ||||||
|                 NotificationSettingsView() |                 NotificationSettingsView() | ||||||
|                  |                  | ||||||
|  |             case .signOut: | ||||||
|  |                 SignOutView() | ||||||
|  |                  | ||||||
|             default: |             default: | ||||||
|                 EmptyView() |                 EmptyView() | ||||||
|                     .frame(width: 0, height: 0, alignment: .topLeading) |                     .frame(width: 0, height: 0, alignment: .topLeading) | ||||||
|   | |||||||
| @@ -181,6 +181,7 @@ struct SettingsView: View { | |||||||
|                         .underline() |                         .underline() | ||||||
|                         .padding(.vertical, 26.7) |                         .padding(.vertical, 26.7) | ||||||
|                         .onTapGesture { |                         .onTapGesture { | ||||||
|  |                             AppState.shared.setAppStep(step: .signOut) | ||||||
|                         } |                         } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								SodaLive/Sources/Settings/SignOut/SignOutRequest.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								SodaLive/Sources/Settings/SignOut/SignOutRequest.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | // | ||||||
|  | //  SignOutRequest.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 2023/08/10. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
|  | struct SignOutRequest: Encodable { | ||||||
|  |     let reason: String | ||||||
|  |     let password: String | ||||||
|  | } | ||||||
							
								
								
									
										137
									
								
								SodaLive/Sources/Settings/SignOut/SignOutView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								SodaLive/Sources/Settings/SignOut/SignOutView.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | |||||||
|  | // | ||||||
|  | //  SignOutView.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 2023/08/10. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct SignOutView: View { | ||||||
|  |      | ||||||
|  |     @StateObject var viewModel = SignOutViewModel() | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         BaseView(isLoading: $viewModel.isLoading) { | ||||||
|  |             VStack(spacing: 0) { | ||||||
|  |                 DetailNavigationBar(title: "회원탈퇴") | ||||||
|  |                  | ||||||
|  |                 ScrollView(.vertical, showsIndicators: false) { | ||||||
|  |                     VStack(spacing: 0) { | ||||||
|  |                         VStack(spacing: 13.3) { | ||||||
|  |                             Text("정말로 탈퇴하실 거에요?\n한 번 더 생각해보지 않으실래요?") | ||||||
|  |                                 .font(.custom(Font.bold.rawValue, size: 20)) | ||||||
|  |                                 .foregroundColor(Color(hex: "a285eb")) | ||||||
|  |                                 .frame(width: screenSize().width - 26.7, alignment: .leading) | ||||||
|  |                              | ||||||
|  |                             Text("계정을 삭제하려는 이유를 선택해주세요.\n서비스 개선에 중요한 자료로 활용하겠습니다.") | ||||||
|  |                                 .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||||
|  |                                 .foregroundColor(Color(hex: "eeeeee")) | ||||||
|  |                                 .frame(width: screenSize().width - 26.7, alignment: .leading) | ||||||
|  |                              | ||||||
|  |                             VStack(alignment: .leading, spacing: 16.7) { | ||||||
|  |                                 ForEach(0..<viewModel.reasons.count, id: \.self) { index in | ||||||
|  |                                     let reason = viewModel.reasons[index] | ||||||
|  |                                      | ||||||
|  |                                     HStack(spacing: 13.3) { | ||||||
|  |                                         Image( | ||||||
|  |                                             viewModel.reasonSelectedIndex == index ? | ||||||
|  |                                             "btn_radio_select_selected" : | ||||||
|  |                                                 "btn_radio_select_normal" | ||||||
|  |                                         ) | ||||||
|  |                                         .resizable() | ||||||
|  |                                         .frame(width: 20, height: 20) | ||||||
|  |                                          | ||||||
|  |                                         Text(reason) | ||||||
|  |                                             .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||||
|  |                                             .foregroundColor(Color(hex: "eeeeee")) | ||||||
|  |                                          | ||||||
|  |                                         if index == viewModel.reasons.count - 1 { | ||||||
|  |                                             VStack(spacing: 6.7) { | ||||||
|  |                                                 TextField("입력해주세요", text: $viewModel.reason) | ||||||
|  |                                                     .autocapitalization(.none) | ||||||
|  |                                                     .disableAutocorrection(true) | ||||||
|  |                                                     .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||||
|  |                                                     .foregroundColor(Color(hex: "eeeeee")) | ||||||
|  |                                                     .keyboardType(.webSearch) | ||||||
|  |                                                  | ||||||
|  |                                                 Rectangle() | ||||||
|  |                                                     .frame(height: 1) | ||||||
|  |                                                     .foregroundColor(Color(hex: "909090").opacity(0.5)) | ||||||
|  |                                             } | ||||||
|  |                                         } | ||||||
|  |                                     } | ||||||
|  |                                     .onTapGesture { | ||||||
|  |                                         viewModel.reasonSelectedIndex = index | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                             .padding(.top, 13.3) | ||||||
|  |                             .padding(.horizontal, 16.7) | ||||||
|  |                              | ||||||
|  |                             Rectangle() | ||||||
|  |                                 .frame(width: screenSize().width, height: 6.7) | ||||||
|  |                                 .foregroundColor(Color(hex: "232323")) | ||||||
|  |                                 .padding(.top, 20) | ||||||
|  |                              | ||||||
|  |                             Text("계정을 삭제하면 회원님의 모든 콘텐츠와 활동 길고, 코인충전 및 적립, 사용내역 등의 기록이 삭제됩니다. 삭제된 정보는 복구할 수 없으니 신중히 결정해주세요.\n코인 충전하기를 통해 적립한 코인은 계정 삭제시 환불이 불가합니다. 또한 환불 신청 후 환불처리가 되기 전에 계정을 삭제하는 경우 포인트 사용내역을 확인할 수 없어 환불이 불가합니다.") | ||||||
|  |                                 .font(.custom(Font.medium.rawValue, size: 12)) | ||||||
|  |                                 .foregroundColor(Color(hex: "ff5c49")) | ||||||
|  |                                 .fixedSize(horizontal: false, vertical: true) | ||||||
|  |                                 .padding(.horizontal, 26.7) | ||||||
|  |                                 .padding(.top, 16.7) | ||||||
|  |                              | ||||||
|  |                             UserTextField( | ||||||
|  |                                 title: "비밀번호 확인", | ||||||
|  |                                 hint: "비밀번호를 입력해주세요.", | ||||||
|  |                                 isSecure: true, | ||||||
|  |                                 variable: $viewModel.password, | ||||||
|  |                                 keyboardType: .emailAddress | ||||||
|  |                             ) | ||||||
|  |                             .padding(.top, 20) | ||||||
|  |                             .padding(.horizontal, 26.7) | ||||||
|  |                              | ||||||
|  |                             Text("탈퇴하기") | ||||||
|  |                                 .font(.custom(Font.bold.rawValue, size: 15)) | ||||||
|  |                                 .foregroundColor(.white) | ||||||
|  |                                 .padding(.vertical, 16) | ||||||
|  |                                 .frame(width: screenSize().width - 26.7) | ||||||
|  |                                 .background(Color(hex: "9970ff")) | ||||||
|  |                                 .cornerRadius(6.7) | ||||||
|  |                                 .padding(.top, 26.7) | ||||||
|  |                                 .onTapGesture { | ||||||
|  |                                     viewModel.signOut() | ||||||
|  |                                 } | ||||||
|  |                         } | ||||||
|  |                         .padding(.top, 26.7) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { | ||||||
|  |             GeometryReader { geo in | ||||||
|  |                 HStack { | ||||||
|  |                     Spacer() | ||||||
|  |                     Text(viewModel.errorMessage) | ||||||
|  |                         .padding(.vertical, 13.3) | ||||||
|  |                         .padding(.horizontal, 6.7) | ||||||
|  |                         .frame(width: geo.size.width - 66.7, alignment: .center) | ||||||
|  |                         .font(.custom(Font.medium.rawValue, size: 12)) | ||||||
|  |                         .background(Color(hex: "9970ff")) | ||||||
|  |                         .foregroundColor(Color.white) | ||||||
|  |                         .multilineTextAlignment(.leading) | ||||||
|  |                         .fixedSize(horizontal: false, vertical: true) | ||||||
|  |                         .cornerRadius(20) | ||||||
|  |                         .padding(.top, 66.7) | ||||||
|  |                     Spacer() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct SignOutView_Previews: PreviewProvider { | ||||||
|  |     static var previews: some View { | ||||||
|  |         SignOutView() | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										93
									
								
								SodaLive/Sources/Settings/SignOut/SignOutViewModel.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								SodaLive/Sources/Settings/SignOut/SignOutViewModel.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | |||||||
|  | // | ||||||
|  | //  SignOutViewModel.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 2023/08/10. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import Foundation | ||||||
|  | import Combine | ||||||
|  |  | ||||||
|  | final class SignOutViewModel: ObservableObject { | ||||||
|  |      | ||||||
|  |     private let repository = UserRepository() | ||||||
|  |     private var subscription = Set<AnyCancellable>() | ||||||
|  |      | ||||||
|  |     let reasons = [ | ||||||
|  |         "닉네임을 변경하고 싶어서", | ||||||
|  |         "다른 사용자와의 다툼이 있어서", | ||||||
|  |         "이용이 불편하고 장애가 많아서", | ||||||
|  |         "서비스 운영이 마음에 들지 않아서", | ||||||
|  |         "다른 서비스가 더 좋아서", | ||||||
|  |         "삭제하고 싶은 내용이 있어서", | ||||||
|  |         "이용빈도가 낮아서", | ||||||
|  |         "원하는 콘텐츠나 크리에이터가 없어서", | ||||||
|  |         "이용요금이 비싸서", | ||||||
|  |         "기타" | ||||||
|  |     ] | ||||||
|  |      | ||||||
|  |     @Published var reasonSelectedIndex = -1 | ||||||
|  |     @Published var reason = "" | ||||||
|  |     @Published var password = "" | ||||||
|  |      | ||||||
|  |     @Published var errorMessage = "" | ||||||
|  |     @Published var isShowPopup = false | ||||||
|  |     @Published var isLoading = false | ||||||
|  |      | ||||||
|  |     func signOut() { | ||||||
|  |         if validate() { | ||||||
|  |             isLoading = true | ||||||
|  |             let reason = reasonSelectedIndex == reasons.count - 1 ? self.reason : reasons[reasonSelectedIndex] | ||||||
|  |             repository.signOut(reason: reason, password: password) | ||||||
|  |                 .sink { result in | ||||||
|  |                     switch result { | ||||||
|  |                     case .finished: | ||||||
|  |                         DEBUG_LOG("finish") | ||||||
|  |                     case .failure(let error): | ||||||
|  |                         ERROR_LOG(error.localizedDescription) | ||||||
|  |                     } | ||||||
|  |                 } receiveValue: { [unowned self] response in | ||||||
|  |                     self.isLoading = false | ||||||
|  |                     let responseData = response.data | ||||||
|  |                      | ||||||
|  |                     do { | ||||||
|  |                         let jsonDecoder = JSONDecoder() | ||||||
|  |                         let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) | ||||||
|  |                          | ||||||
|  |                         if decoded.success { | ||||||
|  |                             UserDefaults.reset() | ||||||
|  |                             AppState.shared.setAppStep(step: .splash) | ||||||
|  |                         } else { | ||||||
|  |                             if let message = decoded.message { | ||||||
|  |                                 self.errorMessage = message | ||||||
|  |                             } else { | ||||||
|  |                                 self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||||
|  |                             } | ||||||
|  |                              | ||||||
|  |                             self.isShowPopup = true | ||||||
|  |                         } | ||||||
|  |                     } catch { | ||||||
|  |                         self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||||
|  |                         self.isShowPopup = true | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 .store(in: &subscription) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     private func validate() -> Bool { | ||||||
|  |         if reasonSelectedIndex < 0 || (reasonSelectedIndex == reasons.count - 1 && self.reason.trimmingCharacters(in: .whitespaces).isEmpty) { | ||||||
|  |             errorMessage = "계정을 삭제하려는 이유를 선택해 주세요." | ||||||
|  |             isShowPopup = true | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         if password.isEmpty { | ||||||
|  |             errorMessage = "비밀번호를 입력해 주세요." | ||||||
|  |             isShowPopup = true | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         return true | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -18,6 +18,7 @@ enum UserApi { | |||||||
|     case notification(request: UpdateNotificationSettingRequest) |     case notification(request: UpdateNotificationSettingRequest) | ||||||
|     case logout |     case logout | ||||||
|     case logoutAllDevice |     case logoutAllDevice | ||||||
|  |     case signOut(request: SignOutRequest) | ||||||
| } | } | ||||||
|  |  | ||||||
| extension UserApi: TargetType { | extension UserApi: TargetType { | ||||||
| @@ -53,12 +54,15 @@ extension UserApi: TargetType { | |||||||
|              |              | ||||||
|         case .logoutAllDevice: |         case .logoutAllDevice: | ||||||
|             return "/member/logout/all" |             return "/member/logout/all" | ||||||
|  |              | ||||||
|  |         case .signOut: | ||||||
|  |             return "/member/sign_out" | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     var method: Moya.Method { |     var method: Moya.Method { | ||||||
|         switch self { |         switch self { | ||||||
|         case .login, .signUp, .findPassword, .notification, .logout, .logoutAllDevice: |         case .login, .signUp, .findPassword, .notification, .logout, .logoutAllDevice, .signOut: | ||||||
|             return .post |             return .post | ||||||
|              |              | ||||||
|         case .searchUser, .getMypage, .getMemberInfo: |         case .searchUser, .getMypage, .getMemberInfo: | ||||||
| @@ -88,6 +92,9 @@ extension UserApi: TargetType { | |||||||
|              |              | ||||||
|         case .notification(let request): |         case .notification(let request): | ||||||
|             return .requestJSONEncodable(request) |             return .requestJSONEncodable(request) | ||||||
|  |              | ||||||
|  |         case .signOut(let request): | ||||||
|  |             return .requestJSONEncodable(request) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|   | |||||||
| @@ -56,4 +56,8 @@ final class UserRepository { | |||||||
|     func logoutAllDevice() -> AnyPublisher<Response, MoyaError> { |     func logoutAllDevice() -> AnyPublisher<Response, MoyaError> { | ||||||
|         return api.requestPublisher(.logoutAllDevice) |         return api.requestPublisher(.logoutAllDevice) | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |     func signOut(reason: String, password: String) -> AnyPublisher<Response, MoyaError> { | ||||||
|  |         return api.requestPublisher(.signOut(request: SignOutRequest(reason: reason, password: password))) | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Yu Sung
					Yu Sung