diff --git a/DiningCoach.xcodeproj/project.pbxproj b/DiningCoach.xcodeproj/project.pbxproj index 638b608..eb57b6f 100644 --- a/DiningCoach.xcodeproj/project.pbxproj +++ b/DiningCoach.xcodeproj/project.pbxproj @@ -29,11 +29,16 @@ 65F8854F2A32764900D151F1 /* View+LineHeightFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F8854E2A32764900D151F1 /* View+LineHeightFont.swift */; }; 65F885532A327AF700D151F1 /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F885522A327AF700D151F1 /* SplashView.swift */; }; 65F885592A341B6000D151F1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 65F885582A341B6000D151F1 /* LaunchScreen.storyboard */; }; + 8E5A37902A5C27BB000281AC /* LoginApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E5A378F2A5C27BB000281AC /* LoginApi.swift */; }; + 8E5A37922A5C298B000281AC /* DCInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E5A37912A5C298B000281AC /* DCInterceptor.swift */; }; + 8EA4CBF12A5EDCEA00734ED3 /* TokenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EA4CBF02A5EDCEA00734ED3 /* TokenManager.swift */; }; 8ECF90E92A4EEBEB00069225 /* LoginButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ECF90E82A4EEBEB00069225 /* LoginButton.swift */; }; 8ECF90EC2A4EF6B500069225 /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = 8ECF90EB2A4EF6B500069225 /* GoogleSignIn */; }; 8ECF90EE2A4EF6B500069225 /* GoogleSignInSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 8ECF90ED2A4EF6B500069225 /* GoogleSignInSwift */; }; 8EE137782A4B09FB00A0D665 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EE137772A4B09FB00A0D665 /* LoginView.swift */; }; 8EE1377B2A4B12B100A0D665 /* DCTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EE1377A2A4B12B100A0D665 /* DCTextField.swift */; }; + 8EE3EFF62A5593E6005585C7 /* Login.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EE3EFF52A5593E6005585C7 /* Login.swift */; }; + 8EE3EFF82A55AD54005585C7 /* Urls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EE3EFF72A55AD54005585C7 /* Urls.swift */; }; 8EF5C5B22A544DBF0025E589 /* NicknameInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EF5C5AE2A544DBF0025E589 /* NicknameInputView.swift */; }; 8EF5C5B32A544DBF0025E589 /* TermsAgreementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EF5C5AF2A544DBF0025E589 /* TermsAgreementView.swift */; }; 8EF5C5B42A544DBF0025E589 /* SigninCompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EF5C5B02A544DBF0025E589 /* SigninCompleteView.swift */; }; @@ -84,10 +89,15 @@ 65F8854E2A32764900D151F1 /* View+LineHeightFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+LineHeightFont.swift"; sourceTree = ""; }; 65F885522A327AF700D151F1 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; }; 65F885582A341B6000D151F1 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + 8E5A378F2A5C27BB000281AC /* LoginApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginApi.swift; sourceTree = ""; }; + 8E5A37912A5C298B000281AC /* DCInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DCInterceptor.swift; sourceTree = ""; }; + 8EA4CBF02A5EDCEA00734ED3 /* TokenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenManager.swift; sourceTree = ""; }; 8ECF90E82A4EEBEB00069225 /* LoginButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginButton.swift; sourceTree = ""; }; 8EE137742A4B06A400A0D665 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; 8EE137772A4B09FB00A0D665 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; 8EE1377A2A4B12B100A0D665 /* DCTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DCTextField.swift; sourceTree = ""; }; + 8EE3EFF52A5593E6005585C7 /* Login.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Login.swift; sourceTree = ""; }; + 8EE3EFF72A55AD54005585C7 /* Urls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Urls.swift; sourceTree = ""; }; 8EF5C5AE2A544DBF0025E589 /* NicknameInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NicknameInputView.swift; sourceTree = ""; }; 8EF5C5AF2A544DBF0025E589 /* TermsAgreementView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TermsAgreementView.swift; sourceTree = ""; }; 8EF5C5B02A544DBF0025E589 /* SigninCompleteView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SigninCompleteView.swift; sourceTree = ""; }; @@ -215,7 +225,6 @@ A51E1C442A406B9900E4EBA3 /* Validation */, A51E1C412A406A3300E4EBA3 /* Registration */, A51E1C3B2A4061B900E4EBA3 /* API */, - A51E1C352A405DD200E4EBA3 /* Login */, A526DDC22A092B0300B682BF /* DiningCoachApp.swift */, A526DDC42A092B0300B682BF /* ContentView.swift */, 65F885502A327AE200D151F1 /* Scene */, @@ -241,6 +250,7 @@ 8EF5C5DB2A544EE40025E589 /* View+CornerRadius.swift */, 8EF5C5DC2A544EE40025E589 /* View+hideKeyboard.swift */, 65F8854E2A32764900D151F1 /* View+LineHeightFont.swift */, + 8EA4CBF02A5EDCEA00734ED3 /* TokenManager.swift */, ); path = Helper; sourceTree = ""; @@ -265,8 +275,8 @@ 8EE137732A4B04AE00A0D665 /* Login */ = { isa = PBXGroup; children = ( - 8EE137772A4B09FB00A0D665 /* LoginView.swift */, - 8ECF90E82A4EEBEB00069225 /* LoginButton.swift */, + 8EE3EFF32A5593C2005585C7 /* View */, + 8EE3EFF22A5593B2005585C7 /* Store */, ); path = Login; sourceTree = ""; @@ -279,6 +289,31 @@ path = TextField; sourceTree = ""; }; + 8EE3EFF22A5593B2005585C7 /* Store */ = { + isa = PBXGroup; + children = ( + A51E1C362A405DDF00E4EBA3 /* LoginStore.swift */, + ); + path = Store; + sourceTree = ""; + }; + 8EE3EFF32A5593C2005585C7 /* View */ = { + isa = PBXGroup; + children = ( + 8EE137772A4B09FB00A0D665 /* LoginView.swift */, + 8ECF90E82A4EEBEB00069225 /* LoginButton.swift */, + ); + path = View; + sourceTree = ""; + }; + 8EE3EFF42A5593CE005585C7 /* Model */ = { + isa = PBXGroup; + children = ( + 8EE3EFF52A5593E6005585C7 /* Login.swift */, + ); + path = Model; + sourceTree = ""; + }; 8EF5C5B82A544E4A0025E589 /* UserInfo */ = { isa = PBXGroup; children = ( @@ -294,18 +329,14 @@ path = UserInfo; sourceTree = ""; }; - A51E1C352A405DD200E4EBA3 /* Login */ = { - isa = PBXGroup; - children = ( - A51E1C362A405DDF00E4EBA3 /* LoginStore.swift */, - ); - path = Login; - sourceTree = ""; - }; A51E1C3B2A4061B900E4EBA3 /* API */ = { isa = PBXGroup; children = ( + 8EE3EFF42A5593CE005585C7 /* Model */, A51E1C3C2A4061C800E4EBA3 /* AppDelegate.swift */, + 8EE3EFF72A55AD54005585C7 /* Urls.swift */, + 8E5A378F2A5C27BB000281AC /* LoginApi.swift */, + 8E5A37912A5C298B000281AC /* DCInterceptor.swift */, ); path = API; sourceTree = ""; @@ -458,6 +489,7 @@ files = ( 8EF5C5B32A544DBF0025E589 /* TermsAgreementView.swift in Sources */, 8EE1377B2A4B12B100A0D665 /* DCTextField.swift in Sources */, + 8E5A37902A5C27BB000281AC /* LoginApi.swift in Sources */, 8EF5C5C62A544E4A0025E589 /* HeightWeightInputView.swift in Sources */, 8EF5C5DE2A544EE40025E589 /* View+hideKeyboard.swift in Sources */, 8EF5C5C82A544E4A0025E589 /* MedicalConditionInputView.swift in Sources */, @@ -465,6 +497,7 @@ 8ECF90E92A4EEBEB00069225 /* LoginButton.swift in Sources */, A51E1C3D2A4061C800E4EBA3 /* AppDelegate.swift in Sources */, 8EF5C5C22A544E4A0025E589 /* AllergyInputView.swift in Sources */, + 8EE3EFF62A5593E6005585C7 /* Login.swift in Sources */, 8EF5C5C12A544E4A0025E589 /* ExerciseSleepInputView.swift in Sources */, 65F8854C2A3275B000D151F1 /* Color+Primary.swift in Sources */, 8EF5C5C42A544E4A0025E589 /* GenderBirthdayInputView.swift in Sources */, @@ -474,18 +507,21 @@ 8EE137782A4B09FB00A0D665 /* LoginView.swift in Sources */, 655D9B962A39DBED005E133E /* DCButton+Style.swift in Sources */, A51E1C372A405DDF00E4EBA3 /* LoginStore.swift in Sources */, + 8E5A37922A5C298B000281AC /* DCInterceptor.swift in Sources */, A526DDC52A092B0300B682BF /* ContentView.swift in Sources */, 8EF5C5B42A544DBF0025E589 /* SigninCompleteView.swift in Sources */, A51E1C432A406A4800E4EBA3 /* RegistrationStore.swift in Sources */, 8EF5C5C32A544E4A0025E589 /* UserInfoCompleteView.swift in Sources */, 65F8854F2A32764900D151F1 /* View+LineHeightFont.swift in Sources */, 8EF5C5C52A544E4A0025E589 /* PreferredFoodInputView.swift in Sources */, + 8EE3EFF82A55AD54005585C7 /* Urls.swift in Sources */, 655D9B922A39DA0C005E133E /* DCButton.swift in Sources */, 655D9B8E2A39D815005E133E /* Color+Neutral.swift in Sources */, A526DDC32A092B0300B682BF /* DiningCoachApp.swift in Sources */, 65F885532A327AF700D151F1 /* SplashView.swift in Sources */, A51E1C462A406BAC00E4EBA3 /* Validation.swift in Sources */, 8EF5C5DA2A544EC10025E589 /* CheckButton.swift in Sources */, + 8EA4CBF12A5EDCEA00734ED3 /* TokenManager.swift in Sources */, 655D9B942A39DA47005E133E /* DCButtonStyle.swift in Sources */, 8EF5C5DD2A544EE40025E589 /* View+CornerRadius.swift in Sources */, ); @@ -617,10 +653,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = DiningCoach/DiningCoach.entitlements; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"DiningCoach/Preview Content\""; DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = D8Z3QRBB63; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "DiningCoach/Supporting Files/Info.plist"; @@ -634,8 +672,10 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.DiningCoach; + PRODUCT_BUNDLE_IDENTIFIER = com.diningcoach.app; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "DiningCoach Development"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -652,10 +692,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = DiningCoach/DiningCoach.entitlements; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"DiningCoach/Preview Content\""; DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = D8Z3QRBB63; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "DiningCoach/Supporting Files/Info.plist"; @@ -669,8 +711,10 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.DiningCoach; + PRODUCT_BUNDLE_IDENTIFIER = com.diningcoach.app; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "DiningCoach Development"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; diff --git a/DiningCoach/Sources/API/DCInterceptor.swift b/DiningCoach/Sources/API/DCInterceptor.swift new file mode 100644 index 0000000..e62c773 --- /dev/null +++ b/DiningCoach/Sources/API/DCInterceptor.swift @@ -0,0 +1,19 @@ +// +// DCInterceptor.swift +// DiningCoach +// +// Created by 이송미 on 2023/07/10. +// + +import Foundation +import Alamofire + +final class DCInterceptor: RequestInterceptor { + func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { + var urlRequest = urlRequest + urlRequest.headers.add(.accept("application/json")) + urlRequest.headers.add(.contentType("application/json")) + + completion(.success(urlRequest)) + } +} diff --git a/DiningCoach/Sources/API/LoginApi.swift b/DiningCoach/Sources/API/LoginApi.swift new file mode 100644 index 0000000..21a253a --- /dev/null +++ b/DiningCoach/Sources/API/LoginApi.swift @@ -0,0 +1,63 @@ +// +// LoginAPI.swift +// DiningCoach +// +// Created by 이송미 on 2023/07/10. +// + +import Foundation +import Alamofire + +final public class LoginApi { + static let shared = LoginApi() + let loginSession: Session + + init() { + let apiConfiguration = URLSessionConfiguration.default + apiConfiguration.tlsMinimumSupportedProtocolVersion = .TLSv12 + loginSession = Session(configuration: apiConfiguration, interceptor: DCInterceptor()) + } + +} + +extension LoginApi { + func loginWithSso(platformType: PlatformType, userAgent: String, accessToken: String, completion: @escaping (SsoResponse?, Error?) -> Void) { + loginSession.request(Urls.compose(path: Paths.requestSso), method: .post, parameters: ["platformType": platformType, "userAgent": userAgent, "accessToken": accessToken]) + .responseDecodable(of: SsoResponse.self) { [weak self] response in + self?.handleResponse(response: response, completion: completion) + } + } + + func loginWithEmail(email: String, password: String, completion: @escaping (SsoResponse?, Error?) -> Void) { + loginSession.request(Urls.compose(path: Paths.login), method: .post, parameters: ["email": email, "password": password]) + .responseDecodable(of: SsoResponse.self) { [weak self] response in + self?.handleResponse(response: response, completion: completion) + } + } + + func register(email: String, password: String, completion: @escaping (SsoResponse?, Error?) -> Void) { + loginSession.request(Urls.compose(path: Paths.register), method: .post, parameters: ["email": email, "password": password]) + .responseDecodable(of: SsoResponse.self) { [weak self] response in + self?.handleResponse(response: response, completion: completion) + } + } + + func refresh(userId: String, refreshToken: String, completion: @escaping (SsoResponse? , Error?) -> Void) { + loginSession.request(Urls.compose(path: Paths.refresh), method: .post, parameters: ["userId": userId, "refreshToken": refreshToken]) + .responseDecodable(of: SsoResponse.self) { [weak self] response in + self?.handleResponse(response: response, completion: completion) + } + } +} + +extension LoginApi { + private func handleResponse(response: DataResponse, completion: @escaping (SsoResponse?, Error?) -> Void) { + switch response.result { + case .success(let result): + completion(result, nil) + case .failure(let error): + completion(nil, error) + } + } +} + diff --git a/DiningCoach/Sources/API/Model/Login.swift b/DiningCoach/Sources/API/Model/Login.swift new file mode 100644 index 0000000..3a4c371 --- /dev/null +++ b/DiningCoach/Sources/API/Model/Login.swift @@ -0,0 +1,29 @@ +// +// Login.swift +// DiningCoach +// +// Created by 이송미 on 2023/07/05. +// + +import Foundation + +public enum PlatformType: String, Codable { + case kakao, google, apple +} + +struct SsoResponse: Codable { + let userId: Int64 + let newUser: Bool? + let accessToken: String + let refreshToken: String +} + +struct User: Codable { + let id: Int64 + let userName: String + let firstName: String + let email: String + let password: String + let phone: String + let userStatus: Int32 +} diff --git a/DiningCoach/Sources/API/Urls.swift b/DiningCoach/Sources/API/Urls.swift new file mode 100644 index 0000000..135296d --- /dev/null +++ b/DiningCoach/Sources/API/Urls.swift @@ -0,0 +1,27 @@ +// +// Urls.swift +// DiningCoach +// +// Created by 이송미 on 2023/07/05. +// + +import Foundation + +public struct Hosts { + public static let shared = Hosts() + + public let base = "virtserver.swaggerhub.com/DININGCOACHTEAM/DiningCoach_API_v1/1.0.0" // "diningcoach.org" +} + +public struct Paths { + public static let requestSso = "/api/v1/auth/sso" + public static let login = "/api/v1/auth/login" + public static let register = "/api/v1/auth/register" + public static let refresh = "/api/v1/auth/refresh" +} + +public struct Urls { + public static func compose(_ host: String = Hosts.shared.base, path: String) -> String { + return "https://\(host)\(path)" + } +} diff --git a/DiningCoach/Sources/Helper/TokenManager.swift b/DiningCoach/Sources/Helper/TokenManager.swift new file mode 100644 index 0000000..5795a29 --- /dev/null +++ b/DiningCoach/Sources/Helper/TokenManager.swift @@ -0,0 +1,41 @@ +// +// TokenManager.swift +// DiningCoach +// +// Created by 이송미 on 2023/07/12. +// + +import Foundation + + +final class TokenManager { + public static let shared = TokenManager() + var token: SsoResponse? = nil + + let key = "com.diningcoach.token" + + init() { + if let tokenData = UserDefaults.standard.data(forKey: key) { + let token = try? JSONDecoder().decode(SsoResponse.self, from: tokenData) + self.token = token + } + } + + func setToken(token: SsoResponse) { + if let data = try? JSONEncoder().encode(token) { + UserDefaults.standard.set(data, forKey: key) + UserDefaults.standard.synchronize() + + self.token = token + } + } + + func getToken() -> SsoResponse? { + return token + } + + func deleteToken() { + UserDefaults.standard.removeObject(forKey: key) + self.token = nil + } +} diff --git a/DiningCoach/Sources/Login/LoginStore.swift b/DiningCoach/Sources/Login/LoginStore.swift deleted file mode 100644 index 6b6bd13..0000000 --- a/DiningCoach/Sources/Login/LoginStore.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// LoginStore.swift -// DiningCoach -// -// Created by 백정수 on 2023/06/03. -// - -import SwiftUI -import KakaoSDKAuth -import KakaoSDKUser -import AuthenticationServices -import GoogleSignIn - -public enum PlatformType: String { - case kakao, google, apple -} - -// MARK: Login Store Protocol -protocol BaseLoginStore { - func login(completion: @escaping (Bool) -> Void) - func loginWithSocial(platformType: PlatformType, completion: @escaping (Bool) -> Void) -} - -class LoginStore: NSObject, ObservableObject, ASAuthorizationControllerDelegate { - @Published var isNextViewPresented = false - @Published var oauthToken: OAuthToken? - @Published var user: User? - - // MARK: kakao login - func kakaoLogin() { - if UserApi.isKakaoTalkLoginAvailable() { - UserApi.shared.loginWithKakaoTalk { (oauthToken, error) in - if let error = error { - print("카카오 로그인 실패: \(error.localizedDescription)") - return - } - - self.kakaoUserInfo() - } - } else { - UserApi.shared.loginWithKakaoAccount { (oauthToken, error) in - if let error = error { - print("카카오 로그인 실패: \(error.localizedDescription)") - return - } - - self.kakaoUserInfo() - } - } - } - - private func kakaoUserInfo() { - UserApi.shared.me() { (user, error) in - if let error = error { - print("사용자 정보 요청 실패: \(error.localizedDescription)") - return - } - - if let user = user { - print("사용자 정보: \(user)") - self.isNextViewPresented = true - } - } - } - - // MARK: apple login - func appleLogin() { - let request = ASAuthorizationAppleIDProvider().createRequest() - request.requestedScopes = [.email] - - let controller = ASAuthorizationController(authorizationRequests: [request]) - controller.delegate = self - controller.performRequests() - } - - func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { - if let _ = authorization.credential as? ASAuthorizationAppleIDCredential { - isNextViewPresented = true - } - } - - func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { - print("Authorization failed: \(error.localizedDescription)") - } -} - -// MARK: google login -extension LoginStore { - func googleLogin() { - guard let rootViewController = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.windows.first?.rootViewController else { return } - - GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController) { (signInResult, error) in - if let error = error { - print("구글 로그인 실패 : \(error)") - return - } - - guard let result = signInResult else { - print("구글 로그인 실패 : signInResult 결과가 없음") - return - } - - - } - } -} - - - diff --git a/DiningCoach/Sources/Scene/Login/Store/LoginStore.swift b/DiningCoach/Sources/Scene/Login/Store/LoginStore.swift new file mode 100644 index 0000000..581e51e --- /dev/null +++ b/DiningCoach/Sources/Scene/Login/Store/LoginStore.swift @@ -0,0 +1,164 @@ +// +// LoginStore.swift +// DiningCoach +// +// Created by 백정수 on 2023/06/03. +// + +import SwiftUI +import KakaoSDKAuth +import KakaoSDKUser +import AuthenticationServices +import GoogleSignIn + +// MARK: Login Store Protocol + +class LoginStore: NSObject, ObservableObject, ASAuthorizationControllerDelegate { + @Published var isNextViewPresented = false + @Published var oauthToken: OAuthToken? + @Published var user: User? + + let loginApi = LoginApi.shared + private var appleLoginDelegator: ((Result) -> Void)? = nil + +} + +extension LoginStore { + // MARK: - kakao login + func kakaoLogin(completion: @escaping (Result) -> Void) { + if UserApi.isKakaoTalkLoginAvailable() { + UserApi.shared.loginWithKakaoTalk { [weak self] (oauthToken, error) in + self?.handleKakaoToken(oauthToken: oauthToken, error: error, completion: completion) + } + + } else { + UserApi.shared.loginWithKakaoAccount { [weak self] (oauthToken, error) in + self?.handleKakaoToken(oauthToken: oauthToken, error: error, completion: completion) + } + } + } + + private func handleKakaoToken(oauthToken: OAuthToken? , error: Error?, completion: @escaping (Result) -> Void) { + if let error = error { + print("카카오 로그인 실패: \(error.localizedDescription)") + completion(.failure(error)) + return + } + + self.handleSsoLogin(platformType: .kakao, accessToken: oauthToken!.accessToken, completion: completion) + self.kakaoUserInfo() + } + + private func kakaoUserInfo() { + UserApi.shared.me() { (user, error) in + if let error = error { + print("사용자 정보 요청 실패: \(error.localizedDescription)") + return + } + + if let user = user { + print("사용자 정보: \(user)") + self.isNextViewPresented = true + } + } + } + + // MARK: - apple login + func appleLogin(completion: @escaping (Result) -> Void) { + self.appleLoginDelegator = completion + + let request = ASAuthorizationAppleIDProvider().createRequest() + request.requestedScopes = [.email] + + let controller = ASAuthorizationController(authorizationRequests: [request]) + controller.delegate = self + controller.performRequests() + } + + func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { + if let appleIdCredential = authorization.credential as? ASAuthorizationAppleIDCredential { + // TODO: Sign in Apple은 access token을 반환하지 않음, 서버를 통해 access token 발급 + } + } + + func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { + print("Authorization failed: \(error.localizedDescription)") + } + + // MARK: - google login + func googleLogin(completion: @escaping (Result) -> Void) { + guard let rootViewController = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.windows.first?.rootViewController else { return } + + GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController) { (signInResult, error) in + if let error = error { + completion(.failure(error)) + return + } + + guard let result = signInResult else { + completion(.success(false)) + return + } + + self.handleSsoLogin(platformType: .google, accessToken: result.user.accessToken.tokenString, completion: completion) + } + } +} + +extension LoginStore { + func isLogin() -> Bool { + let storedToken = TokenManager.shared.getToken() + + if let storedToken = storedToken { + // TODO: Request server + // check valid access token + // if not valid issue the token by refresh token + // also refresh token is not valid go to login + print("LOGIN COMPLETE !!!! ") + return true + } + + return false + } + + func login(email: String, password: String, completion: @escaping (Result) -> Void) { + loginApi.loginWithEmail(email: email, password: password) { (response, error) in + + if let error = error { + completion(.failure(error)) + return + } + + if let response = response { + TokenManager.shared.setToken(token: response) + completion(.success(true)) + + return + } + + completion(.success(false)) + } + } + + func handleSsoLogin(platformType: PlatformType, accessToken: String, completion: @escaping (Result) -> Void) { + + loginApi.loginWithSso(platformType: platformType, userAgent: UIDevice.current.model, accessToken: accessToken) { (response, error) in + if let error = error { + completion(.failure(error)) + return + } + + if let response = response { + TokenManager.shared.setToken(token: response) + completion(.success(true)) + + return + } + + completion(.success(false)) + } + } +} + + + diff --git a/DiningCoach/Sources/Scene/Login/LoginButton.swift b/DiningCoach/Sources/Scene/Login/View/LoginButton.swift similarity index 100% rename from DiningCoach/Sources/Scene/Login/LoginButton.swift rename to DiningCoach/Sources/Scene/Login/View/LoginButton.swift diff --git a/DiningCoach/Sources/Scene/Login/LoginView.swift b/DiningCoach/Sources/Scene/Login/View/LoginView.swift similarity index 66% rename from DiningCoach/Sources/Scene/Login/LoginView.swift rename to DiningCoach/Sources/Scene/Login/View/LoginView.swift index c92cf98..881130a 100644 --- a/DiningCoach/Sources/Scene/Login/LoginView.swift +++ b/DiningCoach/Sources/Scene/Login/View/LoginView.swift @@ -11,35 +11,37 @@ struct LoginView: View { @EnvironmentObject private var loginStore: LoginStore var body: some View { - VStack { - HStack { - Spacer() - Text("둘러보기") - .padding(.trailing, 16) - .padding(.top, 20) - .font(.pretendard(weight: .bold, size: 11)) - .foregroundColor(.neutral600) - .onTapGesture { - // TODO: Gesture + ScrollView { + VStack { + HStack { + Spacer() + Text("둘러보기") + .padding(.horizontal, 16) + .padding(.vertical, 20) + .font(.pretendard(weight: .bold, size: 11)) + .foregroundColor(.neutral600) + .onTapGesture { + // TODO: Gesture + } + } + + Text("Dinning Coach") + .font(.pretendard(weight: .black, size: 22)) + .padding(.top, 43.5) + .padding(.bottom, 78.5) + + DCLoginView() + + SsoLoginView() + + Link(destination: URL(string: "https://www.apple.com")!) { + Text("로그인에 문제가 있으신가요?").loginHelper { + } - } - - Text("Dinning Coach") - .font(.pretendard(weight: .black, size: 22)) - .padding(.top, 43.5) - .padding(.bottom, 78.5) - - DCLoginView() - - SsoLoginView() - - Link(destination: URL(string: "https://www.apple.com")!) { - Text("로그인에 문제가 있으신가요?").loginHelper { - + .underline() } - .underline() + .padding(.top, 37) } - .padding(.top, 37) } } } @@ -140,15 +142,28 @@ struct SsoLoginButtons: View { var body: some View { HStack(spacing: 16) { LoginButton(type: .kakao) { - loginStore.kakaoLogin() + loginStore.kakaoLogin { result in switch result { + // TODO: route or show error + case .success: + // route to main view + print("login success") + case .failure(let error): + print("error: \(error)") + } + } } LoginButton(type: .google) { - loginStore.googleLogin() + loginStore.googleLogin{ result in + // TODO: route or show error + + } } LoginButton(type: .apple) { - loginStore.appleLogin() + loginStore.appleLogin { result in + // TODO: route or show error + } } } } diff --git a/DiningCoach/Sources/Scene/Splash/SplashView.swift b/DiningCoach/Sources/Scene/Splash/SplashView.swift index f29ce31..b01b304 100644 --- a/DiningCoach/Sources/Scene/Splash/SplashView.swift +++ b/DiningCoach/Sources/Scene/Splash/SplashView.swift @@ -9,6 +9,7 @@ import SwiftUI import UserNotifications struct SplashView: View { + @EnvironmentObject private var loginStore: LoginStore @State var isLoading: Bool = true var body: some View { ZStack { @@ -17,8 +18,12 @@ struct SplashView: View { Text("Dining Coach") .font(.extraBold, size: 22, lineHeight: 28) .foregroundColor(.white) - } else { - LoginView().zIndex(1) + } else { + if loginStore.isLogin() { + + } else { + LoginView().zIndex(1) + } } } .ignoresSafeArea() @@ -29,9 +34,11 @@ struct SplashView: View { UNUserNotificationCenter.current() .requestAuthorization(options: [.alert, .sound, .badge]) { hasAllowed, error in // TODO: 알림 권한을 획득하지 않았을 때 처리 필요 + + if hasAllowed { + DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { isLoading.toggle() }) + } } - - DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { isLoading.toggle() }) } }