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
]