Skip to content

Commit

Permalink
Merge branch 'feat/#465-signup' into feat/#468-apple-login-api
Browse files Browse the repository at this point in the history
  • Loading branch information
meltsplit committed Jan 22, 2025
2 parents 5358266 + fe948e7 commit 19c4098
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 43 deletions.
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)
}
19 changes: 9 additions & 10 deletions SOPT-iOS/Projects/Domain/Sources/UseCase/PhoneVerifyUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@ import Combine
import Core

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

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

extension PhoneVerifyPolicy {
static let `default` = Self(phoneNumberCount: 11, timeLimit: .seconds(180))
static let stub = Self(phoneNumberCount: 11, timeLimit: .seconds(10))
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 {
Expand Down Expand Up @@ -78,10 +80,7 @@ public class StubPhoneVerifyUseCase: PhoneVerifyUseCase {
}

public func verify(_ model: PhoneVerifyModel) -> AnyPublisher<Void, Never> {
return Just(()).eraseToAnyPublisher()
sideEffect.send(.userNotFound)
return Empty().eraseToAnyPublisher()
}
}

extension StubPhoneVerifyUseCase: SignUpUseCase {

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@

import UIKit

import BaseFeatureDependency
import DSKit
import Core


final class PhoneVerifyView: UIView {

public var viewModelInput: PhoneVerifyViewModel.Input {
return .init(
sendButtonTapped: sendButton.publisher(for: .touchUpInside).mapVoid().asDriver(),
doneButtonTapped: doneButton.publisher(for: .touchUpInside).mapVoid().asDriver(),
phoneTextFieldText: phoneTextField.publisher(for: .editingChanged).map { $0.text ?? "" }.asDriver(),
codeTextFieldText: codeTextField.publisher(for: .editingChanged).map { $0.text ?? "" }.asDriver(),
phoneTextFieldText: phoneTextField.publisher(for: .editingChanged).compactMap { $0.text }.asDriver(),
codeTextFieldText: codeTextField.publisher(for: .editingChanged).compactMap { $0.text }.asDriver(),
loginHelpButtonTapped: helpView.gesture().mapVoid().asDriver()
)
}
Expand Down Expand Up @@ -52,10 +54,24 @@ final class PhoneVerifyView: UIView {
$0.textColor = DSKitAsset.Colors.gray10.color
$0.layer.cornerRadius = 10
$0.layer.masksToBounds = true
$0.layer.borderWidth = 1
$0.layer.borderColor = UIColor.clear.cgColor
$0.addToolbar()
$0.addLeftPadding(width: 20)
}

private let phoneFailIcon = UIImageView().then {
$0.image = DSKitAsset.Assets.alertCircle.image.withTintColor(DSKitAsset.Colors.error.color)
$0.contentMode = .scaleAspectFit
$0.isHidden = true
}

private let phoneFailLabel = UILabel().then {
$0.font = DSKitFontFamily.Suit.semiBold.font(size: 12)
$0.textColor = DSKitAsset.Colors.error.color
$0.isHidden = true
}

private let sendButton = AppImageTextButton(title: "전송하기").then {
$0.layer.cornerRadius = 10
$0.layer.masksToBounds = true
Expand All @@ -82,13 +98,13 @@ final class PhoneVerifyView: UIView {
$0.text = "03:00"
}

private let failIcon = UIImageView().then {
private let codeFailIcon = UIImageView().then {
$0.image = DSKitAsset.Assets.alertCircle.image.withTintColor(DSKitAsset.Colors.error.color)
$0.contentMode = .scaleAspectFit
$0.isHidden = true
}

private let failLabel = UILabel().then {
private let codeFailLabel = UILabel().then {
$0.font = DSKitFontFamily.Suit.semiBold.font(size: 12)
$0.textColor = DSKitAsset.Colors.error.color
$0.isHidden = true
Expand Down Expand Up @@ -153,10 +169,12 @@ final class PhoneVerifyView: UIView {
descriptionLabel,
phoneLabel,
phoneTextField,
phoneFailIcon,
phoneFailLabel,
sendButton,
codeTextField,
failIcon,
failLabel,
codeFailIcon,
codeFailLabel,
helpView,
doneButton
)
Expand Down Expand Up @@ -193,6 +211,18 @@ final class PhoneVerifyView: UIView {
$0.height.equalTo(48)
}

phoneFailIcon.snp.makeConstraints {
$0.top.equalTo(phoneTextField.snp.bottom).offset(8)
$0.leading.equalTo(phoneTextField)
$0.size.equalTo(14)
}

phoneFailLabel.snp.makeConstraints {
$0.centerY.equalTo(phoneFailIcon)
$0.leading.equalTo(phoneFailIcon.snp.trailing).offset(4)
$0.trailing.equalTo(codeTextField)
}

sendButton.snp.makeConstraints {
$0.centerY.equalTo(phoneTextField)
$0.leading.equalTo(phoneTextField.snp.trailing).offset(7)
Expand All @@ -202,7 +232,7 @@ final class PhoneVerifyView: UIView {
}

codeTextField.snp.makeConstraints {
$0.top.equalTo(phoneTextField.snp.bottom).offset(13)
$0.top.equalTo(phoneFailLabel.snp.bottom).offset(10)
$0.leading.trailing.equalToSuperview().inset(24)
$0.height.equalTo(phoneTextField)
$0.bottom.lessThanOrEqualToSuperview()
Expand All @@ -213,20 +243,20 @@ final class PhoneVerifyView: UIView {
$0.trailing.equalToSuperview().inset(16)
}

failIcon.snp.makeConstraints {
codeFailIcon.snp.makeConstraints {
$0.top.equalTo(codeTextField.snp.bottom).offset(8)
$0.leading.equalTo(codeTextField)
$0.size.equalTo(14)
}

failLabel.snp.makeConstraints {
$0.centerY.equalTo(failIcon)
$0.leading.equalTo(failIcon.snp.trailing).offset(4)
codeFailLabel.snp.makeConstraints {
$0.centerY.equalTo(codeFailIcon)
$0.leading.equalTo(codeFailIcon.snp.trailing).offset(4)
$0.trailing.equalTo(codeTextField)
}

helpView.snp.makeConstraints {
$0.top.equalTo(failLabel.snp.bottom).offset(20)
$0.top.equalTo(codeFailLabel.snp.bottom).offset(20)
$0.leading.trailing.equalTo(codeTextField)
}

Expand Down Expand Up @@ -272,15 +302,25 @@ extension PhoneVerifyView {
.withUnretained(self)
.sink { owner, isSent in
let text = isSent ? "재전송하기" : "전송하기"
ToastUtils.showMDSToast(type: .success, text: "인증번호가 전송되었어요.")
owner.sendButton.updateTitle(text)
owner.codeTextField.isHidden = !isSent
}
.store(in: cancelBag)

output.showToast
output.phoneTextFieldText
.asDriver()
.withUnretained(self)
.sink { owner, text in
owner.phoneTextField.text = text
}
.store(in: cancelBag)

output.codeTextFieldText
.asDriver()
.withUnretained(self)
.sink { owner, text in

owner.codeTextField.text = text
}
.store(in: cancelBag)

Expand All @@ -299,10 +339,17 @@ extension PhoneVerifyView {
}
.store(in: cancelBag)

output.failDescription
output.phoneFailDescription
.withUnretained(self)
.sink { owner, description in
owner.updateFailLabelUI(description)
owner.updateFailLabelUI(isCode: false, description)
}
.store(in: cancelBag)

output.codeFailDescription
.withUnretained(self)
.sink { owner, description in
owner.updateFailLabelUI(isCode: true, description)
}
.store(in: cancelBag)

Expand All @@ -329,11 +376,17 @@ extension PhoneVerifyView {
}


private func updateFailLabelUI(_ description: String?) {
private func updateFailLabelUI(isCode: Bool, _ description: String?) {
let failLabel = isCode ? codeFailLabel : phoneFailLabel
let failIcon = isCode ? codeFailIcon : phoneFailIcon
let textfield = isCode ? codeTextField : phoneTextField
failLabel.text = description
failIcon.isHidden = description == nil
failLabel.isHidden = description == nil
timeLeftLabel.textColor = description == nil ? DSKitAsset.Colors.white.color : DSKitAsset.Colors.error.color
codeTextField.layer.borderColor = description == nil ? UIColor.clear.cgColor : DSKitAsset.Colors.error.color.cgColor
textfield.layer.borderColor = description == nil ? UIColor.clear.cgColor : DSKitAsset.Colors.error.color.cgColor

if isCode {
timeLeftLabel.textColor = description == nil ? DSKitAsset.Colors.white.color : DSKitAsset.Colors.error.color
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ public class PhoneVerifyViewModel: PhoneVerifyViewModelType {
// MARK: - Outputs

public struct Output {
let isSent = CurrentValueSubject<Bool, Never>(false)
let isSent = PassthroughSubject<Bool, Never>()
let verifySuccess = PassthroughSubject<Void, Never>()
let failDescription = PassthroughSubject<String?, Never>()
let showToast = PassthroughSubject<String, Never>()
let phoneFailDescription = PassthroughSubject<String?, Never>()
let codeFailDescription = PassthroughSubject<String?, Never>()
let timeLeft = PassthroughSubject<Int, Never>()
let phoneTextFieldText = CurrentValueSubject<String, Never>("")
let codeTextFieldText = CurrentValueSubject<String, Never>("")
let timerIsRunning = PassthroughSubject<Bool, Never>()
let sendButtonIsEnabled = CurrentValueSubject<Bool, Never>(false)
let doneButtonIsEnabled = CurrentValueSubject<Bool, Never>(false)
Expand Down Expand Up @@ -69,26 +71,34 @@ extension PhoneVerifyViewModel {
.sink { owner, err in
switch err {
case .invalidVerifyCode:
output.failDescription.send("인증번호가 올바르지 않습니다.")
output.codeFailDescription.send("인증번호가 올바르지 않습니다.")
return
case .timeout:
owner.timerCancellable = nil
output.failDescription.send("3분이 초과되었어요. 인증번호를 다시 요청해주세요.")
output.codeFailDescription.send("3분이 초과되었어요. 인증번호를 다시 요청해주세요.")
return
case .userNotFound:
output.phoneFailDescription.send("SOPT 활동 시 사용한 전화번호가 아니에요.")
case .alreadyExist:
output.phoneFailDescription.send("이미 가입된 전화번호예요.")
case .unknown(_):
output.codeFailDescription.send("알 수 없는 오류예요.")
}
}
.store(in: cancelBag)

input.sendButtonTapped
.throttle(for: 2, scheduler: RunLoop.main, latest: false)
.handleEvents(receiveOutput: { _ in
output.isSent.send(true)
output.failDescription.send(nil) }
output.phoneFailDescription.send(nil)
output.codeFailDescription.send(nil)
}
)
.withLatestFrom(input.phoneTextFieldText)
.map { PhoneSendModel(name: nil, phone: $0, type: .register) }
.map { PhoneSendModel(name: nil, phone: output.phoneTextFieldText.value, type: .register) }
.flatMap(useCase.send)
.withUnretained(self)
.sink { owner , _ in
output.isSent.send(true)
output.timeLeft.send(owner.useCase.policy.timeLimit)
owner.timerCancellable = owner.timerPublisher
.autoconnect()
Expand All @@ -101,17 +111,40 @@ extension PhoneVerifyViewModel {
}
output.timeLeft.send(counter)
}
output.showToast.send("인증번호가 전송되었어요.")
}
.store(in: cancelBag)

input.phoneTextFieldText
.map { $0.count >= self.useCase.policy.phoneNumberCount && $0.allSatisfy { $0.isNumber } }
.handleEvents(receiveOutput: { _ in
output.phoneFailDescription.send(nil)
})
.withUnretained(self)
.map { $1.count >= $0.useCase.policy.phoneMaxLength && $1.allSatisfy { $0.isNumber } }
.sink { output.sendButtonIsEnabled.send($0) }
.store(in: cancelBag)

input.phoneTextFieldText
.withUnretained(self)
.filter { $1.count > $0.useCase.policy.phoneMaxLength }
.map {
let newValue = $1.prefix($0.useCase.policy.phoneMaxLength)
return String(newValue)
}
.sink { output.phoneTextFieldText.send($0) }
.store(in: cancelBag)

input.codeTextFieldText
.withUnretained(self)
.filter { $1.count > $0.useCase.policy.codeMaxLength }
.map {
let newValue = $1.prefix($0.useCase.policy.codeMaxLength)
return String(newValue)
}
.sink { output.codeTextFieldText.send($0) }
.store(in: cancelBag)

Publishers.CombineLatest(
input.codeTextFieldText,
output.codeTextFieldText,
output.timerIsRunning
)
.map { !$0.isEmpty && $1 }
Expand All @@ -121,8 +154,13 @@ extension PhoneVerifyViewModel {
input.doneButtonTapped
.withLatestFrom(output.timerIsRunning)
.filter { $0 }
.withLatestFrom(Publishers.Zip(input.phoneTextFieldText, input.codeTextFieldText))
.map { PhoneVerifyModel(name: nil, phone: $0, code: $1, type: .register)}
.mapVoid()
.map { PhoneVerifyModel(
name: nil,
phone: output.phoneTextFieldText.value,
code: output.codeTextFieldText.value,
type: .register)
}
.flatMap(useCase.verify)
.withUnretained(self)
.sink { owner, _ in
Expand Down

0 comments on commit 19c4098

Please sign in to comment.