diff --git a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift index e179a2d..7d3ed7c 100644 --- a/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift +++ b/Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift @@ -24,6 +24,7 @@ extension ModulePaths: MicroTargetPathConvertable { public extension ModulePaths { enum Feature: String, MicroTargetPathConvertable { + case FindPasswordFeature case SignupFeature case SigninFeature case SplashFeature diff --git a/Projects/App/Sources/Application/DI/AppComponent.swift b/Projects/App/Sources/Application/DI/AppComponent.swift index 90717f0..ad5a8d2 100644 --- a/Projects/App/Sources/Application/DI/AppComponent.swift +++ b/Projects/App/Sources/Application/DI/AppComponent.swift @@ -8,6 +8,8 @@ import SigninFeature import SigninFeatureInterface import SignupFeature import SignupFeatureInterface +import FindPasswordFeature +import FindPasswordFeatureInterface public final class AppComponent: BootstrapComponent { // private let _keychain: any Keychain @@ -63,4 +65,15 @@ public extension AppComponent { var signupCheckLevelFactory: any SignupCheckLevelFactory { SignupCheckLevelComponent(parent: self) } + + // Find Password + var inputEmailFactory: any InputEmailFactory { + InputEmailComponent(parent: self) + } + var verifyAuthCodeFactory: any VerifyAuthCodeFactory { + VerifyAuthCodeComponent(parent: self) + } + var inputNewPasswordFactory: any InputNewPasswordFactory { + InputNewPasswordComponent(parent: self) + } } diff --git a/Projects/App/Sources/Application/NeedleGenerated.swift b/Projects/App/Sources/Application/NeedleGenerated.swift index 13eed84..2fa268d 100644 --- a/Projects/App/Sources/Application/NeedleGenerated.swift +++ b/Projects/App/Sources/Application/NeedleGenerated.swift @@ -1,5 +1,7 @@ +import FindPasswordFeature +import FindPasswordFeatureInterface import NeedleFoundation import RootFeature import RootFeatureInterface @@ -162,6 +164,9 @@ private class SigninDependencyde06a9d0b22764487733Provider: SigninDependency { var signupEmailFactory: any SignupEmailFactory { return appComponent.signupEmailFactory } + var inputEmailFactory: any InputEmailFactory { + return appComponent.inputEmailFactory + } private let appComponent: AppComponent init(appComponent: AppComponent) { self.appComponent = appComponent @@ -171,6 +176,43 @@ private class SigninDependencyde06a9d0b22764487733Provider: SigninDependency { private func factory2882a056d84a613debccf47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { return SigninDependencyde06a9d0b22764487733Provider(appComponent: parent1(component) as! AppComponent) } +private class VerifyAuthCodeDependencya16ab19f97e0892b555bProvider: VerifyAuthCodeDependency { + var inputNewPasswordFactory: any InputNewPasswordFactory { + return appComponent.inputNewPasswordFactory + } + private let appComponent: AppComponent + init(appComponent: AppComponent) { + self.appComponent = appComponent + } +} +/// ^->AppComponent->VerifyAuthCodeComponent +private func factoryed5ce75de1bf576b84adf47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { + return VerifyAuthCodeDependencya16ab19f97e0892b555bProvider(appComponent: parent1(component) as! AppComponent) +} +private class InputNewPasswordDependency1149e32e41e1cfce6f6dProvider: InputNewPasswordDependency { + + + init() { + + } +} +/// ^->AppComponent->InputNewPasswordComponent +private func factory27615059458a0576f404e3b0c44298fc1c149afb(_ component: NeedleFoundation.Scope) -> AnyObject { + return InputNewPasswordDependency1149e32e41e1cfce6f6dProvider() +} +private class InputEmailDependency4102766a436592066e97Provider: InputEmailDependency { + var verifyAuthCodeFactory: any VerifyAuthCodeFactory { + return appComponent.verifyAuthCodeFactory + } + private let appComponent: AppComponent + init(appComponent: AppComponent) { + self.appComponent = appComponent + } +} +/// ^->AppComponent->InputEmailComponent +private func factoryf939e41ba3a1151e88f8f47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { + return InputEmailDependency4102766a436592066e97Provider(appComponent: parent1(component) as! AppComponent) +} #else extension AppComponent: Registration { @@ -186,6 +228,9 @@ extension AppComponent: Registration { localTable["signupStudentIDFactory-any SignupStudentIDFactory"] = { [unowned self] in self.signupStudentIDFactory as Any } localTable["signupGenderFactory-any SignupGenderFactory"] = { [unowned self] in self.signupGenderFactory as Any } localTable["signupCheckLevelFactory-any SignupCheckLevelFactory"] = { [unowned self] in self.signupCheckLevelFactory as Any } + localTable["inputEmailFactory-any InputEmailFactory"] = { [unowned self] in self.inputEmailFactory as Any } + localTable["verifyAuthCodeFactory-any VerifyAuthCodeFactory"] = { [unowned self] in self.verifyAuthCodeFactory as Any } + localTable["inputNewPasswordFactory-any InputNewPasswordFactory"] = { [unowned self] in self.inputNewPasswordFactory as Any } } } extension SplashComponent: Registration { @@ -245,6 +290,22 @@ extension RootComponent: Registration { extension SigninComponent: Registration { public func registerItems() { keyPathToName[\SigninDependency.signupEmailFactory] = "signupEmailFactory-any SignupEmailFactory" + keyPathToName[\SigninDependency.inputEmailFactory] = "inputEmailFactory-any InputEmailFactory" + } +} +extension VerifyAuthCodeComponent: Registration { + public func registerItems() { + keyPathToName[\VerifyAuthCodeDependency.inputNewPasswordFactory] = "inputNewPasswordFactory-any InputNewPasswordFactory" + } +} +extension InputNewPasswordComponent: Registration { + public func registerItems() { + + } +} +extension InputEmailComponent: Registration { + public func registerItems() { + keyPathToName[\InputEmailDependency.verifyAuthCodeFactory] = "verifyAuthCodeFactory-any VerifyAuthCodeFactory" } } @@ -275,6 +336,9 @@ private func registerProviderFactory(_ componentPath: String, _ factory: @escapi registerProviderFactory("^->AppComponent->SignupEmailComponent", factory4d1ddf658c5970ef6b47f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->RootComponent", factory264bfc4d4cb6b0629b40f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->SigninComponent", factory2882a056d84a613debccf47b58f8f304c97af4d5) + registerProviderFactory("^->AppComponent->VerifyAuthCodeComponent", factoryed5ce75de1bf576b84adf47b58f8f304c97af4d5) + registerProviderFactory("^->AppComponent->InputNewPasswordComponent", factory27615059458a0576f404e3b0c44298fc1c149afb) + registerProviderFactory("^->AppComponent->InputEmailComponent", factoryf939e41ba3a1151e88f8f47b58f8f304c97af4d5) } #endif diff --git a/Projects/Feature/BaseFeature/Sources/EnvironmentValues/RootPresentationMode.swift b/Projects/Feature/BaseFeature/Sources/EnvironmentValues/RootPresentationMode.swift new file mode 100644 index 0000000..619b788 --- /dev/null +++ b/Projects/Feature/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 Binding { + func popToRootView() { + self.wrappedValue.toggle() + } +} diff --git a/Projects/Feature/FindPasswordFeature/Interface/InputEmailFactory.swift b/Projects/Feature/FindPasswordFeature/Interface/InputEmailFactory.swift new file mode 100644 index 0000000..ce19a83 --- /dev/null +++ b/Projects/Feature/FindPasswordFeature/Interface/InputEmailFactory.swift @@ -0,0 +1,6 @@ +import SwiftUI + +public protocol InputEmailFactory { + associatedtype SomeView: View + func makeView() -> SomeView +} diff --git a/Projects/Feature/FindPasswordFeature/Interface/InputNewPasswordFactory.swift b/Projects/Feature/FindPasswordFeature/Interface/InputNewPasswordFactory.swift new file mode 100644 index 0000000..f12aab8 --- /dev/null +++ b/Projects/Feature/FindPasswordFeature/Interface/InputNewPasswordFactory.swift @@ -0,0 +1,6 @@ +import SwiftUI + +public protocol InputNewPasswordFactory { + associatedtype SomeView: View + func makeView() -> SomeView +} diff --git a/Projects/Feature/FindPasswordFeature/Interface/VerifyAuthCodeFactory.swift b/Projects/Feature/FindPasswordFeature/Interface/VerifyAuthCodeFactory.swift new file mode 100644 index 0000000..b977a25 --- /dev/null +++ b/Projects/Feature/FindPasswordFeature/Interface/VerifyAuthCodeFactory.swift @@ -0,0 +1,6 @@ +import SwiftUI + +public protocol VerifyAuthCodeFactory { + associatedtype SomeView: View + func makeView() -> SomeView +} diff --git a/Projects/Feature/FindPasswordFeature/Project.swift b/Projects/Feature/FindPasswordFeature/Project.swift new file mode 100644 index 0000000..f654b37 --- /dev/null +++ b/Projects/Feature/FindPasswordFeature/Project.swift @@ -0,0 +1,17 @@ +import DependencyPlugin +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.module( + name: ModulePaths.Feature.FindPasswordFeature.rawValue, + targets: [ + .interface(module: .feature(.FindPasswordFeature)), + .implements(module: .feature(.FindPasswordFeature), dependencies: [ + .feature(target: .FindPasswordFeature, type: .interface), + .feature(target: .BaseFeature) + ]), + .tests(module: .feature(.FindPasswordFeature), dependencies: [ + .feature(target: .FindPasswordFeature) + ]) + ] +) diff --git a/Projects/Feature/FindPasswordFeature/Sources/Email/InputEmailComponent.swift b/Projects/Feature/FindPasswordFeature/Sources/Email/InputEmailComponent.swift new file mode 100644 index 0000000..b35cc94 --- /dev/null +++ b/Projects/Feature/FindPasswordFeature/Sources/Email/InputEmailComponent.swift @@ -0,0 +1,16 @@ +import SwiftUI +import NeedleFoundation +import FindPasswordFeatureInterface + +public protocol InputEmailDependency: Dependency { + var verifyAuthCodeFactory: any VerifyAuthCodeFactory { get } +} + +public final class InputEmailComponent: Component, InputEmailFactory { + public func makeView() -> some View { + InputEmailView( + viewModel: .init(), + verifyAuthCodeFactory: dependency.verifyAuthCodeFactory + ) + } +} diff --git a/Projects/Feature/FindPasswordFeature/Sources/Email/InputEmailViewModel.swift b/Projects/Feature/FindPasswordFeature/Sources/Email/InputEmailViewModel.swift new file mode 100644 index 0000000..5831b0f --- /dev/null +++ b/Projects/Feature/FindPasswordFeature/Sources/Email/InputEmailViewModel.swift @@ -0,0 +1,12 @@ +import BaseFeature +import Combine + +final class InputEmailViewModel: BaseViewModel { + @Published var email: String = "" + + @Published var isNavigatedToVerifyAuthCode: Bool = false + + func nextButtonDidTap() { + self.isNavigatedToVerifyAuthCode = true + } +} diff --git a/Projects/Feature/FindPasswordFeature/Sources/Email/InputSignupEmailView.swift b/Projects/Feature/FindPasswordFeature/Sources/Email/InputSignupEmailView.swift new file mode 100644 index 0000000..0b44eac --- /dev/null +++ b/Projects/Feature/FindPasswordFeature/Sources/Email/InputSignupEmailView.swift @@ -0,0 +1,61 @@ +import DesignSystem +import SwiftUI +import BaseFeature +import ViewUtil +import FindPasswordFeatureInterface + +struct InputEmailView: View { + private enum FocusField { + case email + } + @FocusState private var focusField: FocusField? + @StateObject var viewModel: InputEmailViewModel + @Environment(\.rootPresentationMode) var rootPresentationMode + + private let verifyAuthCodeFactory: any VerifyAuthCodeFactory + + init( + viewModel: InputEmailViewModel, + verifyAuthCodeFactory: any VerifyAuthCodeFactory + ) { + _viewModel = StateObject(wrappedValue: viewModel) + self.verifyAuthCodeFactory = verifyAuthCodeFactory + } + + var body: some View { + VStack(spacing: 0) { + NavigationTitleView( + title: "이메일을 입력해 주세요", + description: "이메일로 인증번호를 전송해 드릴게요" + ) + + KGTextField( + "이메일을 입력해주세요", + text: $viewModel.email, + title: "이메일", + isError: viewModel.isErrorOccurred, + errorMessage: viewModel.errorMessage, + onCommit: viewModel.nextButtonDidTap + ) + .textContentType(.emailAddress) + .keyboardType(.emailAddress) + .focused($focusField, equals: .email) + + Spacer() + } + .bottomButton( + text: "다음", + isEditing: focusField != nil, + isDisabled: viewModel.email.isEmpty, + action: viewModel.nextButtonDidTap + ) + .navigationBackButton() + .kgBackground() + .hideKeyboardWhenTap() + .navigate( + to: verifyAuthCodeFactory.makeView().eraseToAnyView() + .environment(\.rootPresentationMode, rootPresentationMode), + when: $viewModel.isNavigatedToVerifyAuthCode + ) + } +} diff --git a/Projects/Feature/FindPasswordFeature/Sources/NewPassword/InputNewPasswordComponent.swift b/Projects/Feature/FindPasswordFeature/Sources/NewPassword/InputNewPasswordComponent.swift new file mode 100644 index 0000000..742f013 --- /dev/null +++ b/Projects/Feature/FindPasswordFeature/Sources/NewPassword/InputNewPasswordComponent.swift @@ -0,0 +1,13 @@ +import SwiftUI +import NeedleFoundation +import FindPasswordFeatureInterface + +public protocol InputNewPasswordDependency: Dependency {} + +public final class InputNewPasswordComponent: Component, InputNewPasswordFactory { + public func makeView() -> some View { + InputNewPasswordView( + viewModel: .init() + ) + } +} diff --git a/Projects/Feature/FindPasswordFeature/Sources/NewPassword/InputNewPasswordView.swift b/Projects/Feature/FindPasswordFeature/Sources/NewPassword/InputNewPasswordView.swift new file mode 100644 index 0000000..194f8b9 --- /dev/null +++ b/Projects/Feature/FindPasswordFeature/Sources/NewPassword/InputNewPasswordView.swift @@ -0,0 +1,71 @@ +import DesignSystem +import SwiftUI +import BaseFeature +import SignupFeatureInterface +import ViewUtil + +struct InputNewPasswordView: View { + private enum FocusField { + case password + case checkPassword + } + @FocusState private var focusField: FocusField? + @StateObject var viewModel: InputNewPasswordViewModel + @Environment(\.rootPresentationMode) var rootPresentationMode + + init( + viewModel: InputNewPasswordViewModel + ) { + _viewModel = StateObject(wrappedValue: viewModel) + } + + var body: some View { + VStack(spacing: 0) { + NavigationTitleView( + title: "새 비밀번호를 입력해주세요", + description: "비밀번호는 영어와 숫자를 조합해 만들어 주세요" + ) + + KGTextField( + "비밀번호(8~12자)를 입력해 주세요", + text: $viewModel.password, + title: "비밀번호", + isError: viewModel.isErrorOccurred, + errorMessage: viewModel.errorMessage, + isSecure: true + ) { + self.focusField = .checkPassword + } + .textContentType(.password) + .focused($focusField, equals: .password) + + KGTextField( + "비밀번호 다시 입력해 주세요", + text: $viewModel.checkPassword, + title: "비밀번호 확인", + isError: viewModel.isErrorOccurred, + errorMessage: viewModel.errorMessage, + isSecure: true, + onCommit: viewModel.nextButtonDidTap + ) + .textContentType(.password) + .focused($focusField, equals: .checkPassword) + + Spacer() + } + .bottomButton( + text: "다음", + isEditing: focusField != nil, + isDisabled: viewModel.password.isEmpty || viewModel.checkPassword.isEmpty, + action: viewModel.nextButtonDidTap + ) + .navigationBar() + .kgBackground() + .hideKeyboardWhenTap() + .onSuccess(of: viewModel.isSuccessToChangePassword) { + DispatchQueue.main.async { + self.rootPresentationMode.popToRootView() + } + } + } +} diff --git a/Projects/Feature/FindPasswordFeature/Sources/NewPassword/InputNewPasswordViewModel.swift b/Projects/Feature/FindPasswordFeature/Sources/NewPassword/InputNewPasswordViewModel.swift new file mode 100644 index 0000000..c0aa1a4 --- /dev/null +++ b/Projects/Feature/FindPasswordFeature/Sources/NewPassword/InputNewPasswordViewModel.swift @@ -0,0 +1,13 @@ +import BaseFeature +import Combine + +final class InputNewPasswordViewModel: BaseViewModel { + @Published var password: String = "" + @Published var checkPassword: String = "" + + @Published var isSuccessToChangePassword: Bool = false + + func nextButtonDidTap() { + self.isSuccessToChangePassword = true + } +} diff --git a/Projects/Feature/FindPasswordFeature/Sources/VerifyAuthCode/VerifyAuthCodeComponent.swift b/Projects/Feature/FindPasswordFeature/Sources/VerifyAuthCode/VerifyAuthCodeComponent.swift new file mode 100644 index 0000000..d713b57 --- /dev/null +++ b/Projects/Feature/FindPasswordFeature/Sources/VerifyAuthCode/VerifyAuthCodeComponent.swift @@ -0,0 +1,17 @@ +import SwiftUI +import NeedleFoundation +import FindPasswordFeatureInterface + +public protocol VerifyAuthCodeDependency: Dependency { + var inputNewPasswordFactory: any InputNewPasswordFactory { get } +} + +public final class VerifyAuthCodeComponent: Component, + VerifyAuthCodeFactory { + public func makeView() -> some View { + VerifyAuthCodeView( + viewModel: .init(), + inputNewPasswordFactory: dependency.inputNewPasswordFactory + ) + } +} diff --git a/Projects/Feature/FindPasswordFeature/Sources/VerifyAuthCode/VerifyAuthCodeView.swift b/Projects/Feature/FindPasswordFeature/Sources/VerifyAuthCode/VerifyAuthCodeView.swift new file mode 100644 index 0000000..9d0777d --- /dev/null +++ b/Projects/Feature/FindPasswordFeature/Sources/VerifyAuthCode/VerifyAuthCodeView.swift @@ -0,0 +1,70 @@ +import DesignSystem +import SwiftUI +import BaseFeature +import ViewUtil +import FindPasswordFeatureInterface + +struct VerifyAuthCodeView: View { + private enum FocusField { + case authCode + } + @FocusState private var focusField: FocusField? + @StateObject var viewModel: VerifyAuthCodeViewModel + @Environment(\.rootPresentationMode) var rootPresentationMode + + private let inputNewPasswordFactory: any InputNewPasswordFactory + + init( + viewModel: VerifyAuthCodeViewModel, + inputNewPasswordFactory: any InputNewPasswordFactory + ) { + _viewModel = StateObject(wrappedValue: viewModel) + self.inputNewPasswordFactory = inputNewPasswordFactory + } + + var body: some View { + VStack(spacing: 0) { + NavigationTitleView( + title: "인증번호를 입력해 주세요", + description: "입력해주신 이메일로 인증번호를 전송해 드렸어요" + ) + + VerifyCodeTextField( + $viewModel.authCode, + isError: viewModel.isErrorOccurred, + errorMessage: viewModel.errorMessage, + onCommit: viewModel.nextButtonDidTap + ) + .focused($focusField, equals: .authCode) + + HStack { + Text("5:00") + .kgFont(.label, weight: .regular, color: .Greens.main) + + Spacer() + + Text("인증번호 재전송") + .kgFont(.label, weight: .regular, color: .Grays.gray400) + .underlineText(color: .Grays.gray400) + } + .padding(.vertical, 16) + .padding(.horizontal, 24) + + Spacer() + } + .bottomButton( + text: "인증", + isEditing: focusField != nil, + isDisabled: viewModel.authCode.count < 4, + action: viewModel.nextButtonDidTap + ) + .navigationBackButton() + .kgBackground() + .hideKeyboardWhenTap() + .navigate( + to: inputNewPasswordFactory.makeView().eraseToAnyView() + .environment(\.rootPresentationMode, rootPresentationMode), + when: $viewModel.isNavigatedToNewPassword + ) + } +} diff --git a/Projects/Feature/FindPasswordFeature/Sources/VerifyAuthCode/VerifyAuthCodeViewModel.swift b/Projects/Feature/FindPasswordFeature/Sources/VerifyAuthCode/VerifyAuthCodeViewModel.swift new file mode 100644 index 0000000..9828a4e --- /dev/null +++ b/Projects/Feature/FindPasswordFeature/Sources/VerifyAuthCode/VerifyAuthCodeViewModel.swift @@ -0,0 +1,13 @@ +import BaseFeature +import Foundation +import Combine + +final class VerifyAuthCodeViewModel: BaseViewModel { + @Published var authCode: String = "" + + @Published var isNavigatedToNewPassword: Bool = false + + func nextButtonDidTap() { + self.isNavigatedToNewPassword = true + } +} diff --git a/Projects/Feature/FindPasswordFeature/Tests/FindPasswordFeatureTest.swift b/Projects/Feature/FindPasswordFeature/Tests/FindPasswordFeatureTest.swift new file mode 100644 index 0000000..ee17017 --- /dev/null +++ b/Projects/Feature/FindPasswordFeature/Tests/FindPasswordFeatureTest.swift @@ -0,0 +1,11 @@ +import XCTest + +final class FindPasswordFeatureTests: XCTestCase { + override func setUpWithError() throws {} + + override func tearDownWithError() throws {} + + func testExample() { + XCTAssertEqual(1, 1) + } +} diff --git a/Projects/Feature/SigninFeature/Project.swift b/Projects/Feature/SigninFeature/Project.swift index 7bde5f1..048b497 100644 --- a/Projects/Feature/SigninFeature/Project.swift +++ b/Projects/Feature/SigninFeature/Project.swift @@ -9,7 +9,8 @@ let project = Project.module( .implements(module: .feature(.SigninFeature), dependencies: [ .feature(target: .SigninFeature, type: .interface), .feature(target: .BaseFeature), - .feature(target: .SignupFeature, type: .interface) + .feature(target: .SignupFeature, type: .interface), + .feature(target: .FindPasswordFeature, type: .interface) ]), .tests(module: .feature(.SigninFeature), dependencies: [ .feature(target: .SigninFeature) diff --git a/Projects/Feature/SigninFeature/Sources/SigninComponent.swift b/Projects/Feature/SigninFeature/Sources/SigninComponent.swift index d5d2bc9..1c3e783 100644 --- a/Projects/Feature/SigninFeature/Sources/SigninComponent.swift +++ b/Projects/Feature/SigninFeature/Sources/SigninComponent.swift @@ -2,16 +2,19 @@ import SwiftUI import NeedleFoundation import SigninFeatureInterface import SignupFeatureInterface +import FindPasswordFeatureInterface public protocol SigninDependency: Dependency { var signupEmailFactory: any SignupEmailFactory { get } + var inputEmailFactory: any InputEmailFactory { get } } public final class SigninComponent: Component, SigninFactory { public func makeView() -> some View { SigninView( viewModel: .init(), - signupEmailFactory: dependency.signupEmailFactory + signupEmailFactory: dependency.signupEmailFactory, + inputEmailFactory: dependency.inputEmailFactory ) } } diff --git a/Projects/Feature/SigninFeature/Sources/SigninView.swift b/Projects/Feature/SigninFeature/Sources/SigninView.swift index ad9b48f..0bee470 100644 --- a/Projects/Feature/SigninFeature/Sources/SigninView.swift +++ b/Projects/Feature/SigninFeature/Sources/SigninView.swift @@ -3,6 +3,7 @@ import SwiftUI import BaseFeature import ViewUtil import SignupFeatureInterface +import FindPasswordFeatureInterface struct SigninView: View { private enum FocusField { @@ -13,13 +14,16 @@ struct SigninView: View { @StateObject var viewModel: SigninViewModel private let signupEmailFactory: any SignupEmailFactory + private let inputEmailFactory: any InputEmailFactory init( viewModel: SigninViewModel, - signupEmailFactory: any SignupEmailFactory + signupEmailFactory: any SignupEmailFactory, + inputEmailFactory: any InputEmailFactory ) { _viewModel = StateObject(wrappedValue: viewModel) self.signupEmailFactory = signupEmailFactory + self.inputEmailFactory = inputEmailFactory } var body: some View { @@ -63,13 +67,24 @@ struct SigninView: View { Color.Grays.gray700 .frame(width: 1, height: 16) - Text("비밀번호 찾기") - .kgFont(.label, weight: .regular, color: .Grays.gray700) + Button(action: viewModel.findPasswordButtonDidTap) { + Text("비밀번호 찾기") + .kgFont(.label, weight: .regular, color: .Grays.gray700) + } } } Spacer() } - .navigate(to: signupEmailFactory.makeView().eraseToAnyView(), when: $viewModel.isNavigatedToSignup) + .hideKeyboardWhenTap() + .navigate( + to: signupEmailFactory.makeView().eraseToAnyView(), + when: $viewModel.isNavigatedToSignup + ) + .navigate( + to: inputEmailFactory.makeView().eraseToAnyView() + .environment(\.rootPresentationMode, $viewModel.isNavigatedToFindPassword), + when: $viewModel.isNavigatedToFindPassword + ) } } diff --git a/Projects/Feature/SigninFeature/Sources/SigninViewModel.swift b/Projects/Feature/SigninFeature/Sources/SigninViewModel.swift index ca3ec76..4e89503 100644 --- a/Projects/Feature/SigninFeature/Sources/SigninViewModel.swift +++ b/Projects/Feature/SigninFeature/Sources/SigninViewModel.swift @@ -6,6 +6,7 @@ final class SigninViewModel: BaseViewModel { @Published var password: String = "" @Published var isNavigatedToSignup: Bool = false + @Published var isNavigatedToFindPassword: Bool = false func signinButtonDidTap() { print("Signin") @@ -14,4 +15,8 @@ final class SigninViewModel: BaseViewModel { func signupButtonDidTap() { self.isNavigatedToSignup = true } + + func findPasswordButtonDidTap() { + self.isNavigatedToFindPassword = true + } } diff --git a/Projects/Feature/SignupFeature/Interface/SignupAuthCodeVerifyFactory.swift b/Projects/Feature/SignupFeature/Interface/SignupVerifyAuthCodeFactory.swift similarity index 100% rename from Projects/Feature/SignupFeature/Interface/SignupAuthCodeVerifyFactory.swift rename to Projects/Feature/SignupFeature/Interface/SignupVerifyAuthCodeFactory.swift diff --git a/Projects/Shared/ViewUtil/Sources/View+onSuccess.swift b/Projects/Shared/ViewUtil/Sources/View+onSuccess.swift new file mode 100644 index 0000000..75f369f --- /dev/null +++ b/Projects/Shared/ViewUtil/Sources/View+onSuccess.swift @@ -0,0 +1,11 @@ +import SwiftUI + +public extension View { + func onSuccess(of value: Bool, _ action: @escaping () -> Void) -> some View { + self.onChange(of: value) { changedValue in + if changedValue { + action() + } + } + } +} diff --git a/Projects/Feature/SignupFeature/Sources/Components/BottomButton.swift b/Projects/UserInterface/DesignSystem/Sources/Extension/View+bottomButton.swift similarity index 95% rename from Projects/Feature/SignupFeature/Sources/Components/BottomButton.swift rename to Projects/UserInterface/DesignSystem/Sources/Extension/View+bottomButton.swift index 6551884..80eb562 100644 --- a/Projects/Feature/SignupFeature/Sources/Components/BottomButton.swift +++ b/Projects/UserInterface/DesignSystem/Sources/Extension/View+bottomButton.swift @@ -1,10 +1,9 @@ import SwiftUI -import DesignSystem /// Vstack으로 할 경우 버튼이 잘리는 문제가 발생 /// ZStack을 사용해 SuperView의 상단으로 띄움 -struct BottomButton: ViewModifier { +private struct BottomButton: ViewModifier { let text: String let isEditing: Bool let isDisabled: Bool diff --git a/Projects/UserInterface/DesignSystem/Sources/TextField/KGTextField.swift b/Projects/UserInterface/DesignSystem/Sources/TextField/KGTextField.swift index cea365c..9e19534 100644 --- a/Projects/UserInterface/DesignSystem/Sources/TextField/KGTextField.swift +++ b/Projects/UserInterface/DesignSystem/Sources/TextField/KGTextField.swift @@ -69,6 +69,9 @@ public struct KGTextField: View { lineWidth: 1 ) } + .onTapGesture { + self.isFocused = true + } if !description.isEmpty || isErrorAndNotEmpty { Text(isErrorAndNotEmpty ? errorMessage : description) diff --git a/Projects/Feature/SignupFeature/Sources/Components/NavigationTitleView.swift b/Projects/UserInterface/DesignSystem/Sources/View/NavigationTitleView.swift similarity index 59% rename from Projects/Feature/SignupFeature/Sources/Components/NavigationTitleView.swift rename to Projects/UserInterface/DesignSystem/Sources/View/NavigationTitleView.swift index 6d6d621..1690ccc 100644 --- a/Projects/Feature/SignupFeature/Sources/Components/NavigationTitleView.swift +++ b/Projects/UserInterface/DesignSystem/Sources/View/NavigationTitleView.swift @@ -1,11 +1,15 @@ import SwiftUI -import DesignSystem -struct NavigationTitleView: View { - let title: String - let description: String +public struct NavigationTitleView: View { + private let title: String + private let description: String - var body: some View { + public init(title: String, description: String) { + self.title = title + self.description = description + } + + public var body: some View { VStack(alignment: .leading, spacing: 8) { Text(title) .kgFont(.h3, weight: .semiBold, color: .white)