diff --git a/Plugin/UtilityPlugin/ProjectDescriptionHelpers/Dependency+Project.swift b/Plugin/UtilityPlugin/ProjectDescriptionHelpers/Dependency+Project.swift index d1faa462..74afae16 100644 --- a/Plugin/UtilityPlugin/ProjectDescriptionHelpers/Dependency+Project.swift +++ b/Plugin/UtilityPlugin/ProjectDescriptionHelpers/Dependency+Project.swift @@ -12,6 +12,7 @@ public extension TargetDependency { public extension TargetDependency.Project.Features { static let SignupFeature = TargetDependency.feature(name: "SignupFeature") static let HomeFeature = TargetDependency.feature(name: "HomeFeature") + static let RenewalPasswordFeature = TargetDependency.feature(name: "RenewalPasswordFeature") static let MainTabFeature = TargetDependency.feature(name: "MainTabFeature") static let FindIDFeature = TargetDependency.feature(name: "FindIDFeature") static let SigninFeature = TargetDependency.feature(name: "SigninFeature") diff --git a/Projects/App/Sources/Application/DI/AppComponent.swift b/Projects/App/Sources/Application/DI/AppComponent.swift index 7b56d500..21926641 100644 --- a/Projects/App/Sources/Application/DI/AppComponent.swift +++ b/Projects/App/Sources/Application/DI/AppComponent.swift @@ -6,6 +6,7 @@ import SignupFeature import FindIDFeature import SigninFeature +import RenewalPasswordFeature import MainTabFeature import HomeFeature @@ -42,6 +43,15 @@ public extension AppComponent { var idSettingComponent: IDSettingComponent { IDSettingComponent(parent: self) } + var enterInformationComponent: EnterInformationComponent { + EnterInformationComponent(parent: self) + } + var authenticationEmailComponent: AuthenticationEmailComponent { + AuthenticationEmailComponent(parent: self) + } + var changePasswordComponent: ChangePasswordComponent { + ChangePasswordComponent(parent: self) + } var signupProfileImageComponent: SignupProfileImageComponent { SignupProfileImageComponent(parent: self) } diff --git a/Projects/App/Sources/Application/NeedleGenerated.swift b/Projects/App/Sources/Application/NeedleGenerated.swift index 1dd6d310..3a67fbb8 100644 --- a/Projects/App/Sources/Application/NeedleGenerated.swift +++ b/Projects/App/Sources/Application/NeedleGenerated.swift @@ -8,6 +8,7 @@ import KeychainModule import MainTabFeature import NeedleFoundation import NetworkModule +import RenewalPasswordFeature import SigninFeature import SignupFeature import SwiftUI @@ -166,6 +167,54 @@ private class HomeDependency443c4e1871277bd8432aProvider: HomeDependency { private func factory67229cdf0f755562b2b1f47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { return HomeDependency443c4e1871277bd8432aProvider(appComponent: parent1(component) as! AppComponent) } +private class AuthenticationEmailDependency73189eb572618b10e0fbProvider: AuthenticationEmailDependency { + var verifyAuthCodeUseCase: any VerifyAuthCodeUseCase { + return appComponent.verifyAuthCodeUseCase + } + var sendAuthCodeUseCase: any SendAuthCodeUseCase { + return appComponent.sendAuthCodeUseCase + } + var changePasswordComponent: ChangePasswordComponent { + return appComponent.changePasswordComponent + } + private let appComponent: AppComponent + init(appComponent: AppComponent) { + self.appComponent = appComponent + } +} +/// ^->AppComponent->AuthenticationEmailComponent +private func factory8798d0becd9d2870112af47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { + return AuthenticationEmailDependency73189eb572618b10e0fbProvider(appComponent: parent1(component) as! AppComponent) +} +private class ChangePasswordDependency04ab7ced24136c4fb27eProvider: ChangePasswordDependency { + var renewalPasswordUseCase: any RenewalPasswordUseCase { + return appComponent.renewalPasswordUseCase + } + private let appComponent: AppComponent + init(appComponent: AppComponent) { + self.appComponent = appComponent + } +} +/// ^->AppComponent->ChangePasswordComponent +private func factoryab7c4d87dab53e0a51b9f47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { + return ChangePasswordDependency04ab7ced24136c4fb27eProvider(appComponent: parent1(component) as! AppComponent) +} +private class EnterInformationDependency9204f24c784151f429ddProvider: EnterInformationDependency { + var checkAccountIDIsExistUseCase: any CheckAccountIDIsExistUseCase { + return appComponent.checkAccountIDIsExistUseCase + } + var authenticationEmailComponent: AuthenticationEmailComponent { + return appComponent.authenticationEmailComponent + } + private let appComponent: AppComponent + init(appComponent: AppComponent) { + self.appComponent = appComponent + } +} +/// ^->AppComponent->EnterInformationComponent +private func factory359a960501e79e833f64f47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { + return EnterInformationDependency9204f24c784151f429ddProvider(appComponent: parent1(component) as! AppComponent) +} private class FindIDDependencyb481fe947a844cc29913Provider: FindIDDependency { var findIDUseCase: any FindIDUseCase { return appComponent.findIDUseCase @@ -203,6 +252,9 @@ extension AppComponent: Registration { localTable["signupEmailVerifyComponent-SignupEmailVerifyComponent"] = { self.signupEmailVerifyComponent as Any } localTable["signupEmailAuthCodeVerifyComponent-SignupEmailAuthCodeVerifyComponent"] = { self.signupEmailAuthCodeVerifyComponent as Any } localTable["idSettingComponent-IDSettingComponent"] = { self.idSettingComponent as Any } + localTable["enterInformationComponent-EnterInformationComponent"] = { self.enterInformationComponent as Any } + localTable["authenticationEmailComponent-AuthenticationEmailComponent"] = { self.authenticationEmailComponent as Any } + localTable["changePasswordComponent-ChangePasswordComponent"] = { self.changePasswordComponent as Any } localTable["signupProfileImageComponent-SignupProfileImageComponent"] = { self.signupProfileImageComponent as Any } localTable["signupPasswordComponent-SignupPasswordComponent"] = { self.signupPasswordComponent as Any } localTable["signupTermsComponent-SignupTermsComponent"] = { self.signupTermsComponent as Any } @@ -287,6 +339,24 @@ extension HomeComponent: Registration { keyPathToName[\HomeDependency.fetchMealListUseCase] = "fetchMealListUseCase-any FetchMealListUseCase" } } +extension AuthenticationEmailComponent: Registration { + public func registerItems() { + keyPathToName[\AuthenticationEmailDependency.verifyAuthCodeUseCase] = "verifyAuthCodeUseCase-any VerifyAuthCodeUseCase" + keyPathToName[\AuthenticationEmailDependency.sendAuthCodeUseCase] = "sendAuthCodeUseCase-any SendAuthCodeUseCase" + keyPathToName[\AuthenticationEmailDependency.changePasswordComponent] = "changePasswordComponent-ChangePasswordComponent" + } +} +extension ChangePasswordComponent: Registration { + public func registerItems() { + keyPathToName[\ChangePasswordDependency.renewalPasswordUseCase] = "renewalPasswordUseCase-any RenewalPasswordUseCase" + } +} +extension EnterInformationComponent: Registration { + public func registerItems() { + keyPathToName[\EnterInformationDependency.checkAccountIDIsExistUseCase] = "checkAccountIDIsExistUseCase-any CheckAccountIDIsExistUseCase" + keyPathToName[\EnterInformationDependency.authenticationEmailComponent] = "authenticationEmailComponent-AuthenticationEmailComponent" + } +} extension FindIDComponent: Registration { public func registerItems() { keyPathToName[\FindIDDependency.findIDUseCase] = "findIDUseCase-any FindIDUseCase" @@ -321,6 +391,9 @@ private func register1() { registerProviderFactory("^->AppComponent->MainTabComponent", factory1ab5a747ddf21e1393f9f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->SigninComponent", factory2882a056d84a613debccf47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->HomeComponent", factory67229cdf0f755562b2b1f47b58f8f304c97af4d5) + registerProviderFactory("^->AppComponent->AuthenticationEmailComponent", factory8798d0becd9d2870112af47b58f8f304c97af4d5) + registerProviderFactory("^->AppComponent->ChangePasswordComponent", factoryab7c4d87dab53e0a51b9f47b58f8f304c97af4d5) + registerProviderFactory("^->AppComponent->EnterInformationComponent", factory359a960501e79e833f64f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->FindIDComponent", factory8dd2f9e0b545ead35ecaf47b58f8f304c97af4d5) } #endif diff --git a/Projects/Features/RenewalPasswordFeature/Derived/InfoPlists/RenewalPasswordFeature-Info.plist b/Projects/Features/RenewalPasswordFeature/Derived/InfoPlists/RenewalPasswordFeature-Info.plist new file mode 100644 index 00000000..323e5ecf --- /dev/null +++ b/Projects/Features/RenewalPasswordFeature/Derived/InfoPlists/RenewalPasswordFeature-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Projects/Features/RenewalPasswordFeature/Derived/InfoPlists/RenewalPasswordFeatureTests-Info.plist b/Projects/Features/RenewalPasswordFeature/Derived/InfoPlists/RenewalPasswordFeatureTests-Info.plist new file mode 100644 index 00000000..6c40a6cd --- /dev/null +++ b/Projects/Features/RenewalPasswordFeature/Derived/InfoPlists/RenewalPasswordFeatureTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Projects/Features/RenewalPasswordFeature/Project.swift b/Projects/Features/RenewalPasswordFeature/Project.swift new file mode 100644 index 00000000..0ac5bc52 --- /dev/null +++ b/Projects/Features/RenewalPasswordFeature/Project.swift @@ -0,0 +1,10 @@ +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.makeModule( + name: "RenewalPasswordFeature", + product: .staticFramework, + dependencies: [ + .Project.Features.BaseFeature + ] +) \ No newline at end of file diff --git a/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailComponent.swift b/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailComponent.swift new file mode 100644 index 00000000..12baf2dd --- /dev/null +++ b/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailComponent.swift @@ -0,0 +1,23 @@ +import DomainModule +import NeedleFoundation +import SwiftUI + +public protocol AuthenticationEmailDependency: Dependency { + var verifyAuthCodeUseCase: any VerifyAuthCodeUseCase { get } + var sendAuthCodeUseCase: any SendAuthCodeUseCase { get } + var changePasswordComponent: ChangePasswordComponent { get } + +} + +public final class AuthenticationEmailComponent: Component { + public func makeView(authenticationEmailParam: AuthenticationEmailParam) -> some View { + AuthenticationEmailView( + viewModel: .init( + sendAuthCodeUseCase: self.dependency.sendAuthCodeUseCase, + verifyAuthCodeUseCase: self.dependency.verifyAuthCodeUseCase, + authenticationEmailParam: authenticationEmailParam + ), + changePasswordComponent: dependency.changePasswordComponent + ) + } +} diff --git a/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailParam.swift b/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailParam.swift new file mode 100644 index 00000000..fb0307ce --- /dev/null +++ b/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailParam.swift @@ -0,0 +1,17 @@ +import Foundation + +public struct AuthenticationEmailParam: Equatable { + public init( + name: String, + email: String, + id: String + ) { + self.name = name + self.email = email + self.id = id + } + + public let name: String + public let email: String + public let id: String +} diff --git a/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailView.swift b/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailView.swift new file mode 100644 index 00000000..4f4e44cd --- /dev/null +++ b/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailView.swift @@ -0,0 +1,76 @@ +import SwiftUI +import DesignSystem + +struct AuthenticationEmailView: View { + @StateObject var viewModel: AuthenticationEmailViewModel + let changePasswordComponent: ChangePasswordComponent + @Environment(\.dismiss) var dismiss + + init( + viewModel: AuthenticationEmailViewModel, + changePasswordComponent: ChangePasswordComponent + ) { + _viewModel = StateObject(wrappedValue: viewModel) + self.changePasswordComponent = changePasswordComponent + } + + var body: some View { + VStack { + HStack { + VStack(alignment: .leading, spacing: 8) { + Text("DMS") + .dmsFont(.title(.extraLarge), color: .PrimaryVariant.primary) + + Text("이메일 주소 입력") + .dmsFont(.text(.medium), color: .GrayScale.gray6) + } + + Spacer() + } + .padding(.top, 24) + + VStack(spacing: 40) { + DMSPassCodeView(codeCount: 6, text: $viewModel.authCode) + + Text(viewModel.isErrorOcuured ? viewModel.errorMessage : "이메일로 전송된 인증코드 6자리를 입력해주세요.") + .dmsFont(.text(.small), color: viewModel.isErrorOcuured ? .System.error : .GrayScale.gray5) + } + .padding(.top, 60) + + Text(viewModel.timeText) + .dmsFont(.text(.small), color: .PrimaryVariant.primary) + .padding(.top, 10) + + Spacer() + + DMSButton(text: "인증코드 재발송", style: .underline, color: .GrayScale.gray6) { + viewModel.sendEmailAuthCode() + } + + DMSWideButton(text: "인증", color: .PrimaryVariant.primary) { + viewModel.verifyEmailAuthCode() + } + .padding(.top, 32) + .padding(.bottom, 40) + } + .padding(.horizontal, 24) + .onAppear { + UIApplication.shared.hideKeyboard() + viewModel.sendEmailAuthCode() + } + .dmsToast(isShowing: $viewModel.isShowingToast, message: viewModel.toastMessage, style: .success) + .navigate( + to: changePasswordComponent.makeView( + changePasswordParm: .init( + name: viewModel.authenticationEmailParam.name, + email: viewModel.authenticationEmailParam.email, + id: viewModel.authenticationEmailParam.id, + authCode: viewModel.authCode + ) + ), + when: $viewModel.isNavigateChangePassword + ) + .dmsBackButton(dismiss: dismiss) + + } +} diff --git a/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailViewModel.swift b/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailViewModel.swift new file mode 100644 index 00000000..548d4490 --- /dev/null +++ b/Projects/Features/RenewalPasswordFeature/Sources/AuthenticationEmail/AuthenticationEmailViewModel.swift @@ -0,0 +1,68 @@ +import BaseFeature +import Combine +import DomainModule +import ErrorModule +import Foundation + +final class AuthenticationEmailViewModel: BaseViewModel { + @Published var authCode = "" { + didSet { isErrorOcuured = false } + } + @Published var timeRemaining = 180 + @Published var isShowingToast = false + @Published var toastMessage = "" + @Published var isNavigateChangePassword = false + private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + var timeText: String { + timeRemaining % 60 < 10 ? + "\(timeRemaining/60):0\(timeRemaining%60)" : + "\(timeRemaining/60):\(timeRemaining%60)" + } + + private let sendAuthCodeUseCase: any SendAuthCodeUseCase + private let verifyAuthCodeUseCase: any VerifyAuthCodeUseCase + let authenticationEmailParam: AuthenticationEmailParam + + init( + sendAuthCodeUseCase: any SendAuthCodeUseCase, + verifyAuthCodeUseCase: any VerifyAuthCodeUseCase, + authenticationEmailParam: AuthenticationEmailParam + ) { + self.sendAuthCodeUseCase = sendAuthCodeUseCase + self.verifyAuthCodeUseCase = verifyAuthCodeUseCase + self.authenticationEmailParam = authenticationEmailParam + super.init() + + addCancellable( + timer.setFailureType(to: DmsError.self).eraseToAnyPublisher() + ) { [weak self] _ in + guard let self, self.timeRemaining > 0 else { return } + self.timeRemaining -= 1 + } + } + + func sendEmailAuthCode() { + addCancellable( + sendAuthCodeUseCase.execute( + req: .init(email: authenticationEmailParam.email, type: .password) + ) + ) { [weak self] _ in + self?.authCode = "" + self?.timeRemaining = 180 + self?.toastMessage = "입력하신 이메일로 인증번호가 전송되었습니다." + self?.isShowingToast = true + } + } + + func verifyEmailAuthCode() { + addCancellable( + verifyAuthCodeUseCase.execute( + req: .init(email: authenticationEmailParam.email, authCode: authCode, type: .signup) + ) + ) { [weak self] _ in + self?.isNavigateChangePassword = true + } onReceiveError: { [weak self] _ in + self?.authCode = "" + } + } +} diff --git a/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordComponent.swift b/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordComponent.swift new file mode 100644 index 00000000..78280950 --- /dev/null +++ b/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordComponent.swift @@ -0,0 +1,18 @@ +import DomainModule +import NeedleFoundation +import SwiftUI + +public protocol ChangePasswordDependency: Dependency { + var renewalPasswordUseCase: any RenewalPasswordUseCase { get } +} + +public final class ChangePasswordComponent: Component { + public func makeView(changePasswordParm: ChangePasswordParm) -> some View { + ChangePasswordView( + viewModel: .init( + renewalPasswordUseCase: self.dependency.renewalPasswordUseCase, + changePasswordParm: changePasswordParm + ) + ) + } +} diff --git a/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordParm.swift b/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordParm.swift new file mode 100644 index 00000000..b5d8a1e9 --- /dev/null +++ b/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordParm.swift @@ -0,0 +1,21 @@ +import Foundation + +public struct ChangePasswordParm: Equatable { + public init( + name: String, + email: String, + id: String, + authCode: String + ) { + self.name = name + self.email = email + self.id = id + self.authCode = authCode + + } + + public let name: String + public let email: String + public let id: String + public let authCode: String +} diff --git a/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordView.swift b/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordView.swift new file mode 100644 index 00000000..641d077d --- /dev/null +++ b/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordView.swift @@ -0,0 +1,59 @@ +import SwiftUI +import DesignSystem + +struct ChangePasswordView: View { + private enum FocusField { + case password + case passwordCheck + } + + @FocusState private var focusField: FocusField? + @StateObject var viewModel: ChangePasswordViewModel + + public init(viewModel: ChangePasswordViewModel) { + _viewModel = StateObject(wrappedValue: viewModel) + } + + var body: some View { + VStack { + HStack { + VStack(alignment: .leading, spacing: 8) { + Text("DMS") + .dmsFont(.title(.extraLarge), color: .PrimaryVariant.primary) + + Text("비밀번호 설정") + .dmsFont(.text(.medium), color: .GrayScale.gray6) + + Text("비밀번호는 영문, 숫자, 기호를 포함한 8~20자이어야 합니다.") + .dmsFont(.text(.extraSmall), color: .GrayScale.gray5) + } + + Spacer() + } + + VStack(spacing: 60) { + SecureDMSFloatingTextField("새 비밀번호 입력", text: $viewModel.password) { + } + .focused($focusField, equals: .password) + + SecureDMSFloatingTextField("새 비밀번호 확인 ", text: $viewModel.passwordCheck) { + } + .focused($focusField, equals: .passwordCheck) + + } + .padding(.top, 68) + + Spacer() + + DMSWideButton(text: "확인", color: .PrimaryVariant.primary) { + viewModel.renewalPasswordButtonDidTap() + } + .disabled(viewModel.isRenewalPasswordButtonEnabled) + .padding(.bottom, 40) + } + .dmsToast(isShowing: $viewModel.isErrorOcuured, message: viewModel.errorMessage, style: .error) + .dmsBackground() + .ignoresSafeArea(.keyboard, edges: .bottom) + .padding(.horizontal, 24) + } +} diff --git a/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordViewModel.swift b/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordViewModel.swift new file mode 100644 index 00000000..85e083ef --- /dev/null +++ b/Projects/Features/RenewalPasswordFeature/Sources/ChangePassword/ChangePasswordViewModel.swift @@ -0,0 +1,66 @@ +import BaseFeature +import Combine +import DomainModule + +final class ChangePasswordViewModel: BaseViewModel { + @Published var password = "" { + didSet { resettingError() } + } + @Published var passwordCheck = "" { + didSet { resettingError() } + } + @Published var isPasswordRegexError = false + @Published var isPasswordMismatchedError = false + @Published var isDoneAlertShow = false + + var isRenewalPasswordButtonEnabled: Bool { + !password.isEmpty && !passwordCheck.isEmpty + } + + private let renewalPasswordUseCase: any RenewalPasswordUseCase + let changePasswordParm: ChangePasswordParm + + public init( + renewalPasswordUseCase: any RenewalPasswordUseCase, + changePasswordParm: ChangePasswordParm + ) { + self.renewalPasswordUseCase = renewalPasswordUseCase + self.changePasswordParm = changePasswordParm + } + + func renewalPasswordButtonDidTap() { + guard isRenewalPasswordButtonEnabled else { + return + } + + let passwordExpression = "^(?=.*[A-Za-z])(?=.*[0-9])(?=.*[!@#$%^&*()_+=-]).{8,20}" + guard password ~= passwordExpression else { + isPasswordRegexError = true + return + } + + guard password == passwordCheck else { + isPasswordMismatchedError = true + return + } + addCancellable( + renewalPasswordUseCase.execute( + req: .init( + accountID: changePasswordParm.id, + name: changePasswordParm.name, + email: changePasswordParm.email, + authCode: changePasswordParm.authCode, + newPassword: password + ) + ) + ) { [weak self] _ in + self?.isDoneAlertShow = true + } + + } + + func resettingError() { + isPasswordRegexError = false + isPasswordMismatchedError = false + } +} diff --git a/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/BlockEmailView.swift b/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/BlockEmailView.swift new file mode 100644 index 00000000..f6efa804 --- /dev/null +++ b/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/BlockEmailView.swift @@ -0,0 +1,36 @@ +import SwiftUI +import DesignSystem + +struct BlockEmailView: View { + @Binding var email: String + + init( + email: Binding + ) { + _email = email + } + + var body: some View { + ZStack { + Color.GrayScale.gray2 + .ignoresSafeArea() + + HStack { + VStack(alignment: .leading) { + Text("아이디와 일치하는 이메일 입니다") + .dmsFont(.text(.small), color: .GrayScale.gray7) + .multilineTextAlignment(.leading) + + Text(email) + .dmsFont(.text(.small), color: .PrimaryVariant.primary) + .multilineTextAlignment(.leading) + } + Spacer() + + } + .padding(.horizontal, 16) + } + .frame(height: 68) + + } +} diff --git a/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/EnterInformationComponent.swift b/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/EnterInformationComponent.swift new file mode 100644 index 00000000..6e6ecc5a --- /dev/null +++ b/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/EnterInformationComponent.swift @@ -0,0 +1,19 @@ +import DomainModule +import NeedleFoundation +import SwiftUI + +public protocol EnterInformationDependency: Dependency { + var checkAccountIDIsExistUseCase: any CheckAccountIDIsExistUseCase { get } + var authenticationEmailComponent: AuthenticationEmailComponent { get } +} + +public final class EnterInformationComponent: Component { + public func makeView() -> some View { + EnterInformationView( + viewModel: .init( + checkAccountIDIsExistUseCase: self.dependency.checkAccountIDIsExistUseCase + ), + authenticationEmailComponent: dependency.authenticationEmailComponent + ) + } +} diff --git a/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/EnterInformationView.swift b/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/EnterInformationView.swift new file mode 100644 index 00000000..9d92d6af --- /dev/null +++ b/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/EnterInformationView.swift @@ -0,0 +1,97 @@ +import SwiftUI +import DesignSystem + +struct EnterInformationView: View { + private enum FocusField { + case id + case name + case email + } + + @FocusState private var focusField: FocusField? + @StateObject var viewModel: EnterInformationViewModel + let authenticationEmailComponent: AuthenticationEmailComponent + @Environment(\.dismiss) var dismiss + + public init( + viewModel: EnterInformationViewModel, + authenticationEmailComponent: AuthenticationEmailComponent + ) { + _viewModel = StateObject(wrappedValue: viewModel) + self.authenticationEmailComponent = authenticationEmailComponent + } + + var body: some View { + VStack { + HStack { + VStack(alignment: .leading, spacing: 8) { + Text("DMS") + .dmsFont(.title(.extraLarge), color: .PrimaryVariant.primary) + .padding(.top, 28) + + Text("계정 본인인증") + .dmsFont(.text(.medium), color: .GrayScale.gray6) + } + + Spacer() + } + + VStack(spacing: 20) { + DMSFloatingTextField("아이디", text: $viewModel.id) { + focusField = .name + withAnimation(Animation.easeIn(duration: 0.2)) { + viewModel.returnEmailTextField() + } + } + .focused($focusField, equals: .id) + .textContentType(.nickname) + + if viewModel.isShow { + BlockEmailView(email: $viewModel.blockEmail) + } + + } + .padding(.top, 68) + .transition(.slide) + + VStack(spacing: 60) { + DMSFloatingTextField("이름", text: $viewModel.name) { + focusField = .email + } + .focused($focusField, equals: .name) + .textContentType(.name) + + DMSFloatingTextField("이메일", text: $viewModel.email) { + viewModel.nextButtonDidTap() + } + .focused($focusField, equals: .email) + .textContentType(.emailAddress) + } + .padding(.top, 40) + + Spacer() + + DMSWideButton(text: "다음", color: .PrimaryVariant.primary) { + viewModel.nextButtonDidTap() + } + .disabled(!viewModel.isNextButtonEnabled ) + .padding(.bottom, 40) + + } + .navigate( + to: authenticationEmailComponent.makeView( + authenticationEmailParam: .init( + name: viewModel.name, + email: viewModel.email, + id: viewModel.id + ) + ), + when: $viewModel.isNavigateAuthenticationEmail + ) + .dmsBackButton(dismiss: dismiss) + .frame(maxWidth: .infinity) + .ignoresSafeArea(.keyboard, edges: .bottom) + .padding(.horizontal, 24) + + } +} diff --git a/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/EnterInformationViewModel.swift b/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/EnterInformationViewModel.swift new file mode 100644 index 00000000..cb407e40 --- /dev/null +++ b/Projects/Features/RenewalPasswordFeature/Sources/EnterInformation/EnterInformationViewModel.swift @@ -0,0 +1,38 @@ +import BaseFeature +import Combine +import DomainModule + +final class EnterInformationViewModel: BaseViewModel { + + @Published var email = "" + @Published var blockEmail = "082****@naver.com" + @Published var id = "" + @Published var name = "" + @Published var isShow = false + @Published var isNavigateAuthenticationEmail = false + + var isNextButtonEnabled: Bool { + !email.isEmpty && !id.isEmpty && !name.isEmpty + } + + private let checkAccountIDIsExistUseCase: any CheckAccountIDIsExistUseCase + + public init( + checkAccountIDIsExistUseCase: any CheckAccountIDIsExistUseCase + ) { + self.checkAccountIDIsExistUseCase = checkAccountIDIsExistUseCase + } + + func nextButtonDidTap() { + self.isNavigateAuthenticationEmail = true + } + + func returnEmailTextField() { + addCancellable( + checkAccountIDIsExistUseCase.execute(id: id) + ) { [weak self] email in + self?.blockEmail = email + self?.isShow = true + } + } +} diff --git a/Projects/Features/RenewalPasswordFeature/Tests/TargetTests.swift b/Projects/Features/RenewalPasswordFeature/Tests/TargetTests.swift new file mode 100644 index 00000000..b1cf7940 --- /dev/null +++ b/Projects/Features/RenewalPasswordFeature/Tests/TargetTests.swift @@ -0,0 +1,17 @@ +import XCTest + +class TargetTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + XCTAssertEqual("A", "A") + } + +} diff --git a/Projects/Features/RootFeature/Project.swift b/Projects/Features/RootFeature/Project.swift index f3d6cc25..4df3602b 100644 --- a/Projects/Features/RootFeature/Project.swift +++ b/Projects/Features/RootFeature/Project.swift @@ -8,6 +8,7 @@ let project = Project.makeModule( .Project.Features.BaseFeature, .Project.Features.SignupFeature, .Project.Features.FindIDFeature, + .Project.Features.RenewalPasswordFeature, .Project.Features.SigninFeature, .Project.Features.MainTabFeature ]