diff --git a/RecorDream-iOS/Projects/Core/Sources/Constants/C.swift b/RecorDream-iOS/Projects/Core/Sources/Constants/C.swift new file mode 100644 index 00000000..086261f3 --- /dev/null +++ b/RecorDream-iOS/Projects/Core/Sources/Constants/C.swift @@ -0,0 +1,14 @@ +// +// C.swift +// RD-CoreTests +// +// Created by 정은희 on 2022/12/04. +// Copyright © 2022 RecorDream. All rights reserved. +// + +public struct C { + public static let KAKAO_APP_KEY = "000e1e31f022f98fbe16c76ab287abd1" + public static let accessToken = "accessToken" + public static let refreshToken = "refreshToken" + public static let nickname = "nickname" +} diff --git a/RecorDream-iOS/Projects/Core/Sources/Constants/Key.swift b/RecorDream-iOS/Projects/Core/Sources/Constants/Key.swift new file mode 100644 index 00000000..f078b0ae --- /dev/null +++ b/RecorDream-iOS/Projects/Core/Sources/Constants/Key.swift @@ -0,0 +1,14 @@ +// +// Key.swift +// RD-Core +// +// Created by 정은희 on 2022/12/05. +// Copyright © 2022 RecorDream. All rights reserved. +// + +public enum Key: String { + case platform = "key.platform" + case userToken = "key.userToken" + case accessToken = "key.accessToken" + case nickname = "key.nickname" +} diff --git a/RecorDream-iOS/Projects/Core/Sources/Protocols/UserDefaultManager.swift b/RecorDream-iOS/Projects/Core/Sources/Protocols/UserDefaultManager.swift new file mode 100644 index 00000000..68a05b28 --- /dev/null +++ b/RecorDream-iOS/Projects/Core/Sources/Protocols/UserDefaultManager.swift @@ -0,0 +1,38 @@ +// +// UserDefaultManager.swift +// RD-Core +// +// Created by 정은희 on 2022/12/05. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import Foundation + +public protocol UserDefaultManager { + static func set(value: Any, keyPath: Key.RawValue) + static func string(key: Key) -> String? + static func int(key: Key) -> Int? + static func remove(key: Key) + static func clearUserData() +} + +public class DefaultUserDefaultManager: UserDefaultManager { + public static func set(value: Any, keyPath: Key.RawValue) { + UserDefaults.standard.setValue(value, forKeyPath: keyPath) + } + public static func string(key: Key) -> String? { + return UserDefaults.standard.string(forKey: key.rawValue) + } + public static func int(key: Key) -> Int? { + return UserDefaults.standard.integer(forKey: key.rawValue) + } + public static func remove(key: Key) { + UserDefaults.standard.removeObject(forKey: key.rawValue) + } + public static func clearUserData() { + self.remove(key: .platform) + self.remove(key: .userToken) + self.remove(key: .accessToken) + self.remove(key: .nickname) + } +} diff --git a/RecorDream-iOS/Projects/Data/Sources/Network/DataTransform/DreamSearchResuest.swift b/RecorDream-iOS/Projects/Data/Sources/Network/DataTransform/DreamSearchResuest.swift deleted file mode 100644 index 3bcc9c82..00000000 --- a/RecorDream-iOS/Projects/Data/Sources/Network/DataTransform/DreamSearchResuest.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// DreamSearchResuest.swift -// Data -// -// Created by 정은희 on 2022/11/09. -// Copyright © 2022 RecorDream. All rights reserved. -// - -import Foundation - -struct DreamSearchResuest: Encodable { - let query: String -} diff --git a/RecorDream-iOS/Projects/Data/Sources/Repositories/DefaultAuthRepository.swift b/RecorDream-iOS/Projects/Data/Sources/Repositories/DefaultAuthRepository.swift new file mode 100644 index 00000000..fa53a041 --- /dev/null +++ b/RecorDream-iOS/Projects/Data/Sources/Repositories/DefaultAuthRepository.swift @@ -0,0 +1,40 @@ +// +// DefaultAuthRepository.swift +// Data +// +// Created by 정은희 on 2022/12/04. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import Foundation + +import Domain +import RD_Network + +import RxSwift + +public final class DefaultAuthRepository { + + private let authService: AuthService + private let disposeBag = DisposeBag() + + init(authService: AuthService) { + self.authService = authService + } +} + +extension DefaultAuthRepository: AuthRepository { + public func requestAuth(request: AuthRequest) -> RxSwift.Observable { + return Observable.create { observer in + self.authService.login(kakaoToken: request.kakaoToken, appleToken: request.appleToken, fcmToken: request.fcmToken) + .subscribe(onNext: { response in + guard let response = response else { return } + observer.onNext(.init(duplicated: response.duplicated, accessToken: response.accessToken, refreshToken: response.refreshToken)) + }, onError: { err in + observer.onError(err) + }) + .disposed(by: self.disposeBag) + return Disposables.create() + } + } +} diff --git a/RecorDream-iOS/Projects/Domain/Sources/Entities/Auth/AuthEntity.swift b/RecorDream-iOS/Projects/Domain/Sources/Entities/Auth/AuthEntity.swift new file mode 100644 index 00000000..55ed99ef --- /dev/null +++ b/RecorDream-iOS/Projects/Domain/Sources/Entities/Auth/AuthEntity.swift @@ -0,0 +1,19 @@ +// +// AuthEntity.swift +// Domain +// +// Created by 정은희 on 2022/12/04. +// Copyright © 2022 RecorDream. All rights reserved. +// + +public struct AuthEntity: Codable { + public let duplicated: Bool + public let accessToken: String + public let refreshToken: String + + public init(duplicated: Bool, accessToken: String, refreshToken: String) { + self.duplicated = duplicated + self.accessToken = accessToken + self.refreshToken = refreshToken + } +} diff --git a/RecorDream-iOS/Projects/Domain/Sources/Entities/Auth/AuthRequest.swift b/RecorDream-iOS/Projects/Domain/Sources/Entities/Auth/AuthRequest.swift new file mode 100644 index 00000000..cbb6aed7 --- /dev/null +++ b/RecorDream-iOS/Projects/Domain/Sources/Entities/Auth/AuthRequest.swift @@ -0,0 +1,21 @@ +// +// AuthRequest.swift +// Domain +// +// Created by 정은희 on 2022/12/05. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import Foundation + +public struct AuthRequest: Codable { + public var kakaoToken: String? = nil + public var appleToken: String? = nil + public let fcmToken: String + + public init(kakaoToken: String?, appleToken: String?, fcmToken: String) { + self.kakaoToken = kakaoToken + self.appleToken = appleToken + self.fcmToken = fcmToken + } +} diff --git a/RecorDream-iOS/Projects/Domain/Sources/RepositoryInterfaces/AuthRepository.swift b/RecorDream-iOS/Projects/Domain/Sources/RepositoryInterfaces/AuthRepository.swift new file mode 100644 index 00000000..85cce614 --- /dev/null +++ b/RecorDream-iOS/Projects/Domain/Sources/RepositoryInterfaces/AuthRepository.swift @@ -0,0 +1,13 @@ +// +// AuthRepository.swift +// Domain +// +// Created by 정은희 on 2022/12/04. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import RxSwift + +public protocol AuthRepository { + func requestAuth(request: AuthRequest) -> Observable +} diff --git a/RecorDream-iOS/Projects/Domain/Sources/UseCases/AuthUseCase.swift b/RecorDream-iOS/Projects/Domain/Sources/UseCases/AuthUseCase.swift new file mode 100644 index 00000000..8953c2a0 --- /dev/null +++ b/RecorDream-iOS/Projects/Domain/Sources/UseCases/AuthUseCase.swift @@ -0,0 +1,49 @@ +// +// AuthUseCase.swift +// Domain +// +// Created by 정은희 on 2022/12/05. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import Foundation + +import RD_Core + +import RxSwift + +public protocol AuthUseCase { + func login(request: AuthRequest) + + var authSuccess: PublishSubject { get set } + var authFail: PublishSubject { get set } +} + +public final class DefaultAuthUseCase { + private let repository: AuthRepository + private let disposeBag = DisposeBag() + + public var authSuccess = PublishSubject() + public var authFail = PublishSubject() + + init(repository: AuthRepository) { + self.repository = repository + } +} + +extension DefaultAuthUseCase: AuthUseCase { + public func login(request: AuthRequest) { + self.repository.requestAuth(request: request) + .filter { $0 != nil } + .subscribe(onNext: { [weak self] entity in + guard let self = self else { return } + guard let entity = entity else { + return } + DefaultUserDefaultManager.set(value: entity.accessToken, keyPath: C.accessToken) + DefaultUserDefaultManager.set(value: entity.refreshToken, keyPath: C.refreshToken) + self.authSuccess.onNext(entity) + }, onError: { err in + self.authFail.onNext(err) + }).disposed(by: disposeBag) + } +} diff --git a/RecorDream-iOS/Projects/Modules/RD-DSKit/Derived/Sources/TuistAssets+RDDSKit.swift b/RecorDream-iOS/Projects/Modules/RD-DSKit/Derived/Sources/TuistAssets+RDDSKit.swift index e28ddb12..a784971a 100644 --- a/RecorDream-iOS/Projects/Modules/RD-DSKit/Derived/Sources/TuistAssets+RDDSKit.swift +++ b/RecorDream-iOS/Projects/Modules/RD-DSKit/Derived/Sources/TuistAssets+RDDSKit.swift @@ -32,8 +32,10 @@ public enum RDDSKitAsset { } public enum Images { public static let icnMicTitle = RDDSKitImages(name: "icn_mic_title") + public static let kakaotalk = RDDSKitImages(name: "Kakaotalk") public static let icnEdit = RDDSKitImages(name: "icn_edit") public static let icnMypage = RDDSKitImages(name: "icn_mypage") + public static let apple = RDDSKitImages(name: "apple") public static let backgroundBlue = RDDSKitImages(name: "background_blue") public static let backgroundPink = RDDSKitImages(name: "background_pink") public static let backgroundPurple = RDDSKitImages(name: "background_purple") diff --git a/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/Kakaotalk.imageset/Contents.json b/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/Kakaotalk.imageset/Contents.json new file mode 100644 index 00000000..b28a3f7e --- /dev/null +++ b/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/Kakaotalk.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "Kakaotalk.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Kakaotalk@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Kakaotalk@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/Kakaotalk.imageset/Kakaotalk.png b/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/Kakaotalk.imageset/Kakaotalk.png new file mode 100644 index 00000000..ace062db Binary files /dev/null and b/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/Kakaotalk.imageset/Kakaotalk.png differ diff --git a/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/Kakaotalk.imageset/Kakaotalk@2x.png b/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/Kakaotalk.imageset/Kakaotalk@2x.png new file mode 100644 index 00000000..90711f0c Binary files /dev/null and b/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/Kakaotalk.imageset/Kakaotalk@2x.png differ diff --git a/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/Kakaotalk.imageset/Kakaotalk@3x.png b/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/Kakaotalk.imageset/Kakaotalk@3x.png new file mode 100644 index 00000000..30a2db74 Binary files /dev/null and b/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/Kakaotalk.imageset/Kakaotalk@3x.png differ diff --git a/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/apple.imageset/Contents.json b/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/apple.imageset/Contents.json new file mode 100644 index 00000000..12b81ab8 --- /dev/null +++ b/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/apple.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "apple.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "apple@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "apple@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/apple.imageset/apple.png b/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/apple.imageset/apple.png new file mode 100644 index 00000000..64953dd6 Binary files /dev/null and b/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/apple.imageset/apple.png differ diff --git a/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/apple.imageset/apple@2x.png b/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/apple.imageset/apple@2x.png new file mode 100644 index 00000000..ad290def Binary files /dev/null and b/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/apple.imageset/apple@2x.png differ diff --git a/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/apple.imageset/apple@3x.png b/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/apple.imageset/apple@3x.png new file mode 100644 index 00000000..395b2528 Binary files /dev/null and b/RecorDream-iOS/Projects/Modules/RD-DSKit/Resources/Images.xcassets/apple.imageset/apple@3x.png differ diff --git a/RecorDream-iOS/Projects/Modules/RD-DSKit/Sources/Components/RDButton/RDLoginButton.swift b/RecorDream-iOS/Projects/Modules/RD-DSKit/Sources/Components/RDButton/RDLoginButton.swift new file mode 100644 index 00000000..7db12573 --- /dev/null +++ b/RecorDream-iOS/Projects/Modules/RD-DSKit/Sources/Components/RDButton/RDLoginButton.swift @@ -0,0 +1,66 @@ +// +// RDLoginButton.swift +// RD-DSKit +// +// Created by 정은희 on 2022/12/01. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import UIKit + +import SnapKit + +public class RDLoginButton: UIButton { + public enum PlatformType: String { + case kakao = "kakao" + case apple = "apple" + } + + public let iconImageView: UIImageView = { + let iv = UIImageView() + iv.image = UIImage() + iv.contentMode = .scaleAspectFit + return iv + }() + + // MARK: - Initialization + public convenience init(platform: PlatformType, title: String) { + self.init(frame: .zero) + + self.setupView(at: platform, for: title) + self.setupConstraint() + } + override init(frame: CGRect) { + super.init(frame: frame) + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Extensions +extension RDLoginButton { + private func setupView(at platform: PlatformType, for title: String) { + switch platform { + case .kakao: + self.iconImageView.image = RDDSKitAsset.Images.kakaotalk.image + case .apple: + self.iconImageView.image = RDDSKitAsset.Images.apple.image + } + + self.backgroundColor = .white.withAlphaComponent(0.05) + self.makeRoundedWithBorder(radius: 12, borderColor: UIColor(white: 1.0, alpha: 0.1).cgColor) + self.setTitle(title, for: .normal) + self.titleLabel?.font = RDDSKitFontFamily.Pretendard.regular.font(size: 14) + self.titleLabel?.textAlignment = .center + self.titleLabel?.textColor = .white + + self.addSubview(iconImageView) + } + private func setupConstraint() { + self.iconImageView.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(24) + make.centerY.equalToSuperview() + } + } +} diff --git a/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Responses/AuthResponse.swift b/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Responses/AuthResponse.swift new file mode 100644 index 00000000..b43cafcb --- /dev/null +++ b/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Responses/AuthResponse.swift @@ -0,0 +1,20 @@ +// +// AuthResponse.swift +// RD-Network +// +// Created by 정은희 on 2022/12/04. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import Foundation + +public struct AuthResponse: Codable { + public let duplicated: Bool + public let accessToken: String + public let refreshToken: String + + enum CodingKeys: String, CodingKey { + case duplicated = "isAlreadyUser" + case accessToken, refreshToken + } +} diff --git a/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Routers/AuthRouter.swift b/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Routers/AuthRouter.swift index b7c88078..1002e701 100644 --- a/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Routers/AuthRouter.swift +++ b/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Routers/AuthRouter.swift @@ -9,31 +9,33 @@ import Alamofire enum AuthRouter { - + case login(kakaoToken: String?, appleToken: String?, fcmToken: String) } extension AuthRouter: BaseRouter { var method: HTTPMethod { switch self { - default: return .get + case .login: + return .post } } var path: String { switch self { - default: return "" + case .login: + return "/auth/login" } } var parameters: RequestParams { switch self { - default: return .requestPlain - } - } - - var header: HeaderType { - switch self { - + case .login(let kakaoToken, let appleToken, let fcmToken): + let body: [String: Any] = [ + "kakaoToken": kakaoToken, + "appleToken": appleToken, + "fcmToken": fcmToken + ] + return .requestBody(body) } } diff --git a/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Services/AuthService.swift b/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Services/AuthService.swift index cab7edd7..4880b0fa 100644 --- a/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Services/AuthService.swift +++ b/RecorDream-iOS/Projects/Modules/RD-Network/Sources/Services/AuthService.swift @@ -7,9 +7,10 @@ // import Alamofire +import RxSwift public protocol AuthService { - + func login(kakaoToken: String?, appleToken: String?, fcmToken: String) -> Observable } public class DefaultAuthService: BaseService { @@ -19,5 +20,7 @@ public class DefaultAuthService: BaseService { } extension DefaultAuthService: AuthService { - + public func login(kakaoToken: String?, appleToken: String?, fcmToken: String) -> RxSwift.Observable { + requestObjectInRx(AuthRouter.login(kakaoToken: kakaoToken, appleToken: appleToken, fcmToken: fcmToken)) + } } diff --git a/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/Auth/View/LoginVC+Network.swift b/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/Auth/View/LoginVC+Network.swift new file mode 100644 index 00000000..cbd5ec3c --- /dev/null +++ b/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/Auth/View/LoginVC+Network.swift @@ -0,0 +1,89 @@ +// +// LoginVC+Network.swift +// Presentation +// +// Created by 정은희 on 2022/12/06. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import UIKit +import AuthenticationServices + +import Domain +import RD_Core + +import KakaoSDKAuth +import KakaoSDKUser + +extension LoginVC { + func kakaoLoginAuthentication() { + if UserApi.isKakaoTalkLoginAvailable() { + UserApi.shared.loginWithKakaoTalk { [weak self] oauthToken, error in + guard let self = self else { return } + self.kakaoLoginRequest(oauthToken, error) + } + } + else { + UserApi.shared.loginWithKakaoAccount { [weak self] oauthToken, error in + guard let self = self else { return } + self.kakaoLoginRequest(oauthToken, error) + } + } + } + + func appleLoginAuthentication() { + let appleIDProvider = ASAuthorizationAppleIDProvider() + let request = appleIDProvider.createRequest() + request.requestedScopes = [.fullName, .email] + + let authorizationController = ASAuthorizationController(authorizationRequests: [request]) + authorizationController.delegate = self + authorizationController.presentationContextProvider = self + authorizationController.performRequests() + } +} + +extension LoginVC: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { + fileprivate func kakaoLoginRequest(_ oauthToken: OAuthToken? = nil, _ error: Error?) { + guard error == nil else { + self.loginRequestFail.onNext(.kakao) + return + } + + guard let kakaoToken = oauthToken?.accessToken else { + self.loginRequestFail.onNext(.kakao) + return + } + + let authRequestEntity = AuthRequest( + kakaoToken: kakaoToken, + appleToken: nil, + fcmToken: UserDefaults.standard.string(forKey: Key.userToken.rawValue)!) + self.loginRequestSuccess.onNext(authRequestEntity) + } + public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + return self.view.window! + } + + public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { + switch authorization.credential { + case let appleIDCredential as ASAuthorizationAppleIDCredential: + guard let identityToken = appleIDCredential.identityToken, + let tokenString = String(data: identityToken, encoding: .utf8) else { + self.loginRequestFail.onNext(.apple) + break + } + let authRequestEntity = AuthRequest( + kakaoToken: nil, + appleToken: tokenString, + fcmToken: UserDefaults.standard.string(forKey: Key.userToken.rawValue)!) + self.loginRequestSuccess.onNext(authRequestEntity) + default: + break + } + } + + public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { + self.loginRequestFail.onNext(.apple) + } +} diff --git a/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/Auth/View/LoginVC.swift b/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/Auth/View/LoginVC.swift new file mode 100644 index 00000000..548e9c01 --- /dev/null +++ b/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/Auth/View/LoginVC.swift @@ -0,0 +1,119 @@ +// +// LoginVC.swift +// Presentation +// +// Created by 정은희 on 2022/12/01. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import UIKit + +import Domain +import RD_DSKit + +import RxSwift +import RxCocoa +import SnapKit + +public final class LoginVC: UIViewController { + + // MARK: - UI Components + private let authView = AuthView() + private let kakaoLoginButton = RDLoginButton(platform: .kakao, title: "카카오로 시작하기") + private let appleLoginButton = RDLoginButton(platform: .apple, title: "Apple로 시작하기") + private let descriptionLabel: UILabel = { + let lb = UILabel() + lb.font = RDDSKitFontFamily.Pretendard.regular.font(size: 12) + lb.text = "로그인 후 이용이 가능합니다." + lb.textAlignment = .center + lb.textColor = .white.withAlphaComponent(0.4) + lb.sizeToFit() + return lb + }() + + // MARK: - Properties + var loginViewModel: LoginViewModel! + var loginRequestFail = PublishSubject() + var loginRequestSuccess = PublishSubject() + private let disposeBag = DisposeBag() + + // MARK: - View Life Cycle + public override func viewDidLoad() { + super.viewDidLoad() + + self.setupView() + self.setupConstraint() + self.bindViewModels() + } + +} + +// MARK: - Extensions +extension LoginVC: AuthControllable { + func setupView() { + self.view.addSubview(authView) + self.authView.addSubviews(kakaoLoginButton, appleLoginButton, descriptionLabel) + } + + func setupConstraint() { + self.authView.snp.makeConstraints { make in + make.width.equalToSuperview() + make.height.equalToSuperview() + make.centerX.centerY.equalToSuperview() + } + self.kakaoLoginButton.snp.makeConstraints { make in + make.width.equalTo(343.adjustedWidth) + make.height.equalTo(52.adjustedHeight) + make.centerX.equalToSuperview() + make.top.equalToSuperview().offset(614) + } + self.appleLoginButton.snp.makeConstraints { make in + make.width.height.equalTo(kakaoLoginButton) + make.centerX.equalToSuperview() + make.top.equalTo(kakaoLoginButton.snp.bottom).offset(8) + } + self.descriptionLabel.snp.makeConstraints { make in + make.top.equalTo(appleLoginButton.snp.bottom).offset(10) + make.centerX.equalToSuperview() + } + } +} + +// MARK: - Reactive Part +extension LoginVC { + private func bindViewModels() { + let input = LoginViewModel.Input(loginButtonTapped: Observable.merge( + self.kakaoLoginButton.rx.tap.map { _ in + AuthPlatformType.kakao + }, + self.appleLoginButton.rx.tap.map { _ in + AuthPlatformType.apple + }) + .asObservable(), + loginRequestFail: loginRequestFail, loginRequestSuccess: loginRequestSuccess) + + let output = self.loginViewModel.transform(from: input, disposeBag: self.disposeBag) + + output.loginRequest.subscribe(onNext: { [weak self] platformType in + guard let self = self else { return } + switch platformType { + case .kakao: + self.kakaoLoginAuthentication() + case .apple: + self.appleLoginAuthentication() + } + }).disposed(by: disposeBag) + + output.loginSuccess.subscribe(onNext: { entity in + // TODO: - 홈뷰로 화면전환, 닉네임 데이터 넘기기 + }).disposed(by: self.disposeBag) + + output.showLoginFailError.subscribe(onNext: { _ in + print("로그인 오류") + }).disposed(by: self.disposeBag) + + output.showNetworkError.subscribe(onNext: { _ in + print("네트워크 오류") + }).disposed(by: self.disposeBag) + } +} diff --git a/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/Auth/ViewModel/LoginViewModel.swift b/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/Auth/ViewModel/LoginViewModel.swift new file mode 100644 index 00000000..1bc095c3 --- /dev/null +++ b/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/Auth/ViewModel/LoginViewModel.swift @@ -0,0 +1,82 @@ +// +// LoginViewModel.swift +// Presentation +// +// Created by 정은희 on 2022/12/05. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import Foundation + +import Domain +import RD_Core + +import RxSwift +import RxCocoa + +public final class LoginViewModel { + private let useCase: AuthUseCase + private let disposeBag = DisposeBag() + + // MARK: - Inputs + public struct Input { + let loginButtonTapped: Observable + let loginRequestFail: Observable + let loginRequestSuccess: Observable + } + + // MARK: - Outputs + public struct Output { + var loginRequest = PublishRelay() + var loginSuccess = PublishRelay() + var showLoginFailError = PublishRelay() + var showNetworkError = PublishRelay() + } + + // MARK: - Properties + let authRequestEntity = BehaviorRelay(value: .init(kakaoToken: "", appleToken: "", fcmToken: "")) + + // MARK: - Initialization + init(useCase: AuthUseCase) { + self.useCase = useCase + } +} + +// MARK: - Transform +extension LoginViewModel: ViewModelType { + public func transform(from input: Input, disposeBag: DisposeBag) -> Output { + let output = Output() + self.bindOutput(output: output, disposeBag: disposeBag) + + input.loginButtonTapped + .subscribe(onNext: { platformType in + output.loginRequest.accept(platformType) + }).disposed(by: disposeBag) + + input.loginRequestSuccess + .subscribe(onNext: { [weak self] request in + guard let self = self else { return } + self.useCase.login(request: self.authRequestEntity.value) + }).disposed(by: disposeBag) + + input.loginRequestFail + .subscribe(onNext: { platformType in + output.showLoginFailError.accept(platformType) + }).disposed(by: disposeBag) + + return output + } + + private func bindOutput(output: Output, disposeBag: DisposeBag) { + let loginRelay = useCase.authSuccess + let loginError = useCase.authFail + + loginRelay.subscribe(onNext: { entity in + output.loginSuccess.accept(entity) + }).disposed(by: disposeBag) + + loginError.subscribe(onNext: { _ in + output.showNetworkError.accept(()) + }).disposed(by: disposeBag) + } +} diff --git a/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/AuthControllable.swift b/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/AuthControllable.swift new file mode 100644 index 00000000..2978dc9a --- /dev/null +++ b/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/AuthControllable.swift @@ -0,0 +1,23 @@ +// +// AuthControllable.swift +// Presentation +// +// Created by 정은희 on 2022/12/01. +// Copyright © 2022 RecorDream. All rights reserved. +// + +protocol AuthControllable { + func setupView() + func setupConstraint() +} + +enum AuthPlatformType: String { + case kakao = "kakao" + case apple = "apple" +} + +enum TokenState { + case valid + case invalid + case missed +} diff --git a/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/Splash/View/AuthView.swift b/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/Splash/View/AuthView.swift new file mode 100644 index 00000000..618cd3de --- /dev/null +++ b/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/Splash/View/AuthView.swift @@ -0,0 +1,57 @@ +// +// AuthView.swift +// Presentation +// +// Created by 정은희 on 2022/12/01. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import UIKit + +import RD_DSKit + +import SnapKit + +final class AuthView: UIView { + + // MARK: - UI Components + private let backgroundImageView: UIImageView = { + let iv = UIImageView() + iv.image = RDDSKitAsset.Images.splashBackground.image + iv.contentMode = .scaleAspectFit + return iv + }() + private let logoImageView: UIImageView = { + let iv = UIImageView() + iv.image = RDDSKitAsset.Images.rdSplashLogo.image + iv.contentMode = .scaleAspectFit + return iv + }() + + // MARK: - Initialization + override init(frame: CGRect) { + super.init(frame: frame) + + self.setupView() + self.setupConstraint() + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension AuthView: AuthControllable { + func setupView() { + self.addSubview(backgroundImageView) + self.backgroundImageView.addSubview(logoImageView) + } + func setupConstraint() { + self.backgroundImageView.snp.makeConstraints { make in + make.width.height.equalToSuperview() + make.centerX.centerY.equalToSuperview() + } + self.logoImageView.snp.makeConstraints { make in + make.centerX.centerY.equalToSuperview() + } + } +} diff --git a/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/Splash/View/SplashVC.swift b/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/Splash/View/SplashVC.swift new file mode 100644 index 00000000..7482c037 --- /dev/null +++ b/RecorDream-iOS/Projects/Presentation/Sources/AuthScene/Splash/View/SplashVC.swift @@ -0,0 +1,93 @@ +// +// SplashVC.swift +// Presentation +// +// Created by 정은희 on 2022/12/01. +// Copyright © 2022 RecorDream. All rights reserved. +// + +import UIKit + +import Domain +import RD_Core +import RD_Network + +import RxSwift + +public final class SplashVC: UIViewController { + + private let authView = AuthView() + private let disposeBag = DisposeBag() + + // MARK: - View Life Cycle + public override func viewDidLoad() { + super.viewDidLoad() + + self.setupView() + self.setupConstraint() + } +} + +// MARK: - Extensions +extension SplashVC: AuthControllable { + func setupView() { + self.view.addSubview(authView) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + UIView.animate(withDuration: 1.0, delay: 0) { + self.setupViewState() + } + } + } + + func setupConstraint() { + self.authView.snp.makeConstraints { make in + make.width.equalToSuperview() + make.height.equalToSuperview() + make.centerX.centerY.equalToSuperview() + } + } +} + +extension SplashVC { + private func setupViewState() { + self.checkLoginEnable { tokenState in +// switch tokenState { +// case .valid: +// // TODO: - 홈 뷰로 전환 +// case .invalid: +// // TODO: - 로그인 뷰로 전환 +// case .missed: +// self.postLogin { state in +// // TODO: - state에 따라 화면전환 +// } +// } + } + } + + private func checkLoginEnable(completion: @escaping ((TokenState) -> Void)) { + if UserDefaults.standard.string(forKey: Key.userToken.rawValue) != nil && + UserDefaults.standard.string(forKey: Key.accessToken.rawValue) != nil { + // TODO: - 홈뷰 서버통신 + /// 성공이라면 토큰 상태로 .valid + /// 실패라면 토큰 상태로 .invalid 넣어주기 + } + else { + completion(.missed) + } + } + + private func postLogin(completion: @escaping ((Bool) -> Void)) { + let userToken = UserDefaults.standard.string(forKey: Key.userToken.rawValue)! + let accessToken = UserDefaults.standard.string(forKey: Key.accessToken.rawValue)! + + DefaultAuthService.shared.login(kakaoToken: userToken, appleToken: userToken, fcmToken: accessToken) // ✅ + .subscribe(onNext: { response in + guard let response = response else { return } + DefaultUserDefaultManager.set(value: response.accessToken, keyPath: Key.accessToken.rawValue) + completion(true) + }, onError: { _ in + completion(false) + }).disposed(by: self.disposeBag) + } +} diff --git a/RecorDream-iOS/Projects/RecorDream-iOS/Sources/Applications/AppDelegate.swift b/RecorDream-iOS/Projects/RecorDream-iOS/Sources/Applications/AppDelegate.swift index c2d33a00..4ce9cfea 100644 --- a/RecorDream-iOS/Projects/RecorDream-iOS/Sources/Applications/AppDelegate.swift +++ b/RecorDream-iOS/Projects/RecorDream-iOS/Sources/Applications/AppDelegate.swift @@ -7,8 +7,11 @@ import UIKit +import RD_Core + import FirebaseCore import FirebaseMessaging +import KakaoSDKCommon import UserNotifications @main @@ -16,6 +19,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + KakaoSDK.initSDK(appKey: C.KAKAO_APP_KEY) self.configureFirebase() self.registerAPNs(to: application) return true diff --git a/RecorDream-iOS/Projects/RecorDream-iOS/Sources/Applications/SceneDelegate.swift b/RecorDream-iOS/Projects/RecorDream-iOS/Sources/Applications/SceneDelegate.swift index 660dac89..3b6442f2 100644 --- a/RecorDream-iOS/Projects/RecorDream-iOS/Sources/Applications/SceneDelegate.swift +++ b/RecorDream-iOS/Projects/RecorDream-iOS/Sources/Applications/SceneDelegate.swift @@ -10,6 +10,8 @@ import UIKit import RD_Navigator import Presentation +import KakaoSDKAuth + class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? @@ -55,3 +57,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func sceneDidEnterBackground(_ scene: UIScene) {} } + +extension SceneDelegate { + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + if let url = URLContexts.first?.url { + if AuthApi.isKakaoTalkLoginUrl(url) { + _ = AuthController.handleOpenUrl(url: url) + } + } + } +}