From fa94c5447f49d3f851db02295d276e3bdf24e8ac Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Mon, 24 Mar 2025 14:09:13 +0900 Subject: [PATCH] =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20-=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=EC=B0=BD=20UI=20=EC=88=98=EC=A0=95=20-=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EB=8B=A8=EA=B3=84=20?= =?UTF-8?q?=EA=B0=84=EC=86=8C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/User/SignUp/SignUpRequest.swift | 2 - SodaLive/Sources/User/SignUp/SignUpView.swift | 376 +++++------------- .../Sources/User/SignUp/SignUpViewModel.swift | 138 ++----- SodaLive/Sources/User/UserApi.swift | 8 +- SodaLive/Sources/User/UserRepository.swift | 4 +- 5 files changed, 145 insertions(+), 383 deletions(-) diff --git a/SodaLive/Sources/User/SignUp/SignUpRequest.swift b/SodaLive/Sources/User/SignUp/SignUpRequest.swift index 44cb41e..9eee793 100644 --- a/SodaLive/Sources/User/SignUp/SignUpRequest.swift +++ b/SodaLive/Sources/User/SignUp/SignUpRequest.swift @@ -10,8 +10,6 @@ import Foundation struct SignUpRequest: Encodable { let email: String let password: String - let nickname: String - let gender: Gender let marketingPid: String let isAgreeTermsOfService: Bool let isAgreePrivacyPolicy: Bool diff --git a/SodaLive/Sources/User/SignUp/SignUpView.swift b/SodaLive/Sources/User/SignUp/SignUpView.swift index 790018d..9487b80 100644 --- a/SodaLive/Sources/User/SignUp/SignUpView.swift +++ b/SodaLive/Sources/User/SignUp/SignUpView.swift @@ -10,99 +10,127 @@ import SwiftUI struct SignUpView: View { @ObservedObject var viewModel = SignUpViewModel() - @StateObject var keyboardHandler = KeyboardHandler() - - @State private var isShowPhotoPicker = false + @State private var isPasswordVisible: Bool = false var body: some View { BaseView(isLoading: $viewModel.isLoading) { GeometryReader { proxy in VStack(spacing: 0) { - DetailNavigationBar(title: viewModel.step == .step2 ? "프로필 설정" : "회원가입") { - viewModel.prevStep() - } + DetailNavigationBar(title: "회원가입") ScrollView(.vertical, showsIndicators: false) { - VStack(spacing: 13.3) { - if viewModel.step == .step1 { - EmailPasswordView() - - TermsOfServiceAgreeView() - - Text("다음") - .font(.custom(Font.bold.rawValue, size: 18.3)) - .foregroundColor(Color.white) - .padding(.vertical, 16) - .frame(width: screenSize().width - 53.4) - .background(Color.button) - .cornerRadius(10) - .padding(.vertical, 13.7) - .frame(width: screenSize().width - 26.7) - .background(Color.gray22) - .cornerRadius(16.7) - .padding(.top, 13.3) - .onTapGesture { viewModel.nextStep() } - } else { - ZStack { - if let selectedImage = viewModel.profileImage { - Image(uiImage: selectedImage) - .resizable() - .scaledToFill() - .frame(width: 80, height: 80, alignment: .top) - .clipShape(Circle()) + VStack(spacing: 0) { + TextField("이메일", text: $viewModel.email) + .autocapitalization(.none) + .disableAutocorrection(true) + .font(.custom(Font.medium.rawValue, size: 15)) + .foregroundColor(.grayee) + .keyboardType(.emailAddress) + .padding(.vertical, 18) + .padding(.horizontal, 13.3) + .frame(height: 56) + .background(RoundedRectangle(cornerRadius: 6.7).fill(Color.gray33.opacity(0.7))) + .padding(.horizontal, 13.3) + .padding(.top, 13.3) + + HStack { + Group { + if isPasswordVisible { + TextField("비밀번호", text: $viewModel.password) } else { - Image("ic_logo") - .resizable() - .scaledToFill() - .frame(width: 80, height: 80, alignment: .top) - .background(Color(hex: "3e3358")) - .clipShape(Circle()) + SecureField("비밀번호", text: $viewModel.password) + } + } + .autocapitalization(.none) + .disableAutocorrection(true) + .font(.custom(Font.medium.rawValue, size: 15)) + .foregroundColor(.grayee) + + Spacer() + + Image(systemName: isPasswordVisible ? "eye.slash.fill" : "eye.fill") + .foregroundColor(.grayee) + .onTapGesture { + isPasswordVisible.toggle() + } + } + .padding(.vertical, 18) + .padding(.horizontal, 13.3) + .frame(height: 56) + .background(RoundedRectangle(cornerRadius: 6.7).fill(Color.gray33.opacity(0.7))) + .padding(.horizontal, 13.3) + .padding(.top, 16) + + VStack(spacing: 5) { + HStack(spacing: 10) { + Image( + viewModel.isAgreeTerms ? + "btn_select_checked" : + "btn_select_normal" + ) + .padding(10) + .onTapGesture { viewModel.isAgreeTerms.toggle() } + + HStack(spacing: 5) { + Text("이용약관") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color.grayee) + + Text("(필수)") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color.button) + } + .onTapGesture { + AppState.shared.setAppStep(step: .terms) } - Image("ic_camera") - .padding(10) - .background(Color.button) - .cornerRadius(30) - .offset(x: 40, y: 40) - } - .frame(alignment: .bottomTrailing) - .padding(.top, 13.3) - .onTapGesture { - isShowPhotoPicker = true + Spacer() } + .contentShape(Rectangle()) + .onTapGesture { viewModel.isAgreeTerms.toggle() } - VStack(spacing: 16.7) { - UserTextField( - title: "닉네임", - hint: "닉네임을 입력해 주세요", - isSecure: false, - variable: $viewModel.nickname + HStack(spacing: 10) { + Image( + viewModel.isAgreePrivacyPolicy ? + "btn_select_checked" : + "btn_select_normal" ) - .padding(.horizontal, 13.3) + .padding(10) + .onTapGesture { viewModel.isAgreePrivacyPolicy.toggle() } - GenderSelectView() + HStack(spacing: 5) { + Text("개인정보수집 및 이용동의") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color.grayee) + + Text("(필수)") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color.button) + } + .onTapGesture { + AppState.shared.setAppStep(step: .privacy) + } + + Spacer() } - .padding(.vertical, 20) - .frame(width: screenSize().width - 26.7, alignment: .leading) - .background(Color.gray22) - .cornerRadius(6.7) - .padding(.top, 16.7) - - Text("회원가입") - .font(.custom(Font.bold.rawValue, size: 18.3)) - .foregroundColor(Color.white) - .padding(.vertical, 16) - .frame(width: screenSize().width - 53.4) - .background(Color.button) - .cornerRadius(10) - .padding(.vertical, 13.7) - .frame(width: screenSize().width - 26.7) - .background(Color.gray22) - .cornerRadius(16.7) - .padding(.top, 13.3) - .onTapGesture { viewModel.signUp() } + .contentShape(Rectangle()) + .onTapGesture { viewModel.isAgreePrivacyPolicy.toggle() } } + .padding(.top, 20) + .padding(.horizontal, 13.3) + + Text("회원가입") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color.white) + .padding(.vertical, 16) + .frame(maxWidth: .infinity) + .background(Color.button) + .cornerRadius(10) + .padding(.horizontal, 13.3) + .contentShape(Rectangle()) + .onTapGesture { viewModel.signUp() } + .padding(.top, 26.7) Rectangle() .foregroundColor(Color.black) @@ -121,14 +149,6 @@ struct SignUpView: View { hideKeyboard() } } - - if isShowPhotoPicker { - ImagePicker( - isShowing: $isShowPhotoPicker, - selectedImage: $viewModel.profileImage, - sourceType: .photoLibrary - ) - } } .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { GeometryReader { geo in @@ -150,194 +170,6 @@ struct SignUpView: View { } } } - - @ViewBuilder - func EmailPasswordView() -> some View { - VStack(spacing: 26.7) { - UserTextField( - title: "이메일", - hint: "이메일 주소를 입력해 주세요", - isSecure: false, - variable: $viewModel.email, - keyboardType: .emailAddress - ) - .padding(.horizontal, 13.3) - - UserTextField( - title: "비밀번호", - hint: "비밀번호 (영문, 숫자 포함 8~20자)", - isSecure: true, - variable: $viewModel.password, - keyboardType: .emailAddress - ) - .padding(.horizontal, 13.3) - - UserTextField( - title: "비밀번호 확인", - hint: "비밀번호를 다시 입력해 주세요", - isSecure: true, - variable: $viewModel.passwordConfirm, - keyboardType: .emailAddress - ) - .padding(.horizontal, 13.3) - } - .padding(.vertical, 20) - .frame(width: screenSize().width - 26.7) - .background(Color.gray22) - .cornerRadius(6.7) - .padding(.top, 13.3) - } - - @ViewBuilder - func GenderSelectView() -> some View { - VStack(alignment: .leading, spacing: 13.3) { - Text("성별") - .font(.custom(Font.bold.rawValue, size: 12)) - .foregroundColor(Color.grayee) - .padding(.leading, 6.7) - - HStack(spacing: 0) { - HStack(spacing: 13.3) { - Image( - viewModel.gender == .FEMALE ? - "btn_radio_select_selected" : - "btn_radio_select_normal" - ).resizable() - .frame(width: 20, height: 20) - - Text("여자") - .font(.custom(Font.medium.rawValue, size: 13.3)) - .foregroundColor(Color.grayee) - } - .contentShape(Rectangle()) - .onTapGesture { - if viewModel.gender != .FEMALE { - viewModel.gender = .FEMALE - } - } - - Spacer() - - HStack(spacing: 13.3) { - Image( - viewModel.gender == .MALE ? - "btn_radio_select_selected" : - "btn_radio_select_normal" - ).resizable() - .frame(width: 20, height: 20) - - Text("남자") - .font(.custom(Font.medium.rawValue, size: 13.3)) - .foregroundColor(Color.grayee) - } - .contentShape(Rectangle()) - .onTapGesture { - if viewModel.gender != .MALE { - viewModel.gender = .MALE - } - } - - Spacer() - - HStack(spacing: 13.3) { - Image( - viewModel.gender == .NONE ? - "btn_radio_select_selected" : - "btn_radio_select_normal" - ).resizable() - .frame(width: 20, height: 20) - - Text("공개 안 함") - .font(.custom(Font.medium.rawValue, size: 13.3)) - .foregroundColor(Color.grayee) - } - .contentShape(Rectangle()) - .onTapGesture { - if viewModel.gender != .NONE { - viewModel.gender = .NONE - } - } - - Spacer() - } - .padding(.leading, 6.7) - } - .frame(width: screenSize().width - 53.4) - } - - @ViewBuilder - func TermsOfServiceAgreeView() -> some View { - VStack(spacing: 0) { - Text("약관 동의") - .font(.custom(Font.bold.rawValue, size: 12)) - .foregroundColor(Color.grayee) - .frame(width: screenSize().width - 60, height: 50, alignment: .leading) - - Rectangle() - .frame(width: screenSize().width - 53.4, height: 1) - .foregroundColor(Color.gray90) - - HStack(spacing: 6.7) { - Text("이용약관 (필수)") - .font(.custom(Font.medium.rawValue, size: 12)) - .foregroundColor(Color.grayee) - - Text("(필수)") - .font(.custom(Font.medium.rawValue, size: 12)) - .foregroundColor(Color.button) - - Spacer() - - Image( - viewModel.isAgreeTerms ? - "btn_select_checked" : - "btn_select_normal" - ).resizable() - .frame(width: 20, height: 20) - } - .frame(width: screenSize().width - 60, height: 50, alignment: .leading) - .contentShape(Rectangle()) - .onTapGesture { - viewModel.isAgreeTerms.toggle() - } - - Rectangle() - .frame(width: screenSize().width - 53.4, height: 1) - .foregroundColor(Color.gray90) - - HStack(spacing: 6.7) { - Text("개인정보수집 동의") - .font(.custom(Font.medium.rawValue, size: 12)) - .foregroundColor(Color.grayee) - - Text("(필수)") - .font(.custom(Font.medium.rawValue, size: 12)) - .foregroundColor(Color.button) - - Spacer() - - Image( - viewModel.isAgreePrivacyPolicy ? - "btn_select_checked" : - "btn_select_normal" - ).resizable() - .frame(width: 20, height: 20) - } - .frame(width: screenSize().width - 60, height: 50, alignment: .leading) - .contentShape(Rectangle()) - .onTapGesture { - viewModel.isAgreePrivacyPolicy.toggle() - } - - Rectangle() - .frame(width: screenSize().width - 53.4, height: 1) - .foregroundColor(Color.gray90) - } - .padding(.vertical, 13.3) - .frame(width: screenSize().width - 26.7) - .background(Color.gray22) - .cornerRadius(6.7) - } } struct SignUpView_Previews: PreviewProvider { diff --git a/SodaLive/Sources/User/SignUp/SignUpViewModel.swift b/SodaLive/Sources/User/SignUp/SignUpViewModel.swift index 075748a..5349347 100644 --- a/SodaLive/Sources/User/SignUp/SignUpViewModel.swift +++ b/SodaLive/Sources/User/SignUp/SignUpViewModel.swift @@ -19,117 +19,65 @@ final class SignUpViewModel: ObservableObject { @Published var isLoading = false @Published var email = "" - @Published var nickname = "" - @Published var gender: Gender = .NONE - @Published var password = "" - @Published var passwordConfirm = "" - - @Published var profileImage: UIImage? = nil @Published var isAgreeTerms = false @Published var isAgreePrivacyPolicy = false - enum Step { - case step1, step2 - } - - @Published private(set) var step = Step.step1 - - func prevStep() { - if step == .step1 { - AppState.shared.back() - } else { - step = .step1 - } - } - - func nextStep() { - if validateStep1() { - step = .step2 - } - } - func signUp() { - if validateStep2() { + if validate() { isLoading = true let request = SignUpRequest( email: email, password: password, - nickname: nickname, - gender: gender, marketingPid: UserDefaults.string(forKey: .marketingPid), isAgreeTermsOfService: true, isAgreePrivacyPolicy: true ) - var multipartData = [MultipartFormData]() - - let encoder = JSONEncoder() - encoder.outputFormatting = .withoutEscapingSlashes - let jsonData = try? encoder.encode(request) - - if let jsonData = jsonData { - if let profileImage = profileImage, let imageData = profileImage.jpegData(compressionQuality: 0.8) { - multipartData.append( - MultipartFormData( - provider: .data(imageData), - name: "profileImage", - fileName: "\(UUID().uuidString)_\(Date().timeIntervalSince1970 * 1000).jpg", - mimeType: "image/*") - ) - } - - multipartData.append(MultipartFormData(provider: .data(jsonData), name: "request")) - - repository.signUp(parameters: multipartData) - .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(ApiResponse.self, from: responseData) - - if let data = decoded.data, decoded.success { - FirebaseTracking.shared.signUp(method: "email") - UserDefaults.set(data.profileImage, forKey: .profileImage) - UserDefaults.set(data.nickname, forKey: .nickname) - UserDefaults.set(data.userId, forKey: .userId) - UserDefaults.set(data.email, forKey: .email) - UserDefaults.set(data.token, forKey: .token) - AppState.shared.back() + repository.signUp(request: request) + .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(ApiResponse.self, from: responseData) + + if let data = decoded.data, decoded.success { + FirebaseTracking.shared.signUp(method: "email") + UserDefaults.set(data.profileImage, forKey: .profileImage) + UserDefaults.set(data.nickname, forKey: .nickname) + UserDefaults.set(data.userId, forKey: .userId) + UserDefaults.set(data.email, forKey: .email) + UserDefaults.set(data.token, forKey: .token) + AppState.shared.back() + } else { + if let message = decoded.message { + self.errorMessage = message } else { - if let message = decoded.message { - self.errorMessage = message - } else { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." - } - - self.isShowPopup = true + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." } - } catch { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true } - .store(in: &subscription) - } else { - self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." - self.isShowPopup = true - self.isLoading = false - } + } + .store(in: &subscription) } } - private func validateStep1() -> Bool { + private func validate() -> Bool { if email.trimmingCharacters(in: .whitespaces).isEmpty || !validateEmail() { errorMessage = "올바른 이메일을 입력하세요" isShowPopup = true @@ -148,12 +96,6 @@ final class SignUpViewModel: ObservableObject { return false } - if password != passwordConfirm { - errorMessage = "비밀번호가 일치하지 않습니다." - isShowPopup = true - return false - } - if !isAgreeTerms || !isAgreePrivacyPolicy { errorMessage = "약관에 동의하셔야 회원가입이 가능합니다." isShowPopup = true @@ -163,16 +105,6 @@ final class SignUpViewModel: ObservableObject { return true } - func validateStep2() -> Bool { - if nickname.trimmingCharacters(in: .whitespaces).isEmpty || nickname.trimmingCharacters(in: .whitespaces).count < 2 { - errorMessage = "닉네임은 2자 이상 입력해 주세요." - isShowPopup = true - return false - } - - return true - } - private func validateEmail() -> Bool { let emailRegEx = "^.+@([A-Za-z0-9-]+\\.)+[A-Za-z]{2}[A-Za-z]*$" diff --git a/SodaLive/Sources/User/UserApi.swift b/SodaLive/Sources/User/UserApi.swift index 1072b47..6570f92 100644 --- a/SodaLive/Sources/User/UserApi.swift +++ b/SodaLive/Sources/User/UserApi.swift @@ -10,7 +10,7 @@ import Moya enum UserApi { case login(request: LoginRequest) - case signUp(parameters: [MultipartFormData]) + case signUp(request: SignUpRequest) case findPassword(request: ForgotPasswordRequest) case searchUser(nickname: String) case getMypage @@ -48,7 +48,7 @@ extension UserApi: TargetType { return "/member/login" case .signUp: - return "/member/signup" + return "/member/signup/v2" case .findPassword: return "/forgot-password" @@ -137,8 +137,8 @@ extension UserApi: TargetType { case .login(let request): return .requestJSONEncodable(request) - case .signUp(let parameters): - return .uploadMultipart(parameters) + case .signUp(let request): + return .requestJSONEncodable(request) case .findPassword(let request): return .requestJSONEncodable(request) diff --git a/SodaLive/Sources/User/UserRepository.swift b/SodaLive/Sources/User/UserRepository.swift index f0bb420..b8b7c1d 100644 --- a/SodaLive/Sources/User/UserRepository.swift +++ b/SodaLive/Sources/User/UserRepository.swift @@ -17,8 +17,8 @@ final class UserRepository { return api.requestPublisher(.login(request: request)) } - func signUp(parameters: [MultipartFormData]) -> AnyPublisher { - return api.requestPublisher(.signUp(parameters: parameters)) + func signUp(request: SignUpRequest) -> AnyPublisher { + return api.requestPublisher(.signUp(request: request)) } func findPassword(email: String) -> AnyPublisher {