diff --git a/SodaLive/Resources/Assets.xcassets/ic_back.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_back.imageset/Contents.json new file mode 100644 index 0000000..c07489e --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_back.imageset/Contents.json @@ -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 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_back.imageset/ic_back.png b/SodaLive/Resources/Assets.xcassets/ic_back.imageset/ic_back.png new file mode 100644 index 0000000..a0d3679 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_back.imageset/ic_back.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_headphones_purple.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_headphones_purple.imageset/Contents.json new file mode 100644 index 0000000..5f15106 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_headphones_purple.imageset/Contents.json @@ -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 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_headphones_purple.imageset/ic_headphones_purple.png b/SodaLive/Resources/Assets.xcassets/ic_headphones_purple.imageset/ic_headphones_purple.png new file mode 100644 index 0000000..d285d38 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_headphones_purple.imageset/ic_headphones_purple.png differ diff --git a/SodaLive/Sources/Common/ApiResponseWithoutData.swift b/SodaLive/Sources/Common/ApiResponseWithoutData.swift new file mode 100644 index 0000000..e21ed53 --- /dev/null +++ b/SodaLive/Sources/Common/ApiResponseWithoutData.swift @@ -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? +} diff --git a/SodaLive/Sources/ContentView.swift b/SodaLive/Sources/ContentView.swift index 35c5288..aa4b2d4 100644 --- a/SodaLive/Sources/ContentView.swift +++ b/SodaLive/Sources/ContentView.swift @@ -23,6 +23,9 @@ struct ContentView: View { case .signUp: SignUpView() + case .findPassword: + FindPasswordView() + default: EmptyView() .frame(width: 0, height: 0, alignment: .topLeading) diff --git a/SodaLive/Sources/User/FindPassword/FindPasswordView.swift b/SodaLive/Sources/User/FindPassword/FindPasswordView.swift new file mode 100644 index 0000000..7ea90f7 --- /dev/null +++ b/SodaLive/Sources/User/FindPassword/FindPasswordView.swift @@ -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() + } +} diff --git a/SodaLive/Sources/User/FindPassword/FindPasswordViewModel.swift b/SodaLive/Sources/User/FindPassword/FindPasswordViewModel.swift new file mode 100644 index 0000000..7fe761e --- /dev/null +++ b/SodaLive/Sources/User/FindPassword/FindPasswordViewModel.swift @@ -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() + + @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) + } +} diff --git a/SodaLive/Sources/User/FindPassword/ForgotPasswordRequest.swift b/SodaLive/Sources/User/FindPassword/ForgotPasswordRequest.swift new file mode 100644 index 0000000..04bf2fe --- /dev/null +++ b/SodaLive/Sources/User/FindPassword/ForgotPasswordRequest.swift @@ -0,0 +1,12 @@ +// +// ForgotPasswordRequest.swift +// SodaLive +// +// Created by klaus on 2023/08/09. +// + +import Foundation + +struct ForgotPasswordRequest: Encodable { + let email: String +} diff --git a/SodaLive/Sources/User/UserApi.swift b/SodaLive/Sources/User/UserApi.swift index a55cfa0..e96dfcd 100644 --- a/SodaLive/Sources/User/UserApi.swift +++ b/SodaLive/Sources/User/UserApi.swift @@ -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: diff --git a/SodaLive/Sources/User/UserRepository.swift b/SodaLive/Sources/User/UserRepository.swift index fe73663..010d402 100644 --- a/SodaLive/Sources/User/UserRepository.swift +++ b/SodaLive/Sources/User/UserRepository.swift @@ -20,4 +20,8 @@ final class UserRepository { func signUp(parameters: [MultipartFormData]) -> AnyPublisher { return api.requestPublisher(.signUp(parameters: parameters)) } + + func findPassword(email: String) -> AnyPublisher { + return api.requestPublisher(.findPassword(request: ForgotPasswordRequest(email: email))) + } }