Skip to content

Commit

Permalink
Merge pull request #474 from sopt-makers/feat/#467-signup_ui
Browse files Browse the repository at this point in the history
[Feat] #467 - 회원가입 UI 구현
  • Loading branch information
meltsplit authored Jan 22, 2025
2 parents 526ef2c + b365451 commit fe948e7
Show file tree
Hide file tree
Showing 54 changed files with 1,624 additions and 176 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//
// Publisher+WithLatestFrom.swift
// Core
//
// Created by 장석우 on 1/9/25.
// Copyright © 2025 SOPT-iOS. All rights reserved.
//

import Foundation
import Combine

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension Publishers {

public struct WithLatestFrom<Upstream, Other> : Publisher where Upstream : Publisher, Other: Publisher, Upstream.Failure == Other.Failure {

public typealias Output = Other.Output
public typealias Failure = Upstream.Failure

public let upstream: Upstream
public let other: Other

public init(upstream: Upstream, other: Other) {
self.upstream = upstream
self.other = other
}

public func receive<S>(subscriber: S) where S : Subscriber, Self.Output == S.Input, Self.Failure == S.Failure {
let merged = mergeStream(upstream, other)
let result = resultStream(from: merged)
result.subscribe(subscriber)
}
}
}

extension Publishers.WithLatestFrom {

enum MergedOutput {
case upstream(Upstream.Output)
case other(Other.Output)
}

typealias ScanResult = (upstream: Upstream.Output?, other: Other.Output?, shouldEmit: Bool)

func mergeStream(_ upstream: Upstream, _ other: Other) -> AnyPublisher<MergedOutput, Failure> {
let upstream = upstream.map { MergedOutput.upstream($0)}
let other = other.map { MergedOutput.other($0)}
return Publishers.Merge(upstream, other).eraseToAnyPublisher()
}

func resultStream(from mergedStream: AnyPublisher<MergedOutput, Failure>) -> AnyPublisher<Output, Failure> {
mergedStream
.scan(nil) { result, mergedOutput -> ScanResult? in
var upstream: Upstream.Output?
var other: Other.Output?
let shouldEmit: Bool

switch mergedOutput {
case let .upstream(output):
upstream = output
shouldEmit = result?.other != nil

case let .other(output):
other = output
shouldEmit = false
}

return ScanResult(
upstream: upstream ?? result?.upstream,
other: other ?? result?.other,
shouldEmit: shouldEmit
)
}
.compactMap { $0 }
.filter { $0.shouldEmit }
.compactMap { $0.other }
.eraseToAnyPublisher()
}
}

extension Publisher {
public func withLatestFrom<P>(_ other: P) -> Publishers.WithLatestFrom<Self, P> where P : Publisher, Self.Failure == P.Failure {
return .init(upstream: self, other: other)
}
}


19 changes: 19 additions & 0 deletions SOPT-iOS/Projects/Core/Sources/Extension/Foundation+/Int+.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// Int+.swift
// Core
//
// Created by 장석우 on 1/6/25.
// Copyright © 2025 SOPT-iOS. All rights reserved.
//

import Foundation

extension Int {

public var to_mmss: String {
// let hours = Int(self) / 3600
let minutes = Int(self) / 60 % 60
let seconds = Int(self) % 60
return String(format: "%02i:%02i", minutes, seconds)
}
}
17 changes: 17 additions & 0 deletions SOPT-iOS/Projects/Core/Sources/Utils/addToolBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@
import Foundation
import UIKit

public extension UITextField {
func addToolbar() {
let toolBarKeyboard = UIToolbar()
toolBarKeyboard.sizeToFit()
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let btnDoneBar = UIBarButtonItem(title: "완료", style: .done, target: self, action: #selector(self.dismissKeyBoard))
toolBarKeyboard.items = [flexSpace, btnDoneBar]
self.inputAccessoryView = toolBarKeyboard

}

@objc func dismissKeyBoard() {
self.endEditing(true)
}
}


public extension UIViewController {

func addToolbar(textfields: [UITextField]) {
Expand Down
3 changes: 3 additions & 0 deletions SOPT-iOS/Projects/Domain/Sources/Error/PhoneVerifyError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
import Foundation

public enum PhoneVerifyError: Error {
case userNotFound
case alreadyExist
case invalidVerifyCode //인증 내역은 유효하나 제출한 “인증 번호”가 일치하지 않을 경우
case timeout // 시간 초과
case unknown(Error)
}
13 changes: 13 additions & 0 deletions SOPT-iOS/Projects/Domain/Sources/Model/PhoneVerifyModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,24 @@ public struct PhoneSendModel {
let name: String?
let phone: String
let type: PhoneVerifyType

public init(name: String?, phone: String, type: PhoneVerifyType) {
self.name = name
self.phone = phone
self.type = type
}
}

public struct PhoneVerifyModel {
let name: String?
let phone: String
let code: String
let type: PhoneVerifyType

public init(name: String?, phone: String, code: String, type: PhoneVerifyType) {
self.name = name
self.phone = phone
self.code = code
self.type = type
}
}
31 changes: 29 additions & 2 deletions SOPT-iOS/Projects/Domain/Sources/UseCase/PhoneVerifyUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,27 @@ import Combine

import Core

public struct PhoneVerifyPolicy {
public let phoneMaxLength: Int
public let codeMaxLength: Int
private let _timeLimit: Duration
public var timeLimit: Int { Int(_timeLimit.components.seconds) }

public init(phoneMaxLength: Int, codeMaxLength: Int, timeLimit: Duration) {
self.phoneMaxLength = phoneMaxLength
self.codeMaxLength = codeMaxLength
self._timeLimit = timeLimit
}
}

extension PhoneVerifyPolicy {
static let `default` = Self(phoneMaxLength: 11, codeMaxLength: 6, timeLimit: .seconds(180))
static let stub = Self(phoneMaxLength: 11, codeMaxLength: 6, timeLimit: .seconds(10))
}

public protocol PhoneVerifyUseCase {
var policy: PhoneVerifyPolicy { get }

var sideEffect: PassthroughSubject<PhoneVerifyError, Never> { get }

func send(_ model: PhoneSendModel) -> AnyPublisher<Void, Never>
Expand All @@ -21,6 +41,8 @@ public protocol PhoneVerifyUseCase {
public struct DefaultPhoneVerifyUseCase: PhoneVerifyUseCase {

private let repository: PhoneVerifyRepositoryInterface

public let policy: PhoneVerifyPolicy = .default
public let sideEffect = PassthroughSubject<PhoneVerifyError, Never>()

init(repository: PhoneVerifyRepositoryInterface) {
Expand All @@ -46,14 +68,19 @@ public struct DefaultPhoneVerifyUseCase: PhoneVerifyUseCase {

}

public struct StubPhoneVerifyUseCase: PhoneVerifyUseCase {
public class StubPhoneVerifyUseCase: PhoneVerifyUseCase {

public init() { }

public let policy: PhoneVerifyPolicy = .stub
public var sideEffect = PassthroughSubject<PhoneVerifyError, Never>()

public func send(_ model: PhoneSendModel) -> AnyPublisher<Void, Never> {
return Just(()).eraseToAnyPublisher()
}

public func verify(_ model: PhoneVerifyModel) -> AnyPublisher<Void, Never> {
return Just(()).eraseToAnyPublisher()
sideEffect.send(.userNotFound)
return Empty().eraseToAnyPublisher()
}
}
13 changes: 13 additions & 0 deletions SOPT-iOS/Projects/Domain/Sources/UseCase/SignUpUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// SignUpUseCase.swift
// Domain
//
// Created by 장석우 on 1/4/25.
// Copyright © 2025 SOPT-iOS. All rights reserved.
//

import Foundation

public protocol SignUpUseCase {

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public protocol AuthFeatureViewBuildable {
func makeSignIn() -> SignInPresentable
func makeLoginHelpBottomSheet() -> LoginHelpBottomSheetPresentable
func makeUserNotFound() -> UserNotFoundPresentable
func makeSignUp() -> SignUpPresentable
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public protocol SignInCoordinatable {
var onLoginHelpButtonTapped: (() -> Void)? { get set }
var onVisitorButtonTapped: (() -> Void)? { get set }
var onSocialLoginFail: (() -> Void)? { get set }
var onSignUpButtonTapped: (() -> Void)? { get set }
}

public typealias SignInViewModelType = ViewModelType & SignInCoordinatable
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// SignUpPhoneVerifyViewControllable.swift
// AuthFeature
//
// Created by 장석우 on 12/20/24.
// Copyright © 2024 SOPT-iOS. All rights reserved.
//

import BaseFeatureDependency
import Core
import Domain

public protocol SignUpViewControllable: ViewControllable {}

public protocol SignUpCoordinatable {
var onLoginHelpButtonTapped: (() -> Void)? { get set }
}

public typealias PhoneVerifyViewModelType = ViewModelType

public typealias SignUpViewModelType = ViewModelType & SignUpCoordinatable

public typealias SignUpPresentable = (vc: SignUpViewControllable, vm: any SignUpViewModelType)
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,12 @@ extension AuthBuilder: AuthFeatureViewBuildable {
public func makeUserNotFound() -> UserNotFoundPresentable {
return UserNotFoundVC()
}

public func makeSignUp() -> SignUpPresentable {
let useCase = StubPhoneVerifyUseCase() // TODO
let vm = SignUpViewModel(useCase: useCase)
let subVM = PhoneVerifyViewModel(useCase: useCase)
let vc = SignUpVC(viewModel: vm, phoneVerifyViewModel: subVM)
return (vc, vm)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ public final class AuthCoordinator: DefaultAuthCoordinator {
self?.runUserNotFoundFlow()
}

signIn.vm.onSignUpButtonTapped = { [weak self] in
self?.runSignUpFlow()
}

switch style {
case .modal:
router.present(
Expand Down Expand Up @@ -119,6 +123,16 @@ extension AuthCoordinator {
self.router.push(userNotFoundVC)
}

private func runSignUpFlow() {
var signUpVC = self.factory.makeSignUp()

signUpVC.vm.onLoginHelpButtonTapped = { [weak self] in
self?.showLoginHelpBottomSheet(on: signUpVC.vc)
}

self.router.push(signUpVC.vc)
}

private func showLoginHelpBottomSheet(on vc: ViewControllable) {
guard let bottomSheetVC = self.factory.makeLoginHelpBottomSheet().viewController as? LoginHelpBottomSheetVC
else { return Void() }
Expand Down
Loading

0 comments on commit fe948e7

Please sign in to comment.