From 592b9b823ea1660768aac8588d1570e26f16e750 Mon Sep 17 00:00:00 2001 From: rlarldud1234 Date: Sun, 4 Dec 2022 19:47:41 +0900 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20::=20TCA=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/Project.swift | 1 + Application/Sources/Application/AppDependency.swift | 13 +++++++++---- Tuist/Dependencies.swift | 5 ++++- .../TargetDependency/SPM.swift | 1 + 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Application/Project.swift b/Application/Project.swift index 86fa0e72..633d3942 100644 --- a/Application/Project.swift +++ b/Application/Project.swift @@ -32,6 +32,7 @@ let project = Project( .Service.UserService, .SPM.FirebaseMessaging, .SPM.SemicolonDesign, + .SPM.ComposableArchitecture, .target(name: "XquareWidget") ] + TargetDependency.universalDependencies, settings: .settings(base: ["OTHER_LDFLAGS": "-ObjC"]) diff --git a/Application/Sources/Application/AppDependency.swift b/Application/Sources/Application/AppDependency.swift index aff95ea5..7a1b25de 100644 --- a/Application/Sources/Application/AppDependency.swift +++ b/Application/Sources/Application/AppDependency.swift @@ -3,9 +3,11 @@ import Foundation import AuthService import MealDataService import UserService +import ComposableArchitecture struct AppDependency { let launchScreenView: LaunchScreenView + let signupView: SignupView } // swiftlint:disable function_body_length @@ -28,8 +30,10 @@ extension AppDependency { let loginViewModel = LoginViewModel( signInUseCase: authServiceDependency.signinUseCase ) - let signupViewModel = SignupViewModel( - signupUseCase: authServiceDependency.signupUseCase + let signupStore = Store( + initialState: SignupState(), + reducer: SignupReducer.reducer, + environment: SignupEnvironment(signupUseCase: authServiceDependency.signupUseCase) ) let launchScreenViewModel = LaunchScreenViewModel( refreshTokenUseCase: authServiceDependency.refreshTokenUseCase @@ -61,7 +65,7 @@ extension AppDependency { mainView: mainView ) let signupView = SignupView( - viewModel: signupViewModel, + store: signupStore, loginView: loginView ) let onboardingView = OnboardingView( @@ -75,7 +79,8 @@ extension AppDependency { ) return AppDependency( - launchScreenView: launchScreenView + launchScreenView: launchScreenView, + signupView: signupView ) } diff --git a/Tuist/Dependencies.swift b/Tuist/Dependencies.swift index 5b030182..2caa6aa8 100644 --- a/Tuist/Dependencies.swift +++ b/Tuist/Dependencies.swift @@ -25,7 +25,10 @@ let dependencies = Dependencies( requirement: .upToNextMajor(from: "0.13.3")), // SemicolonDesign .remote(url: "https://github.com/semicolonDSM/SemicolonDesign_iOS.git", - requirement: .upToNextMajor(from: "1.8.1")) + requirement: .upToNextMajor(from: "1.8.1")), + // TCA + .remote(url: "https://github.com/pointfreeco/swift-composable-architecture", + requirement: .upToNextMajor(from: "0.47.2")) ], baseSettings: Settings.settings( configurations: [ diff --git a/Tuist/ProjectDescriptionHelpers/TargetDependency/SPM.swift b/Tuist/ProjectDescriptionHelpers/TargetDependency/SPM.swift index 399230b4..e8f83caf 100644 --- a/Tuist/ProjectDescriptionHelpers/TargetDependency/SPM.swift +++ b/Tuist/ProjectDescriptionHelpers/TargetDependency/SPM.swift @@ -23,6 +23,7 @@ extension TargetDependency { public static let SQLite = TargetDependency.external(name: "SQLite") public static let SemicolonDesign = TargetDependency.external(name: "SemicolonDesign") + public static let ComposableArchitecture = TargetDependency.external(name: "ComposableArchitecture") } From e279084b71151578b2984aff374e4f5c68452805 Mon Sep 17 00:00:00 2001 From: rlarldud1234 Date: Sun, 4 Dec 2022 19:48:09 +0900 Subject: [PATCH 2/8] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20::=20RxSwift=20to=20Co?= =?UTF-8?q?mbine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Domain/UseCase/SignupUseCase.swift | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Services/AuthService/Sources/Domain/UseCase/SignupUseCase.swift b/Services/AuthService/Sources/Domain/UseCase/SignupUseCase.swift index c235d922..8fde1d40 100644 --- a/Services/AuthService/Sources/Domain/UseCase/SignupUseCase.swift +++ b/Services/AuthService/Sources/Domain/UseCase/SignupUseCase.swift @@ -1,17 +1,28 @@ import Foundation import RxSwift +import Combine +import ComposableArchitecture public class SignupUseCase { private let authRepository: AuthRepository + private var disposeBag = DisposeBag() init(authRepository: AuthRepository) { self.authRepository = authRepository } - public func excute(data: SignupEntity) -> Completable { - self.authRepository.signup(signupEntity: data) + public func excute(data: SignupEntity) -> Future { + return Future { promise in + self.authRepository.signup(signupEntity: data) + .subscribe(onCompleted: { + promise(.success(())) + }, onError: { error in + promise(.failure(error as? AuthServiceError ?? .networkNotWorking)) + }) + .disposed(by: self.disposeBag) + } } } From 2b3baba54ff665d07f6547890bd05185c172a10a Mon Sep 17 00:00:00 2001 From: rlarldud1234 Date: Sun, 4 Dec 2022 19:48:18 +0900 Subject: [PATCH 3/8] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20::=20SignupAction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Scene/Signup/SignupAction.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Application/Sources/Scene/Signup/SignupAction.swift diff --git a/Application/Sources/Scene/Signup/SignupAction.swift b/Application/Sources/Scene/Signup/SignupAction.swift new file mode 100644 index 00000000..98290d4e --- /dev/null +++ b/Application/Sources/Scene/Signup/SignupAction.swift @@ -0,0 +1,16 @@ +import Foundation + +import AuthService +import ComposableArchitecture + +enum SignupAction { + case signup + case signupIsSuccess(Result) + case checkPasswordIsEqual + case checkButtonDisabled + case binding(BindingAction) + case authCodeChanged(String) + case idChanged(String) + case passwordChanged(String) + case reEnterPasswordChange(String) +} From e78fefa77f025640ac62a7c2d168012d0640b401 Mon Sep 17 00:00:00 2001 From: rlarldud1234 Date: Sun, 4 Dec 2022 19:48:30 +0900 Subject: [PATCH 4/8] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20::=20SignupEnvironment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/Sources/Scene/Signup/SignupEnvironment.swift | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 Application/Sources/Scene/Signup/SignupEnvironment.swift diff --git a/Application/Sources/Scene/Signup/SignupEnvironment.swift b/Application/Sources/Scene/Signup/SignupEnvironment.swift new file mode 100644 index 00000000..8407e8f6 --- /dev/null +++ b/Application/Sources/Scene/Signup/SignupEnvironment.swift @@ -0,0 +1,7 @@ +import Foundation + +import AuthService + +struct SignupEnvironment { + let signupUseCase: SignupUseCase +} From a1393eefc629b537a311c0d806d9a2645f8d8ad7 Mon Sep 17 00:00:00 2001 From: rlarldud1234 Date: Sun, 4 Dec 2022 19:48:44 +0900 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20::=20SignupReducer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Scene/Signup/SignupReducer.swift | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 Application/Sources/Scene/Signup/SignupReducer.swift diff --git a/Application/Sources/Scene/Signup/SignupReducer.swift b/Application/Sources/Scene/Signup/SignupReducer.swift new file mode 100644 index 00000000..1daaee39 --- /dev/null +++ b/Application/Sources/Scene/Signup/SignupReducer.swift @@ -0,0 +1,79 @@ +import Foundation + +import AuthService +import ComposableArchitecture + +struct SignupReducer { + + static let reducer = AnyReducer { state, action, environemnt in + switch action { + case .authCodeChanged(let authCode): + state.authCode = authCode + return .none + case .idChanged(let id): + state.id = id + return .none + case .passwordChanged(let password): + state.password = password + return .none + case .reEnterPasswordChange(let password): + state.reEnterPassword = password + return .none + case .signup: + return environemnt.signupUseCase.excute(data: .init( + authCode: state.authCode, + id: state.id, + profileImageUrl: nil, + password: state.password + )) + .catchToEffect(SignupAction.signupIsSuccess) + .eraseToEffect() + case .signupIsSuccess(.success(let success)): + state.isSuccess = true + return .none + case .signupIsSuccess(.failure(let error)): + if error == .duplicateId { + state.isSuccess = false + state.idErrorMessage = "아이디가 중복되었습니다." + } else if error == .networkNotWorking { + state.isInternetNotworking = true + state.isSuccess = false + } + return .none + case .checkPasswordIsEqual: + state.passwordErrorMessage = isPasswordEqual( + state.password, + state.reEnterPassword + ) ? "" : "비밀번호가 일치하지 않습니다." + + return .none + case .checkButtonDisabled: + state.isDisable = !isPasswordEqual( + state.password, + state.reEnterPassword + ) || !checkId(state.id) || !checkAuthCode(state.authCode) || !checkPassword(state.password) + return .none + case .binding: + return .none + } + } + + private static func isPasswordEqual(_ password: String, _ reEnterPassword: String) -> Bool { + return password == reEnterPassword + } + private static func checkId(_ id: String) -> Bool { + let strRegEx = "[A-Za-z0-9]{6,20}" + let pred = NSPredicate(format: "SELF MATCHES %@", strRegEx) + + return pred.evaluate(with: id) + } + private static func checkPassword(_ password: String) -> Bool { + let strRegEx = "^(?=.*[A-Za-z])(?=.*[0-9])(?=.*[!@#$%^&*()_+=-]).{6,30}" + let pred = NSPredicate(format: "SELF MATCHES %@", strRegEx) + + return pred.evaluate(with: password) + } + private static func checkAuthCode(_ authCode: String) -> Bool { + return authCode.count == 6 + } +} From f4c4fe1397b052cdb2fa72271e7e9515646c073d Mon Sep 17 00:00:00 2001 From: rlarldud1234 Date: Sun, 4 Dec 2022 19:48:52 +0900 Subject: [PATCH 6/8] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20::=20SignupState?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/Sources/Scene/Signup/SignupState.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 Application/Sources/Scene/Signup/SignupState.swift diff --git a/Application/Sources/Scene/Signup/SignupState.swift b/Application/Sources/Scene/Signup/SignupState.swift new file mode 100644 index 00000000..762e5894 --- /dev/null +++ b/Application/Sources/Scene/Signup/SignupState.swift @@ -0,0 +1,13 @@ +import Foundation + +struct SignupState: Equatable { + var authCode: String = "" + var id: String = "" + var password: String = "" + var reEnterPassword: String = "" + var idErrorMessage: String = "" + var passwordErrorMessage: String = "" + var isSuccess: Bool = false + var isDisable: Bool = true + var isInternetNotworking: Bool = false +} From 1e4688135b5eefe5c62256ae89580d467ed2eb77 Mon Sep 17 00:00:00 2001 From: rlarldud1234 Date: Sun, 4 Dec 2022 19:49:07 +0900 Subject: [PATCH 7/8] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20::=20viewModel=20->=20?= =?UTF-8?q?Store=EB=A1=9C=20=EB=B3=80=ED=99=98=20=EB=B0=8F=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Scene/Signup/SignupView.swift | 158 ++++++++++-------- 1 file changed, 90 insertions(+), 68 deletions(-) diff --git a/Application/Sources/Scene/Signup/SignupView.swift b/Application/Sources/Scene/Signup/SignupView.swift index 9dc597fd..9514e004 100644 --- a/Application/Sources/Scene/Signup/SignupView.swift +++ b/Application/Sources/Scene/Signup/SignupView.swift @@ -2,86 +2,108 @@ import SwiftUI import SemicolonDesign import AuthService +import ComposableArchitecture struct SignupView: View { - @StateObject var viewModel: SignupViewModel + let store: Store var loginView: LoginView var body: some View { - NavigationView { - ZStack { - ScrollView { - VStack(spacing: 20) { - Spacer() - .frame(height: 16) - SDTextField( - title: "인증코드", - placeholder: "6자리를 입력해주세요", - text: $viewModel.authCode - ) - .padding(.horizontal, 16) - .onChange(of: viewModel.authCode) { _ in - viewModel.checkSignup() - } - SDTextField( - title: "아이디", - placeholder: "영문, 숫자 6~20자", - text: $viewModel.id, - errorMessage: viewModel.idErrorMessage - ) - .padding(.horizontal, 16) - .onChange(of: viewModel.id) { _ in - viewModel.checkSignup() - } - SDTextField( - title: "비밀번호", - placeholder: "숫자, 영문, 특수문자 조합 최소 6자", - text: $viewModel.password, - isSecure: true - ) - .padding(.horizontal, 16) - .onChange(of: viewModel.password) { _ in - viewModel.equalPasswordError() - viewModel.checkSignup() + WithViewStore(self.store) { viewStore in + NavigationView { + ZStack { + ScrollView { + VStack(spacing: 20) { + Spacer() + .frame(height: 16) + SDTextField( + title: "인증코드", + placeholder: "6자리를 입력해주세요", + text: viewStore.binding( + get: \.authCode, + send: SignupAction.authCodeChanged + ) + ) + .padding(.horizontal, 16) + .onChange(of: viewStore.state.authCode) { _ in + viewStore.send(.checkButtonDisabled) + } + SDTextField( + title: "아이디", + placeholder: "영문, 숫자 6~20자", + text: viewStore.binding( + get: \.id, + send: SignupAction.idChanged + ), + errorMessage: viewStore.state.idErrorMessage + ) + .padding(.horizontal, 16) + .onChange(of: viewStore.state.id) { _ in + viewStore.send(.checkButtonDisabled) + } + SDTextField( + title: "비밀번호", + placeholder: "숫자, 영문, 특수문자 조합 최소 6자", + text: viewStore.binding( + get: \.password, + send: SignupAction.passwordChanged + ), + isSecure: true + ) + .padding(.horizontal, 16) + .onChange(of: viewStore.state.password) { _ in + viewStore.send(.checkPasswordIsEqual) + viewStore.send(.checkButtonDisabled) + } + SDTextField( + title: "비밀번호 재입력", + placeholder: "재입력", + text: viewStore.binding( + get: \.reEnterPassword, + send: SignupAction.reEnterPasswordChange + ), + errorMessage: viewStore.state.passwordErrorMessage, + isSecure: true + ) + .padding(.horizontal, 16) + .onChange(of: viewStore.state.reEnterPassword) { _ in + viewStore.send(.checkPasswordIsEqual) + viewStore.send(.checkButtonDisabled) + } + TermsCaptionView() + Spacer().frame(height: 64) } - SDTextField( - title: "비밀번호 재입력", - placeholder: "재입력", - text: $viewModel.reEnterPassword, - errorMessage: viewModel.passwordErrorMessage, - isSecure: true + } + VStack { + Spacer() + FillButton( + isDisabled: viewStore.binding( + get: \.isDisable, + send: SignupAction.checkButtonDisabled + ), + text: "입력 완료", + action: { viewStore.send(.signup) }, + type: .rounded ) - .padding(.horizontal, 16) - .onChange(of: viewModel.reEnterPassword) { _ in - viewModel.equalPasswordError() - viewModel.checkSignup() + .fullScreenCover(isPresented: viewStore.binding( + get: \.isSuccess, + send: SignupAction.signup + )) { + loginView } - TermsCaptionView() - Spacer().frame(height: 64) - } - } - VStack { - Spacer() - FillButton( - isDisabled: $viewModel.isDisabled, - text: "입력 완료", - action: { - viewModel.signup() - }, - type: .rounded - ) - .fullScreenCover(isPresented: $viewModel.isSuccess) { - loginView } } + .sdErrorAlert(isPresented: viewStore.binding( + get: \.isInternetNotworking, + send: SignupAction.signup + ), sdAlert: { + SDErrorAlert(errerMessage: "네트워크가 원할하지 않습니다.") + }) + .navigationTitle("회원가입") + .setNavigationBackButton() } - .sdErrorAlert(isPresented: $viewModel.isInternetNotWorking, sdAlert: { - SDErrorAlert(errerMessage: "네트워크가 원할하지 않습니다.") - }) - .navigationTitle("회원가입") - .setNavigationBackButton() + .accentColor(.GrayScale.gray800) } - .accentColor(.GrayScale.gray800) } } From ee5d18bbb848dc72f232a14696b270271065f86f Mon Sep 17 00:00:00 2001 From: rlarldud1234 Date: Sun, 4 Dec 2022 19:49:18 +0900 Subject: [PATCH 8/8] =?UTF-8?q?=E2=9A=B0=EF=B8=8F=20::=20ViewModel=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Scene/Signup/SignupViewModel.swift | 79 ------------------- 1 file changed, 79 deletions(-) delete mode 100644 Application/Sources/Scene/Signup/SignupViewModel.swift diff --git a/Application/Sources/Scene/Signup/SignupViewModel.swift b/Application/Sources/Scene/Signup/SignupViewModel.swift deleted file mode 100644 index 91eba823..00000000 --- a/Application/Sources/Scene/Signup/SignupViewModel.swift +++ /dev/null @@ -1,79 +0,0 @@ -import Foundation - -import AuthService -import RxSwift - -class SignupViewModel: ObservableObject { - @Published var authCode: String = "" - @Published var id: String = "" - @Published var password: String = "" - @Published var reEnterPassword: String = "" - @Published var passwordErrorMessage: String = "" - @Published var idErrorMessage: String = "" - @Published var isSuccess: Bool = false - @Published var isDisabled: Bool = true - @Published var isInternetNotWorking: Bool = false - - private let signupuseCase: SignupUseCase - - private var disposeBag = DisposeBag() - - init(signupUseCase: SignupUseCase) { - self.signupuseCase = signupUseCase - } - - func signup() { - self.signupuseCase.excute(data: .init( - authCode: authCode, - id: id, - profileImageUrl: nil, - password: password - )) - .subscribe(onCompleted: { [weak self] in - self?.isInternetNotWorking = false - self?.isSuccess = true - }, onError: { [weak self] in - if $0.asAuthServiceError == .duplicateId { - self?.idErrorMessage = "아이디가 중복되었습니다." - } else if $0.asAuthServiceError == .networkNotWorking { - self?.isInternetNotWorking = true - } - self?.isSuccess = false - }) - .disposed(by: self.disposeBag) - } - - func checkSignup() { - self.isDisabled = !isCheckAuthCode() || !isIdCheck() || !isPasswordCheck() || !isReEnterPasswordCheck() - } - - func equalPasswordError() { - if !isReEnterPasswordCheck() { - self.passwordErrorMessage = "비밀번호가 일치하지 않습니다." - } else { - self.passwordErrorMessage = "" - } - } - - private func isCheckAuthCode() -> Bool { - return authCode.count == 6 - } - - private func isIdCheck() -> Bool { - let strRegEx = "[A-Za-z0-9]{6,20}" - let pred = NSPredicate(format: "SELF MATCHES %@", strRegEx) - - return pred.evaluate(with: self.id) - } - - private func isPasswordCheck() -> Bool { - let strRegEx = "^(?=.*[A-Za-z])(?=.*[0-9])(?=.*[!@#$%^&*()_+=-]).{6,30}" - let pred = NSPredicate(format: "SELF MATCHES %@", strRegEx) - - return pred.evaluate(with: self.password) - } - - private func isReEnterPasswordCheck() -> Bool { - return self.password == self.reEnterPassword - } -}