회원탈퇴 페이지 추가
This commit is contained in:
		| @@ -37,4 +37,6 @@ enum AppStep { | ||||
|     case privacy | ||||
|      | ||||
|     case notificationSettings | ||||
|      | ||||
|     case signOut | ||||
| } | ||||
|   | ||||
| @@ -59,6 +59,9 @@ struct ContentView: View { | ||||
|             case .notificationSettings: | ||||
|                 NotificationSettingsView() | ||||
|                  | ||||
|             case .signOut: | ||||
|                 SignOutView() | ||||
|                  | ||||
|             default: | ||||
|                 EmptyView() | ||||
|                     .frame(width: 0, height: 0, alignment: .topLeading) | ||||
|   | ||||
| @@ -181,6 +181,7 @@ struct SettingsView: View { | ||||
|                         .underline() | ||||
|                         .padding(.vertical, 26.7) | ||||
|                         .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 logout | ||||
|     case logoutAllDevice | ||||
|     case signOut(request: SignOutRequest) | ||||
| } | ||||
|  | ||||
| extension UserApi: TargetType { | ||||
| @@ -53,12 +54,15 @@ extension UserApi: TargetType { | ||||
|              | ||||
|         case .logoutAllDevice: | ||||
|             return "/member/logout/all" | ||||
|              | ||||
|         case .signOut: | ||||
|             return "/member/sign_out" | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var method: Moya.Method { | ||||
|         switch self { | ||||
|         case .login, .signUp, .findPassword, .notification, .logout, .logoutAllDevice: | ||||
|         case .login, .signUp, .findPassword, .notification, .logout, .logoutAllDevice, .signOut: | ||||
|             return .post | ||||
|              | ||||
|         case .searchUser, .getMypage, .getMemberInfo: | ||||
| @@ -88,6 +92,9 @@ extension UserApi: TargetType { | ||||
|              | ||||
|         case .notification(let request): | ||||
|             return .requestJSONEncodable(request) | ||||
|              | ||||
|         case .signOut(let request): | ||||
|             return .requestJSONEncodable(request) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|   | ||||
| @@ -56,4 +56,8 @@ final class UserRepository { | ||||
|     func logoutAllDevice() -> AnyPublisher<Response, MoyaError> { | ||||
|         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