diff --git a/Projects/App/Sources/Application/DI/AppComponent.swift b/Projects/App/Sources/Application/DI/AppComponent.swift index e6499773..2653499b 100644 --- a/Projects/App/Sources/Application/DI/AppComponent.swift +++ b/Projects/App/Sources/Application/DI/AppComponent.swift @@ -30,6 +30,12 @@ public extension AppComponent { var signinComponent: SigninComponent { SigninComponent(parent: self) } + var signupEmailVerifyComponent: SignupEmailVerifyComponent { + SignupEmailVerifyComponent(parent: self) + } + var signupEmailAuthCodeVerifyComponent: SignupEmailAuthCodeVerifyComponent { + SignupEmailAuthCodeVerifyComponent(parent: self) + } } // MARK: - Main diff --git a/Projects/App/Sources/Application/NeedleGenerated.swift b/Projects/App/Sources/Application/NeedleGenerated.swift index 9db8dc3f..0bf8da76 100644 --- a/Projects/App/Sources/Application/NeedleGenerated.swift +++ b/Projects/App/Sources/Application/NeedleGenerated.swift @@ -38,6 +38,38 @@ private class SchoolCodeDependencyc0114744c1c8c7843672Provider: SchoolCodeDepend private func factoryb65c1efbf06b87162473f47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { return SchoolCodeDependencyc0114744c1c8c7843672Provider(appComponent: parent1(component) as! AppComponent) } +private class SignupEmailAuthCodeVerifyDependencyaf9da1ebf0e9e5f1b708Provider: SignupEmailAuthCodeVerifyDependency { + var sendAuthCodeUseCase: any SendAuthCodeUseCase { + return appComponent.sendAuthCodeUseCase + } + var verifyAuthCodeUseCase: any VerifyAuthCodeUseCase { + return appComponent.verifyAuthCodeUseCase + } + private let appComponent: AppComponent + init(appComponent: AppComponent) { + self.appComponent = appComponent + } +} +/// ^->AppComponent->SignupEmailAuthCodeVerifyComponent +private func factoryb06be35aa893adde971bf47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { + return SignupEmailAuthCodeVerifyDependencyaf9da1ebf0e9e5f1b708Provider(appComponent: parent1(component) as! AppComponent) +} +private class SignupEmailVerifyDependencyf9d372ac752ee19b78caProvider: SignupEmailVerifyDependency { + var checkDuplicateEmailUseCase: any CheckDuplicateEmailUseCase { + return appComponent.checkDuplicateEmailUseCase + } + var signupEmailAuthCodeVerifyComponent: SignupEmailAuthCodeVerifyComponent { + return appComponent.signupEmailAuthCodeVerifyComponent + } + private let appComponent: AppComponent + init(appComponent: AppComponent) { + self.appComponent = appComponent + } +} +/// ^->AppComponent->SignupEmailVerifyComponent +private func factory3b1904c76335d70151ebf47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { + return SignupEmailVerifyDependencyf9d372ac752ee19b78caProvider(appComponent: parent1(component) as! AppComponent) +} private class MainTabDependency2826cdb310ed0b17a725Provider: MainTabDependency { var homeComponent: HomeComponent { return appComponent.homeComponent @@ -110,6 +142,8 @@ extension AppComponent: Registration { localTable["schoolCodeComponent-SchoolCodeComponent"] = { self.schoolCodeComponent as Any } localTable["findIDComponent-FindIDComponent"] = { self.findIDComponent as Any } localTable["signinComponent-SigninComponent"] = { self.signinComponent as Any } + localTable["signupEmailVerifyComponent-SignupEmailVerifyComponent"] = { self.signupEmailVerifyComponent as Any } + localTable["signupEmailAuthCodeVerifyComponent-SignupEmailAuthCodeVerifyComponent"] = { self.signupEmailAuthCodeVerifyComponent as Any } localTable["mainTabComponent-MainTabComponent"] = { self.mainTabComponent as Any } localTable["homeComponent-HomeComponent"] = { self.homeComponent as Any } localTable["remoteStudentsDataSource-any RemoteStudentsDataSource"] = { self.remoteStudentsDataSource as Any } @@ -136,6 +170,18 @@ extension SchoolCodeComponent: Registration { keyPathToName[\SchoolCodeDependency.checkSchoolCodeUseCase] = "checkSchoolCodeUseCase-any CheckSchoolCodeUseCase" } } +extension SignupEmailAuthCodeVerifyComponent: Registration { + public func registerItems() { + keyPathToName[\SignupEmailAuthCodeVerifyDependency.sendAuthCodeUseCase] = "sendAuthCodeUseCase-any SendAuthCodeUseCase" + keyPathToName[\SignupEmailAuthCodeVerifyDependency.verifyAuthCodeUseCase] = "verifyAuthCodeUseCase-any VerifyAuthCodeUseCase" + } +} +extension SignupEmailVerifyComponent: Registration { + public func registerItems() { + keyPathToName[\SignupEmailVerifyDependency.checkDuplicateEmailUseCase] = "checkDuplicateEmailUseCase-any CheckDuplicateEmailUseCase" + keyPathToName[\SignupEmailVerifyDependency.signupEmailAuthCodeVerifyComponent] = "signupEmailAuthCodeVerifyComponent-SignupEmailAuthCodeVerifyComponent" + } +} extension MainTabComponent: Registration { public func registerItems() { keyPathToName[\MainTabDependency.homeComponent] = "homeComponent-HomeComponent" @@ -175,6 +221,8 @@ private func registerProviderFactory(_ componentPath: String, _ factory: @escapi private func register1() { registerProviderFactory("^->AppComponent", factoryEmptyDependencyProvider) registerProviderFactory("^->AppComponent->SchoolCodeComponent", factoryb65c1efbf06b87162473f47b58f8f304c97af4d5) + registerProviderFactory("^->AppComponent->SignupEmailAuthCodeVerifyComponent", factoryb06be35aa893adde971bf47b58f8f304c97af4d5) + registerProviderFactory("^->AppComponent->SignupEmailVerifyComponent", factory3b1904c76335d70151ebf47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->MainTabComponent", factory1ab5a747ddf21e1393f9f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->SigninComponent", factory2882a056d84a613debccf47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->HomeComponent", factory67229cdf0f755562b2b1f47b58f8f304c97af4d5) diff --git a/Projects/Features/BaseFeature/Sources/EnvironmentValues/RootPresentationMode.swift b/Projects/Features/BaseFeature/Sources/EnvironmentValues/RootPresentationMode.swift new file mode 100644 index 00000000..96aedfca --- /dev/null +++ b/Projects/Features/BaseFeature/Sources/EnvironmentValues/RootPresentationMode.swift @@ -0,0 +1,20 @@ +import SwiftUI + +public struct RootPresentationModeKey: EnvironmentKey { + public static let defaultValue: Binding = .constant(RootPresentationMode()) +} + +public extension EnvironmentValues { + var rootPresentationMode: Binding { + get { return self[RootPresentationModeKey.self] } + set { self[RootPresentationModeKey.self] = newValue } + } +} + +public typealias RootPresentationMode = Bool + +public extension RootPresentationMode { + mutating func dismiss() { + self.toggle() + } +} diff --git a/Projects/Features/BaseFeature/Sources/HideKeyboard.swift b/Projects/Features/BaseFeature/Sources/HideKeyboard.swift new file mode 100644 index 00000000..3926cd92 --- /dev/null +++ b/Projects/Features/BaseFeature/Sources/HideKeyboard.swift @@ -0,0 +1,23 @@ +import UIKit + +public extension UIApplication { + func hideKeyboard() { + guard + let scene = connectedScenes.first as? UIWindowScene, + let window = scene.windows.first + else { return } + let tapRecognizer = UITapGestureRecognizer(target: window, action: #selector(UIView.endEditing)) + tapRecognizer.cancelsTouchesInView = false + tapRecognizer.delegate = self + window.addGestureRecognizer(tapRecognizer) + } +} + +extension UIApplication: UIGestureRecognizerDelegate { + public func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer + ) -> Bool { + return false + } +} diff --git a/Projects/Features/SigninFeature/Sources/SigninView.swift b/Projects/Features/SigninFeature/Sources/SigninView.swift index 74acdd10..22e380fb 100644 --- a/Projects/Features/SigninFeature/Sources/SigninView.swift +++ b/Projects/Features/SigninFeature/Sources/SigninView.swift @@ -19,7 +19,6 @@ struct SigninView: View { VStack(alignment: .leading, spacing: 8) { Text("DMS") .dmsFont(.title(.extraLarge), color: .PrimaryVariant.primary) - .padding(.top, 28) Text("더 편한 기숙사 생활을 위해") .dmsFont(.text(.medium), color: .GrayScale.gray6) @@ -27,6 +26,7 @@ struct SigninView: View { Spacer() } + .padding(.top, 24) VStack(spacing: 72) { DMSFloatingTextField( @@ -96,7 +96,7 @@ struct SigninView: View { DMSWideButton(text: "로그인", color: .PrimaryVariant.primary) { viewModel.signinButtonDidTap() } - .disabled(!viewModel.isSigninButtonEnabled) + .disabled(!viewModel.isSigninEnabled) .padding(.top, 24) .frame(maxWidth: .infinity) .padding(.bottom, 40) diff --git a/Projects/Features/SigninFeature/Sources/SigninViewModel.swift b/Projects/Features/SigninFeature/Sources/SigninViewModel.swift index 502240d1..03d6ff6b 100644 --- a/Projects/Features/SigninFeature/Sources/SigninViewModel.swift +++ b/Projects/Features/SigninFeature/Sources/SigninViewModel.swift @@ -8,7 +8,7 @@ final class SigninViewModel: BaseViewModel { @Published var isOnAutoSignin = true @Published var isNavigateSignup = false @Published var isSuccessSignin = false - var isSigninButtonEnabled: Bool { + var isSigninEnabled: Bool { !id.isEmpty && !password.isEmpty } @@ -19,7 +19,7 @@ final class SigninViewModel: BaseViewModel { } func signinButtonDidTap() { - guard isSigninButtonEnabled else { return } + guard isSigninEnabled else { return } addCancellable(signinUseCase.execute(req: .init(accountID: id, password: password))) { [weak self] _ in self?.isSuccessSignin = true } diff --git a/Projects/Features/SigninFeature/Tests/SigninViewModelSpec.swift b/Projects/Features/SigninFeature/Tests/SigninViewModelSpec.swift index 83709c66..889d8457 100644 --- a/Projects/Features/SigninFeature/Tests/SigninViewModelSpec.swift +++ b/Projects/Features/SigninFeature/Tests/SigninViewModelSpec.swift @@ -20,7 +20,7 @@ final class SigninViewModelSpec: QuickSpec { it("isSigninButtonEnabled은 false이다.") { expect { sut.id }.to(beEmpty()) expect { sut.password }.to(beEmpty()) - expect { sut.isSigninButtonEnabled }.to(beFalse()) + expect { sut.isSigninEnabled }.to(beFalse()) } } context("유저가 ID만 입력했다면") { @@ -30,7 +30,7 @@ final class SigninViewModelSpec: QuickSpec { it("isSigninButtonEnabled은 false이다.") { expect { sut.id }.toNot(beEmpty()) expect { sut.password }.to(beEmpty()) - expect { sut.isSigninButtonEnabled }.to(beFalse()) + expect { sut.isSigninEnabled }.to(beFalse()) } } context("유저가 Password만 입력했다면") { @@ -40,7 +40,7 @@ final class SigninViewModelSpec: QuickSpec { it("isSigninButtonEnabled은 false이다.") { expect { sut.id }.to(beEmpty()) expect { sut.password }.toNot(beEmpty()) - expect { sut.isSigninButtonEnabled }.to(beFalse()) + expect { sut.isSigninEnabled }.to(beFalse()) } } context("유저가 ID와 Password를 모두 입력했다면") { @@ -51,7 +51,7 @@ final class SigninViewModelSpec: QuickSpec { it("isSigninButtonEnabled은 true이다.") { expect { sut.id }.toNot(beEmpty()) expect { sut.password }.toNot(beEmpty()) - expect { sut.isSigninButtonEnabled }.to(beTrue()) + expect { sut.isSigninEnabled }.to(beTrue()) } } context("유저가 ID와 Password를 알맞게 입력하고 로그인을 시도한다면") { diff --git a/Projects/Features/SignupFeature/Sources/SchoolCodeComponent.swift b/Projects/Features/SignupFeature/Sources/SchoolCode/SchoolCodeComponent.swift similarity index 100% rename from Projects/Features/SignupFeature/Sources/SchoolCodeComponent.swift rename to Projects/Features/SignupFeature/Sources/SchoolCode/SchoolCodeComponent.swift diff --git a/Projects/Features/SignupFeature/Sources/SchoolCodeView.swift b/Projects/Features/SignupFeature/Sources/SchoolCode/SchoolCodeView.swift similarity index 98% rename from Projects/Features/SignupFeature/Sources/SchoolCodeView.swift rename to Projects/Features/SignupFeature/Sources/SchoolCode/SchoolCodeView.swift index ef8b3507..7e43acef 100644 --- a/Projects/Features/SignupFeature/Sources/SchoolCodeView.swift +++ b/Projects/Features/SignupFeature/Sources/SchoolCode/SchoolCodeView.swift @@ -18,10 +18,10 @@ struct SchoolCodeView: View { Text("학교 인증코드 입력") .dmsFont(.text(.medium), color: .GrayScale.gray6) } - .padding(.top, 28) Spacer() } + .padding(.top, 24) .padding(.horizontal, 24) Spacer() diff --git a/Projects/Features/SignupFeature/Sources/SchoolCodeViewModel.swift b/Projects/Features/SignupFeature/Sources/SchoolCode/SchoolCodeViewModel.swift similarity index 100% rename from Projects/Features/SignupFeature/Sources/SchoolCodeViewModel.swift rename to Projects/Features/SignupFeature/Sources/SchoolCode/SchoolCodeViewModel.swift diff --git a/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/EmailAuthCodeVerifyComponent.swift b/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/EmailAuthCodeVerifyComponent.swift new file mode 100644 index 00000000..e495b99b --- /dev/null +++ b/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/EmailAuthCodeVerifyComponent.swift @@ -0,0 +1,20 @@ +import DomainModule +import NeedleFoundation +import SwiftUI + +public protocol SignupEmailAuthCodeVerifyDependency: Dependency { + var sendAuthCodeUseCase: any SendAuthCodeUseCase { get } + var verifyAuthCodeUseCase: any VerifyAuthCodeUseCase { get } +} + +public final class SignupEmailAuthCodeVerifyComponent: Component { + public func makeView(signupEmailAuthCodeVerifyParam: SignupEmailAuthCodeVerifyParam) -> some View { + SignupEmailAuthCodeVerifyView( + viewModel: .init( + sendAuthCodeUseCase: dependency.sendAuthCodeUseCase, + verifyAuthCodeUseCase: dependency.verifyAuthCodeUseCase, + signupEmailAuthCodeVerifyParam: signupEmailAuthCodeVerifyParam + ) + ) + } +} diff --git a/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/EmailAuthCodeVerifyView.swift b/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/EmailAuthCodeVerifyView.swift new file mode 100644 index 00000000..92f41b7e --- /dev/null +++ b/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/EmailAuthCodeVerifyView.swift @@ -0,0 +1,62 @@ +import BaseFeature +import DesignSystem +import SwiftUI + +struct SignupEmailAuthCodeVerifyView: View { + @StateObject var viewModel: SignupEmailAuthCodeVerifyViewModel + @Environment(\.dismiss) var dismiss + + init( + viewModel: SignupEmailAuthCodeVerifyViewModel + ) { + _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) + } + + 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) + } + .dmsBackButton(dismiss: dismiss) + .padding(.horizontal, 24) + .onAppear { + UIApplication.shared.hideKeyboard() + viewModel.sendEmailAuthCode() + } + .dmsToast(isShowing: $viewModel.isShowingToast, message: viewModel.toastMessage, style: .success) + } +} diff --git a/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/EmailAuthCodeVerifyViewModel.swift b/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/EmailAuthCodeVerifyViewModel.swift new file mode 100644 index 00000000..0fc835d0 --- /dev/null +++ b/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/EmailAuthCodeVerifyViewModel.swift @@ -0,0 +1,68 @@ +import BaseFeature +import Combine +import DomainModule +import ErrorModule +import Foundation + +final class SignupEmailAuthCodeVerifyViewModel: BaseViewModel { + @Published var authCode = "" { + didSet { isErrorOcuured = false } + } + @Published var timeRemaining = 180 + @Published var isShowingToast = false + @Published var toastMessage = "" + @Published var isNavigateSignupID = 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 signupEmailAuthCodeVerifyParam: SignupEmailAuthCodeVerifyParam + + init( + sendAuthCodeUseCase: any SendAuthCodeUseCase, + verifyAuthCodeUseCase: any VerifyAuthCodeUseCase, + signupEmailAuthCodeVerifyParam: SignupEmailAuthCodeVerifyParam + ) { + self.sendAuthCodeUseCase = sendAuthCodeUseCase + self.verifyAuthCodeUseCase = verifyAuthCodeUseCase + self.signupEmailAuthCodeVerifyParam = signupEmailAuthCodeVerifyParam + 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: signupEmailAuthCodeVerifyParam.email, type: .signup) + ) + ) { [weak self] _ in + self?.authCode = "" + self?.timeRemaining = 180 + self?.toastMessage = "입력하신 이메일로 인증번호가 전송되었습니다." + self?.isShowingToast = true + } + } + + func verifyEmailAuthCode() { + addCancellable( + verifyAuthCodeUseCase.execute( + req: .init(email: signupEmailAuthCodeVerifyParam.email, authCode: authCode, type: .signup) + ) + ) { [weak self] _ in + self?.isNavigateSignupID = true + } onReceiveError: { [weak self] _ in + self?.authCode = "" + } + } +} diff --git a/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/SignupEmailAuthCodeVerifyParam.swift b/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/SignupEmailAuthCodeVerifyParam.swift new file mode 100644 index 00000000..2051bc65 --- /dev/null +++ b/Projects/Features/SignupFeature/Sources/SignupEmailAuthCodeVerify/SignupEmailAuthCodeVerifyParam.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct SignupEmailAuthCodeVerifyParam: Equatable { + public init(schoolCode: String, schoolAnswer: String, email: String) { + self.schoolCode = schoolCode + self.schoolAnswer = schoolAnswer + self.email = email + } + + public let schoolCode: String + public let schoolAnswer: String + public let email: String +} diff --git a/Projects/Features/SignupFeature/Sources/SignupEmailVerify/SignupEmailVerifyComponent.swift b/Projects/Features/SignupFeature/Sources/SignupEmailVerify/SignupEmailVerifyComponent.swift new file mode 100644 index 00000000..a2c04e34 --- /dev/null +++ b/Projects/Features/SignupFeature/Sources/SignupEmailVerify/SignupEmailVerifyComponent.swift @@ -0,0 +1,20 @@ +import DomainModule +import SwiftUI +import NeedleFoundation + +public protocol SignupEmailVerifyDependency: Dependency { + var checkDuplicateEmailUseCase: any CheckDuplicateEmailUseCase { get } + var signupEmailAuthCodeVerifyComponent: SignupEmailAuthCodeVerifyComponent { get } +} + +public final class SignupEmailVerifyComponent: Component { + public func makeView(signupEmailVerifyParam: SignupEmailVerifyParam) -> some View { + SignupEmailVerifyView( + viewModel: .init( + checkDuplicateEmailUseCase: dependency.checkDuplicateEmailUseCase, + signupEmailVerifyParam: signupEmailVerifyParam + ), + signupEmailAuthCodeVerifyComponent: dependency.signupEmailAuthCodeVerifyComponent + ) + } +} diff --git a/Projects/Features/SignupFeature/Sources/SignupEmailVerify/SignupEmailVerifyParam.swift b/Projects/Features/SignupFeature/Sources/SignupEmailVerify/SignupEmailVerifyParam.swift new file mode 100644 index 00000000..08800e8c --- /dev/null +++ b/Projects/Features/SignupFeature/Sources/SignupEmailVerify/SignupEmailVerifyParam.swift @@ -0,0 +1,6 @@ +import Foundation + +public struct SignupEmailVerifyParam: Equatable { + public let schoolCode: String + public let schoolAnswer: String +} diff --git a/Projects/Features/SignupFeature/Sources/SignupEmailVerify/SignupEmailVerifyView.swift b/Projects/Features/SignupFeature/Sources/SignupEmailVerify/SignupEmailVerifyView.swift new file mode 100644 index 00000000..07508abe --- /dev/null +++ b/Projects/Features/SignupFeature/Sources/SignupEmailVerify/SignupEmailVerifyView.swift @@ -0,0 +1,74 @@ +import DesignSystem +import SwiftUI + +struct SignupEmailVerifyView: View { + @StateObject var viewModel: SignupEmailVerifyViewModel + @Environment(\.rootPresentationMode) var rootPresentationMode + @Environment(\.dismiss) var dismiss + let signupEmailAuthCodeVerifyComponent: SignupEmailAuthCodeVerifyComponent + + public init( + viewModel: SignupEmailVerifyViewModel, + signupEmailAuthCodeVerifyComponent: SignupEmailAuthCodeVerifyComponent + ) { + _viewModel = StateObject(wrappedValue: viewModel) + self.signupEmailAuthCodeVerifyComponent = signupEmailAuthCodeVerifyComponent + } + + 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) + + DMSFloatingTextField( + "이메일 주소", + text: $viewModel.email, + isError: viewModel.isErrorOcuured, + errorMessage: viewModel.errorMessage + ) { + viewModel.sendButtonDidTap() + } + .padding(.top, 68) + + Spacer() + + HStack(spacing: 6) { + Text("이미 계정이 있으신가요?") + .dmsFont(.text(.small), color: .GrayScale.gray5) + + DMSButton(text: "로그인", style: .text, color: .GrayScale.gray6) { + rootPresentationMode.wrappedValue.toggle() + } + } + + DMSWideButton(text: "인증코드 발송", color: .PrimaryVariant.primary) { + viewModel.sendButtonDidTap() + } + .disabled(!viewModel.isSendEnabled) + .padding(.top, 24) + .padding(.bottom, 40) + } + .navigate( + to: signupEmailAuthCodeVerifyComponent.makeView( + signupEmailAuthCodeVerifyParam: .init( + schoolCode: viewModel.signupEmailVerifyParam.schoolCode, + schoolAnswer: viewModel.signupEmailVerifyParam.schoolAnswer, + email: viewModel.email + ) + ), + when: $viewModel.isNavigateSignupEmailAuthCodeVerify + ) + .dmsBackButton(dismiss: dismiss) + .padding(.horizontal, 24) + } +} diff --git a/Projects/Features/SignupFeature/Sources/SignupEmailVerify/SignupEmailVerifyViewModel.swift b/Projects/Features/SignupFeature/Sources/SignupEmailVerify/SignupEmailVerifyViewModel.swift new file mode 100644 index 00000000..813ed3c8 --- /dev/null +++ b/Projects/Features/SignupFeature/Sources/SignupEmailVerify/SignupEmailVerifyViewModel.swift @@ -0,0 +1,42 @@ +import BaseFeature +import Combine +import DomainModule +import Utility + +final class SignupEmailVerifyViewModel: BaseViewModel { + @Published var email = "" { + didSet { isErrorOcuured = false } + } + @Published var isNavigateSignupEmailAuthCodeVerify = false + var isSendEnabled: Bool { + !email.isEmpty + } + + private let checkDuplicateEmailUseCase: any CheckDuplicateEmailUseCase + let signupEmailVerifyParam: SignupEmailVerifyParam + + public init( + checkDuplicateEmailUseCase: any CheckDuplicateEmailUseCase, + signupEmailVerifyParam: SignupEmailVerifyParam + ) { + self.checkDuplicateEmailUseCase = checkDuplicateEmailUseCase + self.signupEmailVerifyParam = signupEmailVerifyParam + } + + func sendButtonDidTap() { + let emailExpression = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}" + guard + email ~= emailExpression, + isSendEnabled + else { + isErrorOcuured = true + errorMessage = "올바른 이메일 형식이 아닙니다." + return + } + addCancellable( + checkDuplicateEmailUseCase.execute(email: email) + ) { [weak self] _ in + self?.isNavigateSignupEmailAuthCodeVerify = true + } + } +} diff --git a/Projects/Modules/Utility/Sources/RegexUtil.swift b/Projects/Modules/Utility/Sources/RegexUtil.swift new file mode 100644 index 00000000..049eb504 --- /dev/null +++ b/Projects/Modules/Utility/Sources/RegexUtil.swift @@ -0,0 +1,26 @@ +import Foundation + +public extension NSRegularExpression { + convenience init(_ pattern: String) { + do { + try self.init(pattern: pattern) + } catch { + preconditionFailure("Illegal regular expression: \(pattern).") + } + } +} + +public extension NSRegularExpression { + func matches(_ string: String) -> Bool { + let range = NSRange(location: 0, length: string.count) + return firstMatch(in: string, options: [], range: range) != nil + } +} + +public extension String { + static func ~= (lhs: String, rhs: String) -> Bool { + guard let regex = try? NSRegularExpression(pattern: rhs) else { return false } + let range = NSRange(location: 0, length: lhs.utf16.count) + return regex.firstMatch(in: lhs, options: [], range: range) != nil + } +} diff --git a/Projects/UsertInterfaces/DesignSystem/Sources/PassCode/DMSPassCodeView.swift b/Projects/UsertInterfaces/DesignSystem/Sources/PassCode/DMSPassCodeView.swift index 38360032..bef0e338 100644 --- a/Projects/UsertInterfaces/DesignSystem/Sources/PassCode/DMSPassCodeView.swift +++ b/Projects/UsertInterfaces/DesignSystem/Sources/PassCode/DMSPassCodeView.swift @@ -34,6 +34,9 @@ public struct DMSPassCodeView: View { Circle() .frame(width: 20, height: 20) .foregroundColor(text.count >= num ? Color.GrayScale.gray6 : Color.GrayScale.gray4) + .onTapGesture { + focused = true + } } } } diff --git a/Projects/UsertInterfaces/DesignSystem/Sources/TextField/Floating/DMSFloatingTextField.swift b/Projects/UsertInterfaces/DesignSystem/Sources/TextField/Floating/DMSFloatingTextField.swift index 2b6c5137..2364801b 100644 --- a/Projects/UsertInterfaces/DesignSystem/Sources/TextField/Floating/DMSFloatingTextField.swift +++ b/Projects/UsertInterfaces/DesignSystem/Sources/TextField/Floating/DMSFloatingTextField.swift @@ -8,7 +8,6 @@ public struct DMSFloatingTextField: View { var errorMessage: String var onCommit: () -> Void @FocusState var isFocused: Bool - @Namespace var animation private var isFloaintg: Bool { isFocused || !text.isEmpty }