비밀번호 찾기 페이지
This commit is contained in:
		
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_back.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_back.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "ic_back.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_back.imageset/ic_back.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_back.imageset/ic_back.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 274 B | 
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_headphones_purple.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_headphones_purple.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "ic_headphones_purple.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_headphones_purple.imageset/ic_headphones_purple.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_headphones_purple.imageset/ic_headphones_purple.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.3 KiB | 
							
								
								
									
										14
									
								
								SodaLive/Sources/Common/ApiResponseWithoutData.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								SodaLive/Sources/Common/ApiResponseWithoutData.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| // | ||||
| //  ApiResponseWithoutData.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| struct ApiResponseWithoutData: Decodable { | ||||
|     let success: Bool | ||||
|     let message: String? | ||||
|     let errorProperty: String? | ||||
| } | ||||
| @@ -23,6 +23,9 @@ struct ContentView: View { | ||||
|             case .signUp: | ||||
|                 SignUpView() | ||||
|                  | ||||
|             case .findPassword: | ||||
|                 FindPasswordView() | ||||
|                  | ||||
|             default: | ||||
|                 EmptyView() | ||||
|                     .frame(width: 0, height: 0, alignment: .topLeading) | ||||
|   | ||||
							
								
								
									
										104
									
								
								SodaLive/Sources/User/FindPassword/FindPasswordView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								SodaLive/Sources/User/FindPassword/FindPasswordView.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | ||||
| // | ||||
| //  FindPasswordView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct FindPasswordView: View { | ||||
|      | ||||
|     @StateObject var viewModel = FindPasswordViewModel() | ||||
|     @StateObject var keyboardHandler = KeyboardHandler() | ||||
|      | ||||
|     var body: some View { | ||||
|         BaseView(isLoading: $viewModel.isLoading) { | ||||
|             GeometryReader { proxy in | ||||
|                 VStack(spacing: 0) { | ||||
|                     DetailNavigationBar(title: "비밀번호 재설정") | ||||
|                      | ||||
|                     ScrollView(.vertical, showsIndicators: false) { | ||||
|                         VStack(spacing: 0) { | ||||
|                             Text("회원가입한 이메일 주소로\n임시 비밀번호를 보내드립니다.") | ||||
|                                 .font(.custom(Font.bold.rawValue, size: 16)) | ||||
|                                 .foregroundColor(Color(hex: "eeeeee")) | ||||
|                                 .multilineTextAlignment(.center) | ||||
|                                 .lineSpacing(6) | ||||
|                                 .padding(.top, 40) | ||||
|                                 .padding(.horizontal, 26.7) | ||||
|                              | ||||
|                             Text("임시 비밀번호로 로그인 후, 마이페이지 > 프로필 설정에서\n비밀번호를 변경하고 이용하세요.") | ||||
|                                 .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                                 .foregroundColor(Color(hex: "909090")) | ||||
|                                 .multilineTextAlignment(.center) | ||||
|                                 .lineSpacing(6) | ||||
|                                 .padding(.top, 40) | ||||
|                                 .padding(.horizontal, 26.7) | ||||
|                              | ||||
|                             UserTextField( | ||||
|                                 title: "이메일", | ||||
|                                 hint: "이메일 주소를 입력해 주세요", | ||||
|                                 isSecure: false, | ||||
|                                 variable: $viewModel.email, | ||||
|                                 keyboardType: .emailAddress | ||||
|                             ) | ||||
|                             .padding(.top, 40) | ||||
|                             .padding(.horizontal, 26.7) | ||||
|                              | ||||
|                             Text("임시 비밀번호 받기") | ||||
|                                 .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                                 .foregroundColor(Color.white) | ||||
|                                 .frame(maxWidth: proxy.size.width - 26.7) | ||||
|                                 .padding(.vertical, 16) | ||||
|                                 .background(Color(hex: "9970ff")) | ||||
|                                 .cornerRadius(6.7) | ||||
|                                 .padding(.top, 60) | ||||
|                                 .onTapGesture { viewModel.findPassword() } | ||||
|                              | ||||
|                             HStack(spacing: 13.3) { | ||||
|                                 Image("ic_headphones_purple") | ||||
|                                  | ||||
|                                 Text("고객센터로 문의하기") | ||||
|                                     .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||
|                                     .foregroundColor(Color(hex: "9970ff")) | ||||
|                             } | ||||
|                             .padding(.vertical, 10.7) | ||||
|                             .padding(.horizontal, 18.7) | ||||
|                             .overlay( | ||||
|                                 RoundedRectangle(cornerRadius: 8) | ||||
|                                     .stroke(Color(hex: "9970ff"), lineWidth: 1) | ||||
|                             ) | ||||
|                             .padding(.top, 93) | ||||
|                             .onTapGesture { | ||||
|                                 UIApplication.shared.open(URL(string: "http://pf.kakao.com/_sZaeb")!) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { | ||||
|             HStack { | ||||
|                 Spacer() | ||||
|                 Text(viewModel.errorMessage) | ||||
|                     .padding(.vertical, 13.3) | ||||
|                     .padding(.horizontal, 13.3) | ||||
|                     .frame(width: screenSize().width - 66.7, alignment: .center) | ||||
|                     .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                     .background(Color(hex: "9970ff")) | ||||
|                     .foregroundColor(Color.white) | ||||
|                     .multilineTextAlignment(.leading) | ||||
|                     .cornerRadius(20) | ||||
|                     .padding(.bottom, 66.7) | ||||
|                 Spacer() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct FindPasswordView_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         FindPasswordView() | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,69 @@ | ||||
| // | ||||
| //  FindPasswordViewModel.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import Combine | ||||
|  | ||||
| import Moya | ||||
|  | ||||
| final class FindPasswordViewModel: ObservableObject { | ||||
|     private let repository = UserRepository() | ||||
|     private var subscription = Set<AnyCancellable>() | ||||
|      | ||||
|     @Published var errorMessage = "" | ||||
|     @Published var isShowPopup = false | ||||
|     @Published var isLoading = false | ||||
|      | ||||
|     @Published var email = "" | ||||
|      | ||||
|     func findPassword() { | ||||
|         if email.trimmingCharacters(in: .whitespaces).isEmpty { | ||||
|             errorMessage = "이메일을 입력하세요." | ||||
|             isShowPopup = true | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         isLoading = true | ||||
|         repository.findPassword(email: email) | ||||
|             .sink { result in | ||||
|                 switch result { | ||||
|                 case .finished: | ||||
|                     DEBUG_LOG("finish") | ||||
|                 case .failure(let error): | ||||
|                     ERROR_LOG(error.localizedDescription) | ||||
|                 } | ||||
|             } receiveValue: { 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 { | ||||
|                         self.email = "" | ||||
|                         self.errorMessage = "임시 비밀번호가 입력하신 이메일로 발송되었습니다.\n이메일을 확인해 주세요." | ||||
|                         self.isShowPopup = true | ||||
|                         DispatchQueue.main.asyncAfter(deadline: .now() + 1) { | ||||
|                             AppState.shared.back() | ||||
|                         } | ||||
|                     } 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) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| // | ||||
| //  ForgotPasswordRequest.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| struct ForgotPasswordRequest: Encodable { | ||||
|     let email: String | ||||
| } | ||||
| @@ -11,6 +11,7 @@ import Moya | ||||
| enum UserApi { | ||||
|     case login(request: LoginRequest) | ||||
|     case signUp(parameters: [MultipartFormData]) | ||||
|     case findPassword(request: ForgotPasswordRequest) | ||||
| } | ||||
|  | ||||
| extension UserApi: TargetType { | ||||
| @@ -25,12 +26,15 @@ extension UserApi: TargetType { | ||||
|              | ||||
|         case .signUp: | ||||
|             return "/member/signup" | ||||
|              | ||||
|         case .findPassword: | ||||
|             return "/forgot-password" | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var method: Moya.Method { | ||||
|         switch self { | ||||
|         case .login, .signUp: | ||||
|         case .login, .signUp, .findPassword: | ||||
|             return .post | ||||
|         } | ||||
|     } | ||||
| @@ -42,12 +46,15 @@ extension UserApi: TargetType { | ||||
|              | ||||
|         case .signUp(let parameters): | ||||
|             return .uploadMultipart(parameters) | ||||
|              | ||||
|         case .findPassword(let request): | ||||
|             return .requestJSONEncodable(request) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var headers: [String : String]? { | ||||
|         switch self { | ||||
|         case .login, .signUp: | ||||
|         case .login, .signUp, .findPassword: | ||||
|             return nil | ||||
|              | ||||
|         default: | ||||
|   | ||||
| @@ -20,4 +20,8 @@ final class UserRepository { | ||||
|     func signUp(parameters: [MultipartFormData]) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.signUp(parameters: parameters)) | ||||
|     } | ||||
|      | ||||
|     func findPassword(email: String) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.findPassword(request: ForgotPasswordRequest(email: email))) | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Yu Sung
					Yu Sung