diff --git a/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj b/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj index 4b22f8d..1ae3142 100644 --- a/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj +++ b/Projects/CommonUI/CommonUI.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 950A0D562E5C29D000C07CF2 /* LMTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D552E5C29CC00C07CF2 /* LMTextField.swift */; }; 950A0D602E5C3C7000C07CF2 /* LMButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D5F2E5C3C6D00C07CF2 /* LMButton.swift */; }; 950A0D622E5C562700C07CF2 /* LMInputField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D612E5C561400C07CF2 /* LMInputField.swift */; }; + 950A0D962E605CEA00C07CF2 /* UIStackView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D952E605CE300C07CF2 /* UIStackView+Extension.swift */; }; BAD8B768F782046D4AA1C073 /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F3DE048BEDCB92B48F401061 /* RxCocoa.framework */; }; BE81B1F3E60D37D75A058D2B /* SnapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9CCDC15081A22BAED6318E3E /* SnapKit.framework */; }; C2B0F8237715D14D8797DBC9 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012F45F908769FA7C3C0792F /* LoginView.swift */; }; @@ -74,6 +75,7 @@ 950A0D552E5C29CC00C07CF2 /* LMTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LMTextField.swift; sourceTree = ""; }; 950A0D5F2E5C3C6D00C07CF2 /* LMButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LMButton.swift; sourceTree = ""; }; 950A0D612E5C561400C07CF2 /* LMInputField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LMInputField.swift; sourceTree = ""; }; + 950A0D952E605CE300C07CF2 /* UIStackView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackView+Extension.swift"; sourceTree = ""; }; 9CCDC15081A22BAED6318E3E /* SnapKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SnapKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BACC7259FC0C14CB352A4E6B /* OptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionView.swift; sourceTree = ""; }; C28FE6392E1612667826E5C5 /* DiaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiaryView.swift; sourceTree = ""; }; @@ -184,6 +186,14 @@ path = Component; sourceTree = ""; }; + 950A0D942E605CE100C07CF2 /* Extension */ = { + isa = PBXGroup; + children = ( + 950A0D952E605CE300C07CF2 /* UIStackView+Extension.swift */, + ); + path = Extension; + sourceTree = ""; + }; 9786E1056828B1E6D5EDAB7C /* View */ = { isa = PBXGroup; children = ( @@ -208,6 +218,7 @@ BADD047B94176A526A5B7FB2 /* Sources */ = { isa = PBXGroup; children = ( + 950A0D942E605CE100C07CF2 /* Extension */, 950A0D522E5C296400C07CF2 /* Component */, 15CD642D82E9115C7976C408 /* Assets */, 88FA467F361B11350139B775 /* Base */, @@ -336,6 +347,7 @@ 7DC59B80630854028C7C80F4 /* TuistBundle+CommonUI.swift in Sources */, F3DA12FF4D18AE405F2F6B08 /* BaseViewController.swift in Sources */, E0865F2849CCB89CC83D3B77 /* Coordinator.swift in Sources */, + 950A0D962E605CEA00C07CF2 /* UIStackView+Extension.swift in Sources */, E055AA66777B1D4CC8C884E4 /* CommonUIAssets.swift in Sources */, 950A0D4F2E5AADB500C07CF2 /* SignUpView.swift in Sources */, F7673E4248628D67F3542848 /* ChatView.swift in Sources */, diff --git a/Projects/CommonUI/Sources/Component/LMInputField.swift b/Projects/CommonUI/Sources/Component/LMInputField.swift index 777f13a..fa26590 100644 --- a/Projects/CommonUI/Sources/Component/LMInputField.swift +++ b/Projects/CommonUI/Sources/Component/LMInputField.swift @@ -9,6 +9,8 @@ import UIKit public class LMInputField: UIStackView { + public var onEmailButtonTapped: ((String) -> Void)? + public enum InputType { case email case password @@ -62,7 +64,7 @@ public class LMInputField: UIStackView { warningLabel = warningLabel.then { $0.text = waringText - $0.textColor = CommonUIAssets.LMRed2 + $0.textColor = .clear $0.font = UIFont.systemFont(ofSize: 13, weight: .light) } } @@ -88,6 +90,7 @@ public class LMInputField: UIStackView { let emailButton = LMButton(textColor: CommonUIAssets.LMBlack, bgColor: CommonUIAssets.LMOrange1).then { $0.setTitle(buttonTitle, for: .normal) + $0.addTarget(self, action: #selector(emailButtonTapped), for: .touchUpInside) } [inputTextField, emailButton] @@ -143,4 +146,39 @@ public class LMInputField: UIStackView { $0.width.equalToSuperview() } } + + @objc private func emailButtonTapped() { + onEmailButtonTapped?(inputTextField.text ?? "") + } + + public func showWarning() { + warningLabel.textColor = CommonUIAssets.LMRed2 + } + + public func hideWarning() { + warningLabel.textColor = .clear + } + + public func currentText() -> String { + return inputTextField.text ?? "" + } + + public func disableButton(buttonTitle: String) { + for subview in self.arrangedSubviews { + if let stackView = subview as? UIStackView { + for stackSubview in stackView.arrangedSubviews { + if let textField = stackSubview as? LMTextField { + textField.isEnabled = false + } + + if let button = stackSubview as? LMButton { + button.setTitle(buttonTitle, for: .normal) + button.isEnabled = false + button.backgroundColor = CommonUIAssets.LMGray5 + button.setTitleColor(CommonUIAssets.LMWhite, for: .normal) + } + } + } + } + } } diff --git a/Projects/CommonUI/Sources/Coordinator/Coordinator.swift b/Projects/CommonUI/Sources/Coordinator/Coordinator.swift index 862361a..785ae88 100644 --- a/Projects/CommonUI/Sources/Coordinator/Coordinator.swift +++ b/Projects/CommonUI/Sources/Coordinator/Coordinator.swift @@ -15,6 +15,8 @@ public enum CoordinatorType { case diary case stats case myPage + case signIn + case signUp } public protocol Coordinator: AnyObject { diff --git a/Projects/CommonUI/Sources/Extension/UIStackView+Extension.swift b/Projects/CommonUI/Sources/Extension/UIStackView+Extension.swift new file mode 100644 index 0000000..99ebac6 --- /dev/null +++ b/Projects/CommonUI/Sources/Extension/UIStackView+Extension.swift @@ -0,0 +1,17 @@ +// +// UIStackView+Extension.swift +// CommonUI +// +// Created by 박지윤 on 8/28/25. +// + +import UIKit + +extension UIStackView { + public func getText() -> String { + return arrangedSubviews + .compactMap { $0 as? LMTextField } + .map { $0.text ?? "" } + .joined(separator: " ") + } +} diff --git a/Projects/CommonUI/Sources/View/Login/SignUpView.swift b/Projects/CommonUI/Sources/View/Login/SignUpView.swift index 9b47002..fc76e7f 100644 --- a/Projects/CommonUI/Sources/View/Login/SignUpView.swift +++ b/Projects/CommonUI/Sources/View/Login/SignUpView.swift @@ -14,24 +14,24 @@ import Then open class SignUpView: UIView { var inputFieldStackView = UIStackView() - let nameInputField = LMInputField(inputText: "이름", + public let nameInputField = LMInputField(inputText: "이름", inputPlaceholder: "이름을 입력하세요", warningText: " 이름은 필수입니다") - let emailInputField = LMInputField(inputType: .email, + public let emailInputField = LMInputField(inputType: .email, inputText: "이메일", inputPlaceholder: " 이메일을 입력하세요", warningText: " 이메일 형식이 올바르지 않습니다", buttonTitle: "인증 요청") - let authenticationInputField = LMInputField(inputType: .email, + public let confirmInputField = LMInputField(inputType: .email, inputText: "인증번호", inputPlaceholder: " 인증번호를 입력하세요", warningText: " 인증번호가 일치하지 않습니다", buttonTitle: "확인") - let passwordInputField = LMInputField(inputType: .password, + public let passwordInputField = LMInputField(inputType: .password, inputText: "비밀번호", inputPlaceholder: "비밀번호를 입력하세요", warningText: " 비밀번호 형식이 올바르지 않습니다") - let passwordCheckInputField = LMInputField(inputText: "비밀번호 확인", + public let passwordCheckInputField = LMInputField(inputText: "비밀번호 확인", inputPlaceholder: "비밀번호를 한번 더 입력하세요", warningText: " 비밀번호가 일치하지 않습니다") @@ -61,7 +61,7 @@ open class SignUpView: UIView { func initUI() { self.addSubview(inputFieldStackView) - [nameInputField, emailInputField, authenticationInputField, passwordInputField, passwordCheckInputField] + [nameInputField, emailInputField, confirmInputField, passwordInputField, passwordCheckInputField] .forEach { inputFieldStackView.addArrangedSubview($0) } inputFieldStackView.snp.makeConstraints { diff --git a/Projects/Data/Data.xcodeproj/project.pbxproj b/Projects/Data/Data.xcodeproj/project.pbxproj index 15e7bed..c9e0efb 100644 --- a/Projects/Data/Data.xcodeproj/project.pbxproj +++ b/Projects/Data/Data.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ @@ -11,6 +11,8 @@ 43E9C2380F425520C1FA1AD2 /* CourseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAAC2885ACFE998578DC25E8 /* CourseDTO.swift */; }; 684AAEA9796EED3F9FC592FC /* NetworkConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D46FD93B80D052843AD5063 /* NetworkConfiguration.swift */; }; 901ACA7B98089AB702ADA830 /* Domain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06FAA1459D11CCE724C34195 /* Domain.framework */; }; + 950A0D702E5CCF0200C07CF2 /* SignRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D6F2E5CCEFD00C07CF2 /* SignRepository.swift */; }; + 950A0D902E6039D600C07CF2 /* DefaultDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D8F2E6039D300C07CF2 /* DefaultDTO.swift */; }; A334985695DC9388841BBC43 /* QuizRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCA951B89F2C3D20AA31F7F /* QuizRepository.swift */; }; B2F8FBFA915F696CCCA4152A /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A3B0D3D8C7049B6856791C1D /* Alamofire.framework */; }; E1BFC73FB539432F6E12CD94 /* CourseRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0730BC3657E24BCEA511A3C /* CourseRepository.swift */; }; @@ -41,6 +43,8 @@ 4E75197C294DE74F5162FAA7 /* Data.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Data.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5D46FD93B80D052843AD5063 /* NetworkConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConfiguration.swift; sourceTree = ""; }; 77810122262C6CB16D4D47DA /* QuizDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizDTO.swift; sourceTree = ""; }; + 950A0D6F2E5CCEFD00C07CF2 /* SignRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignRepository.swift; sourceTree = ""; }; + 950A0D8F2E6039D300C07CF2 /* DefaultDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultDTO.swift; sourceTree = ""; }; A3B0D3D8C7049B6856791C1D /* Alamofire.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Alamofire.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A44BC1E2E75FC256F832CA38 /* LoginDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginDTO.swift; sourceTree = ""; }; AAAC2885ACFE998578DC25E8 /* CourseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDTO.swift; sourceTree = ""; }; @@ -73,6 +77,7 @@ 647255CD65221C9CD4A43DED /* DTO */ = { isa = PBXGroup; children = ( + 950A0D8F2E6039D300C07CF2 /* DefaultDTO.swift */, AAAC2885ACFE998578DC25E8 /* CourseDTO.swift */, A44BC1E2E75FC256F832CA38 /* LoginDTO.swift */, 77810122262C6CB16D4D47DA /* QuizDTO.swift */, @@ -83,6 +88,7 @@ 73F3ED55BFDC2EF15878F5B6 /* Repository */ = { isa = PBXGroup; children = ( + 950A0D6F2E5CCEFD00C07CF2 /* SignRepository.swift */, B0730BC3657E24BCEA511A3C /* CourseRepository.swift */, 3D7624DE90CFBBEF778E120E /* LoginRepository.swift */, 3CCA951B89F2C3D20AA31F7F /* QuizRepository.swift */, @@ -172,8 +178,6 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - TargetAttributes = { - }; }; buildConfigurationList = BDB112DA78B2B42275EC3A62 /* Build configuration list for PBXProject "Data" */; compatibilityVersion = "Xcode 14.0"; @@ -212,8 +216,10 @@ FF43B3A4D0DC88307E918DB0 /* LoginDTO.swift in Sources */, E9463A3FF42D5F0960245F80 /* QuizDTO.swift in Sources */, 684AAEA9796EED3F9FC592FC /* NetworkConfiguration.swift in Sources */, + 950A0D702E5CCF0200C07CF2 /* SignRepository.swift in Sources */, E1BFC73FB539432F6E12CD94 /* CourseRepository.swift in Sources */, 34FD760EB97BB96E9D770BF0 /* LoginRepository.swift in Sources */, + 950A0D902E6039D600C07CF2 /* DefaultDTO.swift in Sources */, A334985695DC9388841BBC43 /* QuizRepository.swift in Sources */, E6785667C8E247C344474CBB /* TokenRepository.swift in Sources */, ); @@ -245,10 +251,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( - "$(inherited)", - DEBUG, - ); + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; SWIFT_COMPILATION_MODE = singlefile; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; diff --git a/Projects/Data/Sources/DTO/DefaultDTO.swift b/Projects/Data/Sources/DTO/DefaultDTO.swift new file mode 100644 index 0000000..f339791 --- /dev/null +++ b/Projects/Data/Sources/DTO/DefaultDTO.swift @@ -0,0 +1,21 @@ +// +// DefaultDTO.swift +// Data +// +// Created by 박지윤 on 8/28/25. +// + +import Foundation +import Domain + +public struct DefaultDTO: Decodable { + public let is_success: Bool + public let code: String + public let message: String +} + +extension DefaultDTO { + func getMessage() -> DefaultVO { + return .init(message: message) + } +} diff --git a/Projects/Data/Sources/Repository/SignRepository.swift b/Projects/Data/Sources/Repository/SignRepository.swift new file mode 100644 index 0000000..123bb61 --- /dev/null +++ b/Projects/Data/Sources/Repository/SignRepository.swift @@ -0,0 +1,75 @@ +// +// SignRepository.swift +// Data +// +// Created by 박지윤 on 8/26/25. +// + +import Domain +import RxSwift +import Alamofire + +public class DefaultSignRepository: SignRepository { + + public init() { } + + public func postEmail(email: String) -> Single { + let params = ["email": email] + return request(endpoint: "/api/auth/email", + parameters: params, + responseType: DefaultDTO.self) + .map { dto in + return dto.getMessage() + } + } + + public func postConfirm(email: String, code: String) -> Single { + let params = ["email": email, "code": code] + return request(endpoint: "/api/auth/email/confirm", + parameters: params, + responseType: DefaultDTO.self) + .map { dto in + return dto.getMessage() + } + } + + public func postSignUp(username: String, email: String, password: String) -> Single { + let params = ["username": username, "email": email, "password": password] + return request(endpoint: "/api/auth/sign-up", + parameters: params, + responseType: DefaultDTO.self) + .map { dto in + return dto.getMessage() + } + } + + private func request( + endpoint: String, + parameters: [String: Any]? = nil, + responseType: T.Type + ) -> Single { + return Single.create { single in + let url = "\(NetworkConfiguration.baseUrl)\(endpoint)" + let headers: HTTPHeaders = [:] + let request = AF.request( + url, + method: .post, + parameters: parameters, + encoding: JSONEncoding.default, + headers: headers + ) + .validate() + .responseDecodable(of: responseType) { response in + switch response.result { + case .success(let value): + print("✅ API 응답 성공: \(value)") + single(.success(value)) + case .failure(let error): + print("❌ API 응답 실패: \(error)") + single(.failure(error)) + } + } + return Disposables.create { request.cancel() } + } + } +} diff --git a/Projects/Domain/Domain.xcodeproj/project.pbxproj b/Projects/Domain/Domain.xcodeproj/project.pbxproj index b51b4c5..27d2c4a 100644 --- a/Projects/Domain/Domain.xcodeproj/project.pbxproj +++ b/Projects/Domain/Domain.xcodeproj/project.pbxproj @@ -14,7 +14,10 @@ 5EB8590C9D2C002D0BDBC021 /* CourseVO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 747DBBCAB797E5E0A82177F2 /* CourseVO.swift */; }; 69DAD609572D32F2BA3845AE /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E1DA9A49E79A791F7A526A1 /* String+Extension.swift */; }; 92BD46EE48F6C63B6E43D069 /* UIView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32AB908BB15CBDA006ADC3A1 /* UIView+Extension.swift */; }; - 94174705336F0E353686EA83 /* LoginUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 574E8F32CEB080432AB7EA94 /* LoginUseCase.swift */; }; + 950A0D692E5CCBB900C07CF2 /* SignUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D682E5CCBB400C07CF2 /* SignUseCase.swift */; }; + 950A0D6B2E5CCBF000C07CF2 /* SignRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D6A2E5CCBE800C07CF2 /* SignRepository.swift */; }; + 950A0D822E5DE10C00C07CF2 /* LoginUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D812E5DE10700C07CF2 /* LoginUseCase.swift */; }; + 950A0D922E603DA100C07CF2 /* DefaultVO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D912E603D9C00C07CF2 /* DefaultVO.swift */; }; 95369A7E2E28B8D9000C893F /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95369A7D2E28B8D1000C893F /* UIImage+Extension.swift */; }; A0CEC5A2E5A76EBD987CE37D /* StepVO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798CFC331192C9FE77F3446A /* StepVO.swift */; }; B0B0382EF5DFDACDD3EB8A75 /* QuizUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4565F6A02AFBF3D9A0D41B /* QuizUseCase.swift */; }; @@ -44,11 +47,14 @@ 24F9958F3DAC8013B440CEEA /* QuizVO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizVO.swift; sourceTree = ""; }; 2E1DA9A49E79A791F7A526A1 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; 32AB908BB15CBDA006ADC3A1 /* UIView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extension.swift"; sourceTree = ""; }; - 574E8F32CEB080432AB7EA94 /* LoginUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginUseCase.swift; sourceTree = ""; }; 6F4565F6A02AFBF3D9A0D41B /* QuizUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizUseCase.swift; sourceTree = ""; }; 747DBBCAB797E5E0A82177F2 /* CourseVO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseVO.swift; sourceTree = ""; }; 798CFC331192C9FE77F3446A /* StepVO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepVO.swift; sourceTree = ""; }; 8A2AB67DE6AA50B3229C7A86 /* LoginRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginRepository.swift; sourceTree = ""; }; + 950A0D682E5CCBB400C07CF2 /* SignUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUseCase.swift; sourceTree = ""; }; + 950A0D6A2E5CCBE800C07CF2 /* SignRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignRepository.swift; sourceTree = ""; }; + 950A0D812E5DE10700C07CF2 /* LoginUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginUseCase.swift; sourceTree = ""; }; + 950A0D912E603D9C00C07CF2 /* DefaultVO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultVO.swift; sourceTree = ""; }; 95369A7D2E28B8D1000C893F /* UIImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = ""; }; 9A2E30672E510822AAF38EAD /* RxSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RxSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; ABB8C62A6FE12783AD3819BE /* TokenUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenUseCase.swift; sourceTree = ""; }; @@ -112,8 +118,8 @@ 2BE278B80FDD95404961E580 /* UseCase */ = { isa = PBXGroup; children = ( + 950A0D672E5CCBAC00C07CF2 /* Login */, F904C836B5CD2D4DA5EAE38D /* CourseUseCase.swift */, - 574E8F32CEB080432AB7EA94 /* LoginUseCase.swift */, 6F4565F6A02AFBF3D9A0D41B /* QuizUseCase.swift */, ABB8C62A6FE12783AD3819BE /* TokenUseCase.swift */, ); @@ -137,9 +143,19 @@ name = Project; sourceTree = ""; }; + 950A0D672E5CCBAC00C07CF2 /* Login */ = { + isa = PBXGroup; + children = ( + 950A0D812E5DE10700C07CF2 /* LoginUseCase.swift */, + 950A0D682E5CCBB400C07CF2 /* SignUseCase.swift */, + ); + path = Login; + sourceTree = ""; + }; 9C17CEB3595F4C3FA69C5BAF /* VO */ = { isa = PBXGroup; children = ( + 950A0D912E603D9C00C07CF2 /* DefaultVO.swift */, 747DBBCAB797E5E0A82177F2 /* CourseVO.swift */, FD08A7186FB9676854B7AEAC /* LoginVO.swift */, 24F9958F3DAC8013B440CEEA /* QuizVO.swift */, @@ -151,6 +167,7 @@ BC1A55AF7DC675AFEA6E7C12 /* RepositoryProtocol */ = { isa = PBXGroup; children = ( + 950A0D6A2E5CCBE800C07CF2 /* SignRepository.swift */, DCA3E06ED2A8180A63919B6C /* CourseRepository.swift */, 8A2AB67DE6AA50B3229C7A86 /* LoginRepository.swift */, 1BA1471CB01ECE73BAD3D595 /* QuizRepository.swift */, @@ -231,19 +248,22 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 950A0D6B2E5CCBF000C07CF2 /* SignRepository.swift in Sources */, 69DAD609572D32F2BA3845AE /* String+Extension.swift in Sources */, + 950A0D822E5DE10C00C07CF2 /* LoginUseCase.swift in Sources */, 92BD46EE48F6C63B6E43D069 /* UIView+Extension.swift in Sources */, D510BC17C4583615CB60439E /* CourseRepository.swift in Sources */, D7012CC494E56CD8CE58176F /* LoginRepository.swift in Sources */, 1782D2A1A7FB2BD6FFA1DFEA /* QuizRepository.swift in Sources */, D81C7F2583A1BD30BD7C53DD /* TokenRepository.swift in Sources */, BBED9ACB62B271298F7028F8 /* CourseUseCase.swift in Sources */, - 94174705336F0E353686EA83 /* LoginUseCase.swift in Sources */, B0B0382EF5DFDACDD3EB8A75 /* QuizUseCase.swift in Sources */, 3E835D4F2E5F639557AC7C06 /* TokenUseCase.swift in Sources */, 5EB8590C9D2C002D0BDBC021 /* CourseVO.swift in Sources */, + 950A0D922E603DA100C07CF2 /* DefaultVO.swift in Sources */, 0DED9075FE34124C093D9FDF /* LoginVO.swift in Sources */, 95369A7E2E28B8D9000C893F /* UIImage+Extension.swift in Sources */, + 950A0D692E5CCBB900C07CF2 /* SignUseCase.swift in Sources */, CFBF3D58082C5F201FCFFD5B /* QuizVO.swift in Sources */, A0CEC5A2E5A76EBD987CE37D /* StepVO.swift in Sources */, ); diff --git a/Projects/Domain/Sources/RepositoryProtocol/SignRepository.swift b/Projects/Domain/Sources/RepositoryProtocol/SignRepository.swift new file mode 100644 index 0000000..8f231b5 --- /dev/null +++ b/Projects/Domain/Sources/RepositoryProtocol/SignRepository.swift @@ -0,0 +1,14 @@ +// +// SignRepository.swift +// Domain +// +// Created by 박지윤 on 8/26/25. +// + +import RxSwift + +public protocol SignRepository { + func postEmail(email: String) -> Single + func postConfirm(email: String, code: String) -> Single + func postSignUp(username: String, email: String, password: String) -> Single +} diff --git a/Projects/Domain/Sources/UseCase/LoginUseCase.swift b/Projects/Domain/Sources/UseCase/Login/LoginUseCase.swift similarity index 95% rename from Projects/Domain/Sources/UseCase/LoginUseCase.swift rename to Projects/Domain/Sources/UseCase/Login/LoginUseCase.swift index aba0ba8..77625bf 100644 --- a/Projects/Domain/Sources/UseCase/LoginUseCase.swift +++ b/Projects/Domain/Sources/UseCase/Login/LoginUseCase.swift @@ -2,7 +2,7 @@ // LoginUseCase.swift // Domain // -// Created by 박지윤 on 7/16/25. +// Created by 박지윤 on 8/26/25. // import RxSwift diff --git a/Projects/Domain/Sources/UseCase/Login/SignUseCase.swift b/Projects/Domain/Sources/UseCase/Login/SignUseCase.swift new file mode 100644 index 0000000..d47e6a2 --- /dev/null +++ b/Projects/Domain/Sources/UseCase/Login/SignUseCase.swift @@ -0,0 +1,34 @@ +// +// SignUseCase.swift +// Domain +// +// Created by 박지윤 on 8/26/25. +// + +import RxSwift + +public protocol SignUseCase { + func postEmail(email: String) -> Single + func postConfirm(email: String, code: String) -> Single + func postSignUp(username: String, email: String, password: String) -> Single +} + +public final class DefaultSignUseCase: SignUseCase { + let repository: SignRepository + + public init(repository: SignRepository) { + self.repository = repository + } + + public func postEmail(email: String) -> Single { + return repository.postEmail(email: email) + } + + public func postConfirm(email: String, code: String) -> Single { + return repository.postConfirm(email: email, code: code) + } + + public func postSignUp(username: String, email: String, password: String) -> Single { + return repository.postSignUp(username: username, email: email, password: password) + } +} diff --git a/Projects/Domain/Sources/VO/DefaultVO.swift b/Projects/Domain/Sources/VO/DefaultVO.swift new file mode 100644 index 0000000..3ee9714 --- /dev/null +++ b/Projects/Domain/Sources/VO/DefaultVO.swift @@ -0,0 +1,14 @@ +// +// DefaultVO.swift +// Domain +// +// Created by 박지윤 on 8/28/25. +// + +public struct DefaultVO { + public let message: String + + public init(message: String) { + self.message = message + } +} diff --git a/Projects/LearnMate/Sources/Coordinator/AppCoordinator.swift b/Projects/LearnMate/Sources/Coordinator/AppCoordinator.swift index d360751..f92d587 100644 --- a/Projects/LearnMate/Sources/Coordinator/AppCoordinator.swift +++ b/Projects/LearnMate/Sources/Coordinator/AppCoordinator.swift @@ -36,6 +36,16 @@ final class DefaultAppCoordinator: AppCoordinator{ func start() { let loginViewController = dependency.injector.resolve(LoginViewController.self) + loginViewController.onPresentLmLogin = { [weak self] in + guard let self else { return } + let signInViewController = self.dependency.injector.resolve(SignInViewController.self) + signInViewController.onPresentSignUp = { [weak self] in + guard let self else { return } + let signUpViewController = self.dependency.injector.resolve(SignUpViewController.self) + self.navigationController.pushViewController(signUpViewController, animated: true) + } + self.navigationController.pushViewController(signInViewController, animated: true) + } self.navigationController.pushViewController(loginViewController, animated: true) // setNavigationBar() // setTabBarCoordinator() diff --git a/Projects/LearnMate/Sources/DI/DataAssembly.swift b/Projects/LearnMate/Sources/DI/DataAssembly.swift index 1e8aec7..2e969cf 100644 --- a/Projects/LearnMate/Sources/DI/DataAssembly.swift +++ b/Projects/LearnMate/Sources/DI/DataAssembly.swift @@ -28,5 +28,9 @@ public struct DataAssembly: Assembly { let tokenRepository = resolver.resolve(TokenRepository.self)! return DefaultQuizRepository(tokenRepository: tokenRepository) } + + container.register(SignRepository.self) { _ in + return DefaultSignRepository() + } } } diff --git a/Projects/LearnMate/Sources/DI/DomainAssembly.swift b/Projects/LearnMate/Sources/DI/DomainAssembly.swift index e7d7507..6fdb825 100644 --- a/Projects/LearnMate/Sources/DI/DomainAssembly.swift +++ b/Projects/LearnMate/Sources/DI/DomainAssembly.swift @@ -29,5 +29,10 @@ public struct DomainAssembly: Assembly { let repository = resolver.resolve(QuizRepository.self)! return DefaultQuizUseCase(repository: repository) } + + container.register(SignUseCase.self) { resolver in + let repository = resolver.resolve(SignRepository.self)! + return DefaultSignUseCase(repository: repository) + } } } diff --git a/Projects/LearnMate/Sources/DI/LoginAssembly.swift b/Projects/LearnMate/Sources/DI/LoginAssembly.swift index c72a932..f3981cc 100644 --- a/Projects/LearnMate/Sources/DI/LoginAssembly.swift +++ b/Projects/LearnMate/Sources/DI/LoginAssembly.swift @@ -24,5 +24,21 @@ public struct LoginAssembly: Assembly { let loginViewModel = resolver.resolve(LoginViewModel.self)! return LoginViewController(loginViewModel: loginViewModel) } + + /// Sign DI 등록 + container.register(SignViewModel.self) { resolver in + let useCase = resolver.resolve(SignUseCase.self)! + return SignViewModel(signUseCase: useCase) + } + + container.register(SignInViewController.self) { resolver in + let vm = resolver.resolve(SignViewModel.self)! + return SignInViewController(signViewModel: vm) + } + + container.register(SignUpViewController.self) { resolver in + let vm = resolver.resolve(SignViewModel.self)! + return SignUpViewController(signViewModel: vm) + } } } diff --git a/Projects/Login/Login.xcodeproj/project.pbxproj b/Projects/Login/Login.xcodeproj/project.pbxproj index c1431fd..15f0774 100644 --- a/Projects/Login/Login.xcodeproj/project.pbxproj +++ b/Projects/Login/Login.xcodeproj/project.pbxproj @@ -14,6 +14,9 @@ 92D7E2A0C6D4E3A6F57BAFB0 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C32E71FB3926A3A3118D0096 /* LoginViewModel.swift */; }; 950A0D5E2E5C3A6D00C07CF2 /* SignInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D5D2E5C3A6700C07CF2 /* SignInViewController.swift */; }; 950A0D642E5C5F7D00C07CF2 /* SignUpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D632E5C5F7900C07CF2 /* SignUpViewController.swift */; }; + 950A0D662E5CCB2F00C07CF2 /* SignViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D652E5CCB2900C07CF2 /* SignViewModel.swift */; }; + 950A0D862E5DE6D000C07CF2 /* SignInCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D852E5DE6C600C07CF2 /* SignInCoordinator.swift */; }; + 950A0D8A2E5E0C8600C07CF2 /* SignUpCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A0D892E5E0C8000C07CF2 /* SignUpCoordinator.swift */; }; 9543B5BD011752760C9E8D48 /* Common.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A3E03A11BBC654DEC110ECE /* Common.framework */; }; A2DD99D036B33B4B67E19670 /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 149968626029AF0A6483A522 /* RxSwift.framework */; }; A60EB3815F8DEA7C7B2A1E4D /* SnapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A78DFB39E6F2ADF7C38CFA79 /* SnapKit.framework */; }; @@ -66,6 +69,9 @@ 7D44C290B1A433AEF98F1870 /* Alamofire.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Alamofire.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 950A0D5D2E5C3A6700C07CF2 /* SignInViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInViewController.swift; sourceTree = ""; }; 950A0D632E5C5F7900C07CF2 /* SignUpViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpViewController.swift; sourceTree = ""; }; + 950A0D652E5CCB2900C07CF2 /* SignViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignViewModel.swift; sourceTree = ""; }; + 950A0D852E5DE6C600C07CF2 /* SignInCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInCoordinator.swift; sourceTree = ""; }; + 950A0D892E5E0C8000C07CF2 /* SignUpCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpCoordinator.swift; sourceTree = ""; }; 9A9C214589834256AC9DD6AB /* Then.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Then.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A6D197429DF4204F655968BA /* LoginTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LoginTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; A78DFB39E6F2ADF7C38CFA79 /* SnapKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SnapKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -145,6 +151,7 @@ isa = PBXGroup; children = ( C32E71FB3926A3A3118D0096 /* LoginViewModel.swift */, + 950A0D652E5CCB2900C07CF2 /* SignViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -152,12 +159,22 @@ 82158BAC7310E834F5971A06 /* Sources */ = { isa = PBXGroup; children = ( + 950A0D842E5DE4F000C07CF2 /* Coordinator */, 04AE692FD1ACBC62C53BE327 /* View */, 787BEC2C2BCE59B03D483875 /* ViewModel */, ); path = Sources; sourceTree = ""; }; + 950A0D842E5DE4F000C07CF2 /* Coordinator */ = { + isa = PBXGroup; + children = ( + 950A0D852E5DE6C600C07CF2 /* SignInCoordinator.swift */, + 950A0D892E5E0C8000C07CF2 /* SignUpCoordinator.swift */, + ); + path = Coordinator; + sourceTree = ""; + }; A19BD00EC1C84FD92121B04F /* Project */ = { isa = PBXGroup; children = ( @@ -269,8 +286,11 @@ buildActionMask = 2147483647; files = ( 4027F4EA82D89F9520C87D34 /* LoginViewController.swift in Sources */, + 950A0D8A2E5E0C8600C07CF2 /* SignUpCoordinator.swift in Sources */, 92D7E2A0C6D4E3A6F57BAFB0 /* LoginViewModel.swift in Sources */, + 950A0D662E5CCB2F00C07CF2 /* SignViewModel.swift in Sources */, 950A0D642E5C5F7D00C07CF2 /* SignUpViewController.swift in Sources */, + 950A0D862E5DE6D000C07CF2 /* SignInCoordinator.swift in Sources */, 950A0D5E2E5C3A6D00C07CF2 /* SignInViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Projects/Login/Sources/Coordinator/SignInCoordinator.swift b/Projects/Login/Sources/Coordinator/SignInCoordinator.swift new file mode 100644 index 0000000..48b3bc7 --- /dev/null +++ b/Projects/Login/Sources/Coordinator/SignInCoordinator.swift @@ -0,0 +1,54 @@ +// +// SignInCoordinator.swift +// Login +// +// Created by 박지윤 on 8/26/25. +// + +import CommonUI +import UIKit + +public protocol SignInCoordinator: Coordinator { + func showSignInFlow() +} + +public class DefaultSignInCoordinator: SignInCoordinator { + public struct Dependency { + let signInViewController: SignInViewController + let navigationController: UINavigationController + weak var finishDelegate: CoordinatorFinishDelegate? + + public init(signInViewController: SignInViewController, + navigationController: UINavigationController, + finishDelegate: CoordinatorFinishDelegate? = nil) { + self.signInViewController = signInViewController + self.navigationController = navigationController + self.finishDelegate = finishDelegate + } + } + + let dependency: Dependency + public var childCoordinators: [Coordinator] = [] + public var navigationController: UINavigationController + public var type: CoordinatorType = .signIn + public var finishDelegate: CoordinatorFinishDelegate? + + public init(dependency: Dependency) { + self.dependency = dependency + self.navigationController = dependency.navigationController + self.finishDelegate = dependency.finishDelegate + dependency.signInViewController.viewModel.signInViewCoordinator = self + } + + public func start() { + setNavigationBar() + showSignInFlow() + } + + func setNavigationBar() { + } + + public func showSignInFlow() { + navigationController.pushViewController(dependency.signInViewController, animated: true) + } +} diff --git a/Projects/Login/Sources/Coordinator/SignUpCoordinator.swift b/Projects/Login/Sources/Coordinator/SignUpCoordinator.swift new file mode 100644 index 0000000..b34f6ad --- /dev/null +++ b/Projects/Login/Sources/Coordinator/SignUpCoordinator.swift @@ -0,0 +1,54 @@ +// +// SignUpCoordinator.swift +// Login +// +// Created by 박지윤 on 8/27/25. +// + +import CommonUI +import UIKit + +public protocol SignUpCoordinator: Coordinator { + func showSignUpFlow() +} + +public class DefaultSignUpCoordinator: SignUpCoordinator { + public struct Dependency { + let signUpViewController: SignUpViewController + let navigationController: UINavigationController + weak var finishDelegate: CoordinatorFinishDelegate? + + public init(signUpViewController: SignUpViewController, + navigationController: UINavigationController, + finishDelegate: CoordinatorFinishDelegate? = nil) { + self.signUpViewController = signUpViewController + self.navigationController = navigationController + self.finishDelegate = finishDelegate + } + } + + let dependency: Dependency + public var childCoordinators: [Coordinator] = [] + public var navigationController: UINavigationController + public var type: CoordinatorType = .signUp + public var finishDelegate: CoordinatorFinishDelegate? + + public init(dependency: Dependency) { + self.dependency = dependency + self.navigationController = dependency.navigationController + self.finishDelegate = dependency.finishDelegate + dependency.signUpViewController.viewModel.signUpViewCoordinator = self + } + + public func start() { + setNavigationBar() + showSignUpFlow() + } + + func setNavigationBar() { + } + + public func showSignUpFlow() { + navigationController.pushViewController(dependency.signUpViewController, animated: true) + } +} diff --git a/Projects/Login/Sources/View/LoginViewController.swift b/Projects/Login/Sources/View/LoginViewController.swift index 92eae3c..bf99654 100644 --- a/Projects/Login/Sources/View/LoginViewController.swift +++ b/Projects/Login/Sources/View/LoginViewController.swift @@ -15,6 +15,7 @@ import AuthenticationServices public class LoginViewController: BaseViewController, SFSafariViewControllerDelegate, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { let viewModel: LoginViewModel let loginView = LoginView() + public var onPresentLmLogin: (() -> Void)? public init(loginViewModel: LoginViewModel) { self.viewModel = loginViewModel @@ -61,8 +62,7 @@ public class LoginViewController: BaseViewController, SFSafariViewControllerDele } private func presentLmLogin() { - let signInViewController = SignInViewController() - self.navigationController?.pushViewController(signInViewController, animated: true) + onPresentLmLogin?() } private func presentGoogleLogin() { diff --git a/Projects/Login/Sources/View/SignInViewController.swift b/Projects/Login/Sources/View/SignInViewController.swift index f3f81b1..e6c5871 100644 --- a/Projects/Login/Sources/View/SignInViewController.swift +++ b/Projects/Login/Sources/View/SignInViewController.swift @@ -11,16 +11,18 @@ import SnapKit import RxSwift public class SignInViewController: BaseViewController { -// let viewModel: LoginViewModel + let viewModel: SignViewModel + let navigationBar = DefaultNavigationBar(leftImage: CommonUIAssets.IconBack ?? nil, rightImage: nil, title: nil, isRightButtonHidden: true) let signInView = SignInView() + public var onPresentSignUp: (() -> Void)? - public override init() { -// self.viewModel = loginViewModel + public init(signViewModel: SignViewModel) { + self.viewModel = signViewModel super.init() } @@ -48,8 +50,9 @@ public class SignInViewController: BaseViewController { } private func presentSignUp() { - let signUpViewController = SignUpViewController() - self.navigationController?.pushViewController(signUpViewController, animated: true) + onPresentSignUp?() +// let signUpViewController = SignUpViewController(signViewModel: viewModel) +// self.navigationController?.pushViewController(signUpViewController, animated: true) } private func bindTransition() { diff --git a/Projects/Login/Sources/View/SignUpViewController.swift b/Projects/Login/Sources/View/SignUpViewController.swift index 50901a7..7d1abfe 100644 --- a/Projects/Login/Sources/View/SignUpViewController.swift +++ b/Projects/Login/Sources/View/SignUpViewController.swift @@ -11,7 +11,9 @@ import SnapKit import RxSwift public class SignUpViewController: BaseViewController { - // let viewModel: LoginViewModel + let viewModel: SignViewModel + public weak var signUpViewCoordinator: SignUpCoordinator? + let navigationBar = DefaultNavigationBar(leftImage: CommonUIAssets.IconBack ?? nil, rightImage: nil, title: "회원가입", @@ -24,9 +26,10 @@ public class SignUpViewController: BaseViewController { bgColor: CommonUIAssets.LMOrange1).then { $0.setTitle("회원가입", for: .normal) } + private var email: String = "" - public override init() { -// self.viewModel = loginViewModel + public init(signViewModel: SignViewModel) { + self.viewModel = signViewModel super.init() } @@ -42,14 +45,88 @@ public class SignUpViewController: BaseViewController { super.viewDidLoad() bindActions() bindTransition() + setupKeyboardDismissGesture() + + viewModel.onEmailSuccess = { [weak self] in + DispatchQueue.main.async { + self?.signUpView.emailInputField.disableButton(buttonTitle: "전송 완료") + } + } + + viewModel.onConfirmSuccess = { [weak self] in + DispatchQueue.main.async { + self?.signUpView.confirmInputField.hideWarning() + self?.signUpView.confirmInputField.disableButton(buttonTitle: "인증 완료") + } + } + + viewModel.onConfirmFailure = { [weak self] in + DispatchQueue.main.async { + self?.signUpView.confirmInputField.showWarning() + } + } } private func bindActions() { + signUpView.emailInputField.onEmailButtonTapped = { [weak self] email in + guard let self = self else { return } + + if self.isValidEmail(email) { + print("유효한 이메일: \(email)") + self.email = email + self.viewModel.postEmail(email: email) + self.signUpView.emailInputField.hideWarning() + } else { + print("유효하지 않은 이메일: \(email)") + self.signUpView.emailInputField.showWarning() + } + } + + signUpView.confirmInputField.onEmailButtonTapped = { [weak self] code in + guard let self = self else { return } + + self.viewModel.postConfirm(email: email, code: code) + } + + signUpButton.rx.tap + .subscribe(onNext: { [weak self] in + guard let self = self else { return } + + let name = self.signUpView.nameInputField.currentText() + let email = self.signUpView.emailInputField.currentText() + let password = self.signUpView.passwordInputField.currentText() + let confirm = self.signUpView.passwordCheckInputField.currentText() + + print("회원가입 버튼 탭 - name: \(name), email: \(email), password: \(password), confirm: \(confirm)") + + // 조건 체크 + let isNameValid = !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + let isEmailVerified = !self.email.isEmpty // 이메일 인증 완료 여부 + let isPasswordValid = self.isValidPassword(password) + let isConfirmValid = !password.isEmpty && password == confirm + + if isNameValid && isEmailVerified && isPasswordValid && isConfirmValid { + self.postSignUp(username: name, email: email, password: password) + } else { + print("회원가입 조건 미충족") + } + }) + .disposed(by: disposeBag) + } + + private func postSignUp(username: String, email: String, password: String) { + self.viewModel.postSignUp(username: username, email: email, password: password) } private func bindTransition() { } + private func isValidEmail(_ email: String) -> Bool { + let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex) + return emailPredicate.evaluate(with: email) + } + public override func setupViewProperty() { view.backgroundColor = CommonUIAssets.LMWhite } @@ -64,6 +141,46 @@ public class SignUpViewController: BaseViewController { public override func setupDelegate() { } + + private func setupKeyboardDismissGesture() { + let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + tap.cancelsTouchesInView = false + view.addGestureRecognizer(tap) + } + + @objc private func dismissKeyboard() { + view.endEditing(true) + validateOnKeyboardDismiss() + } + + private func validateOnKeyboardDismiss() { + let name = signUpView.nameInputField.currentText() + if name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + signUpView.nameInputField.showWarning() + } else { + signUpView.nameInputField.hideWarning() + } + + let password = signUpView.passwordInputField.currentText() + if isValidPassword(password) { + signUpView.passwordInputField.hideWarning() + } else { + signUpView.passwordInputField.showWarning() + } + + let confirm = signUpView.passwordCheckInputField.currentText() + if !password.isEmpty && password == confirm { + signUpView.passwordCheckInputField.hideWarning() + } else { + signUpView.passwordCheckInputField.showWarning() + } + } + + private func isValidPassword(_ password: String) -> Bool { + let passwordRegex = "^(?=.*[a-z])(?=.*\\d)(?=.*[!@#$%^&*(),.?\":{}|<>])[A-Za-z\\d!@#$%^&*(),.?\":{}|<>]{8,}$" + let predicate = NSPredicate(format: "SELF MATCHES %@", passwordRegex) + return predicate.evaluate(with: password) + } public override func setupLayout() { navigationBar.snp.makeConstraints { diff --git a/Projects/Login/Sources/ViewModel/SignViewModel.swift b/Projects/Login/Sources/ViewModel/SignViewModel.swift new file mode 100644 index 0000000..4c8cf55 --- /dev/null +++ b/Projects/Login/Sources/ViewModel/SignViewModel.swift @@ -0,0 +1,69 @@ +// +// SignViewModel.swift +// Login +// +// Created by 박지윤 on 8/26/25. +// + +import Domain +import RxSwift +import RxRelay + +protocol SignViewModelProtocol { + func postEmail(email: String) + func postConfirm(email: String, code: String) + func postSignUp(username: String, email: String, password: String) +} + +public class SignViewModel: SignViewModelProtocol { + private let disposeBag = DisposeBag() + private let signUseCase: SignUseCase + public weak var signInViewCoordinator: SignInCoordinator? + public weak var signUpViewCoordinator: SignUpCoordinator? + public var onEmailSuccess: (() -> Void)? + public var onConfirmSuccess: (() -> Void)? + public var onConfirmFailure: (() -> Void)? + public let emailVerified = BehaviorRelay(value: false) + + public init(signUseCase: SignUseCase) { + self.signUseCase = signUseCase + } + + func postEmail(email: String) { + signUseCase.postEmail(email: email) + .subscribe(onSuccess: { [weak self] response in + print("이메일 전송 성공: \(response.message)") + self?.onEmailSuccess?() + }, onFailure: { error in + print("이메일 전송 실패: \(error)") + }) + .disposed(by: disposeBag) + } + + func postConfirm(email: String, code: String) { + signUseCase.postConfirm(email: email, code: code) + .subscribe(onSuccess: { [weak self] response in + guard let self = self else { return } + print("이메일 인증 성공: \(response.message)") + self.onConfirmSuccess?() + self.emailVerified.accept(true) + }, onFailure: { [weak self] error in + guard let self = self else { return } + print("이메일 인증 실패: \(error)") + self.onConfirmFailure?() + }) + .disposed(by: disposeBag) + } + + func postSignUp(username: String, email: String, password: String) { + signUseCase.postSignUp(username: username, email: email, password: password) + .subscribe(onSuccess: { [weak self] response in + guard let self = self else { return } + print("회원가입 성공: \(response.message)") + }, onFailure: { [weak self] error in + guard let self = self else { return } + print("회원가입 실패: \(error)") + }) + .disposed(by: disposeBag) + } +}