diff --git a/KkuMulKum.xcodeproj/project.pbxproj b/KkuMulKum.xcodeproj/project.pbxproj index 5a824095..3f685193 100644 --- a/KkuMulKum.xcodeproj/project.pbxproj +++ b/KkuMulKum.xcodeproj/project.pbxproj @@ -62,6 +62,8 @@ 78B928752C29402E006D9942 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 78B928742C29402E006D9942 /* Assets.xcassets */; }; 78B928782C29402E006D9942 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 78B928762C29402E006D9942 /* LaunchScreen.storyboard */; }; 78BD61202C43F557005752FD /* SwiftKeychainWrapper in Frameworks */ = {isa = PBXBuildFile; productRef = 78BD611F2C43F557005752FD /* SwiftKeychainWrapper */; }; + 78BD61272C446A97005752FD /* LoginTargetType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BD61262C446A97005752FD /* LoginTargetType.swift */; }; + 78BD612B2C4550A6005752FD /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BD612A2C4550A6005752FD /* Bundle.swift */; }; A3DD9C3D2C41BAD000E58A13 /* MeetingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DD9C322C41BAD000E58A13 /* MeetingTableViewCell.swift */; }; A3DD9C3E2C41BAD000E58A13 /* MeetingDummyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DD9C342C41BAD000E58A13 /* MeetingDummyModel.swift */; }; A3DD9C3F2C41BAD000E58A13 /* MeetingListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DD9C362C41BAD000E58A13 /* MeetingListView.swift */; }; @@ -206,6 +208,8 @@ 78B928742C29402E006D9942 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 78B928772C29402E006D9942 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 78B928792C29402E006D9942 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 78BD61262C446A97005752FD /* LoginTargetType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginTargetType.swift; sourceTree = ""; }; + 78BD612A2C4550A6005752FD /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; A3DD9C322C41BAD000E58A13 /* MeetingTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeetingTableViewCell.swift; sourceTree = ""; }; A3DD9C342C41BAD000E58A13 /* MeetingDummyModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeetingDummyModel.swift; sourceTree = ""; }; A3DD9C362C41BAD000E58A13 /* MeetingListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeetingListView.swift; sourceTree = ""; }; @@ -528,6 +532,22 @@ path = KkuMulKum; sourceTree = ""; }; + 78BD61252C446A84005752FD /* TargetType */ = { + isa = PBXGroup; + children = ( + 78BD61262C446A97005752FD /* LoginTargetType.swift */, + ); + path = TargetType; + sourceTree = ""; + }; + 78BD612C2C455680005752FD /* Bundle */ = { + isa = PBXGroup; + children = ( + 78BD612A2C4550A6005752FD /* Bundle.swift */, + ); + path = Bundle; + sourceTree = ""; + }; A3DD9C332C41BAD000E58A13 /* Cell */ = { isa = PBXGroup; children = ( @@ -1047,6 +1067,7 @@ DE254AA32C31107C00A4015E /* Resource */ = { isa = PBXGroup; children = ( + 78BD612C2C455680005752FD /* Bundle */, DEBA03302C3C2972002ED8F2 /* ViewController.swift */, DD39768B2C41C36B00E2A4C4 /* Color.xcassets */, 78B928742C29402E006D9942 /* Assets.xcassets */, @@ -1181,6 +1202,7 @@ DE9E18852C3BC8F000DB76B4 /* DTO */ = { isa = PBXGroup; children = ( + 78BD61252C446A84005752FD /* TargetType */, DE9E18872C3BC90300DB76B4 /* Model */, DE9E18862C3BC8F900DB76B4 /* ResponseBody */, ); @@ -1465,6 +1487,7 @@ A3FB184F2C3BF4BC001483E5 /* MakeMeetingsResponseModel.swift in Sources */, DD3976852C41C2AD00E2A4C4 /* UpcomingPromiseModel.swift in Sources */, DEF725DB2C3F3BBF008C87C7 /* Toast.swift in Sources */, + 78BD61272C446A97005752FD /* LoginTargetType.swift in Sources */, DD43937A2C412F4500EC1799 /* FinishCreateViewController.swift in Sources */, DE254AAC2C31192400A4015E /* UILabel+.swift in Sources */, DE254AB72C3119D000A4015E /* ReuseIdentifiable.swift in Sources */, @@ -1548,6 +1571,7 @@ DD43937B2C412F4500EC1799 /* CreateMeetingViewController.swift in Sources */, DE8247FD2C36E7C7000601BC /* MoyaLoggingPlugin.swift in Sources */, DDAF1C842C3D5D19008A37D3 /* ViewModelType.swift in Sources */, + 78BD612B2C4550A6005752FD /* Bundle.swift in Sources */, DD3072262C3C0F0B00416D9F /* TardyInfoModel.swift in Sources */, DE254AB92C311AB300A4015E /* Screen.swift in Sources */, ); @@ -1623,6 +1647,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; + NEW_SETTING = ""; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; @@ -1680,6 +1705,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + NEW_SETTING = ""; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; VALIDATE_PRODUCT = YES; diff --git a/KkuMulKum/Network/DTO/Model/Auth/SocialLoginRequestModel.swift b/KkuMulKum/Network/DTO/Model/Auth/SocialLoginRequestModel.swift index bb35bb9c..cd4074dd 100644 --- a/KkuMulKum/Network/DTO/Model/Auth/SocialLoginRequestModel.swift +++ b/KkuMulKum/Network/DTO/Model/Auth/SocialLoginRequestModel.swift @@ -8,5 +8,6 @@ import Foundation struct SocialLoginRequestModel: RequestModelType { - let provider: String? + let provider: String + let fcmToken: String } diff --git a/KkuMulKum/Network/DTO/Model/Auth/SocialLoginResponseModel.swift b/KkuMulKum/Network/DTO/Model/Auth/SocialLoginResponseModel.swift index c7ca4889..37bbff1d 100644 --- a/KkuMulKum/Network/DTO/Model/Auth/SocialLoginResponseModel.swift +++ b/KkuMulKum/Network/DTO/Model/Auth/SocialLoginResponseModel.swift @@ -8,5 +8,16 @@ import Foundation struct SocialLoginResponseModel: ResponseModelType { - let name, accessToken, refreshToken: String? + let name: String? + let jwtTokenDTO: JwtTokenDTO + + enum CodingKeys: String, CodingKey { + case name + case jwtTokenDTO = "jwtTokenDto" + } +} + +struct JwtTokenDTO: Codable { + let accessToken: String + let refreshToken: String } diff --git a/KkuMulKum/Network/DTO/ResponseBody/ResponseBodyDTO.swift b/KkuMulKum/Network/DTO/ResponseBody/ResponseBodyDTO.swift index 1f25daff..e07306af 100644 --- a/KkuMulKum/Network/DTO/ResponseBody/ResponseBodyDTO.swift +++ b/KkuMulKum/Network/DTO/ResponseBody/ResponseBodyDTO.swift @@ -19,3 +19,4 @@ struct ErrorResponse: Codable { let code: Int let message: String } + diff --git a/KkuMulKum/Network/DTO/TargetType/LoginTargetType.swift b/KkuMulKum/Network/DTO/TargetType/LoginTargetType.swift new file mode 100644 index 00000000..f5c09c89 --- /dev/null +++ b/KkuMulKum/Network/DTO/TargetType/LoginTargetType.swift @@ -0,0 +1,58 @@ +// +// LoginService.swift +// KkuMulKum +// +// Created by 이지훈 on 7/15/24. +// + +import Foundation + +import Moya + +enum LoginTargetType { + case appleLogin(identityToken: String, fcmToken: String) + case kakaoLogin(accessToken: String, fcmToken: String) +} + +extension LoginTargetType: TargetType { + var baseURL: URL { + guard let privacyInfo = Bundle.main.privacyInfo, + let urlString = privacyInfo["BASE_URL"] as? String, + let url = URL(string: urlString) else { + fatalError("Invalid BASE_URL in PrivacyInfo.plist") + } + return url + } + + var path: String { + return "/api/v1/auth/signin" + } + + var method: Moya.Method { + return .post + } + + var task: Task { + switch self { + case let .appleLogin(_, fcmToken): + return .requestParameters( + parameters: ["provider": "APPLE", "fcmToken": fcmToken], + encoding: JSONEncoding.default + ) + case let .kakaoLogin(_, fcmToken): + return .requestParameters( + parameters: ["provider": "KAKAO", "fcmToken": fcmToken], + encoding: JSONEncoding.default + ) + } + } + + var headers: [String : String]? { + switch self { + case .appleLogin(let identityToken, _): + return ["Authorization": identityToken, "Content-Type": "application/json"] + case .kakaoLogin(let accessToken, _): + return ["Authorization": accessToken, "Content-Type": "application/json"] + } + } +} diff --git a/KkuMulKum/Resource/Bundle/Bundle.swift b/KkuMulKum/Resource/Bundle/Bundle.swift new file mode 100644 index 00000000..33200100 --- /dev/null +++ b/KkuMulKum/Resource/Bundle/Bundle.swift @@ -0,0 +1,23 @@ +// +// Bundle.swift +// KkuMulKum +// +// Created by 이지훈 on 7/15/24. +// + +import Foundation + +extension Bundle { + var privacyInfo: [String: Any]? { + guard let url = self.url(forResource: "PrivacyInfo", withExtension: "plist"), + let data = try? Data(contentsOf: url), + let result = try? PropertyListSerialization.propertyList( + from: data, + options: [], + format: nil + ) as? [String: Any] else { + return nil + } + return result + } +} diff --git a/KkuMulKum/Source/Onboarding/Login/VIewModel/LoginViewModel.swift b/KkuMulKum/Source/Onboarding/Login/VIewModel/LoginViewModel.swift index cfb801eb..5e874191 100644 --- a/KkuMulKum/Source/Onboarding/Login/VIewModel/LoginViewModel.swift +++ b/KkuMulKum/Source/Onboarding/Login/VIewModel/LoginViewModel.swift @@ -6,22 +6,39 @@ // import UIKit - import AuthenticationServices + import KakaoSDKUser import KakaoSDKAuth - +import Moya enum LoginState { - case notLoggedIn - case loggedIn(userInfo: String) + case notLogin + case login(userInfo: String) + case needOnboarding } class LoginViewModel: NSObject { - var loginState: ObservablePattern = ObservablePattern(.notLoggedIn) + var loginState: ObservablePattern = ObservablePattern(.notLogin) var error: ObservablePattern = ObservablePattern("") - + + private let provider: MoyaProvider + + init( + provider: MoyaProvider = MoyaProvider( + plugins: [NetworkLoggerPlugin( + configuration: .init( + logOptions: .verbose + ) + )] + ) + ) { + self.provider = provider + super.init() + } + func performAppleLogin(presentationAnchor: ASPresentationAnchor) { + print("Performing Apple Login") let request = ASAuthorizationAppleIDProvider().createRequest() request.requestedScopes = [.fullName, .email] @@ -31,12 +48,14 @@ class LoginViewModel: NSObject { controller.performRequests() } - func performKakaoLogin(presentationAnchor: UIWindow) { + func performKakaoLogin() { if UserApi.isKakaoTalkLoginAvailable() { + print("Kakao Talk is available") UserApi.shared.loginWithKakaoTalk { [weak self] (oauthToken, error) in self?.handleKakaoLoginResult(oauthToken: oauthToken, error: error) } } else { + print("Kakao Talk is not available") UserApi.shared.loginWithKakaoAccount { [weak self] (oauthToken, error) in self?.handleKakaoLoginResult(oauthToken: oauthToken, error: error) } @@ -45,27 +64,71 @@ class LoginViewModel: NSObject { private func handleKakaoLoginResult(oauthToken: OAuthToken?, error: Error?) { if let error = error { + print("Kakao Login Error: \(error.localizedDescription)") self.error.value = error.localizedDescription return } - if let _ = oauthToken { - fetchKakaoUserInfo() + if let token = oauthToken?.accessToken { + print("Kakao Login Successful, access token: \(token)") + loginToServer(with: .kakaoLogin(accessToken: token, fcmToken: "dummy_fcm_token")) + } else { + print("Kakao Login Error: No access token") + self.error.value = "No access token received" } } - private func fetchKakaoUserInfo() { - UserApi.shared.me() { [weak self] (user, error) in - if let error = error { - self?.error.value = error.localizedDescription - return - } - - if let nickname = user?.kakaoAccount?.profile?.nickname { - self?.loginState.value = .loggedIn(userInfo: "Kakao user: \(nickname)") + private func loginToServer(with loginTarget: LoginTargetType) { + provider.request(loginTarget) { [weak self] result in + switch result { + case .success(let response): + print("Received response from server: \(response)") + do { + let loginResponse = try response.map(ResponseBodyDTO.self) + print("Successfully mapped response: \(loginResponse)") + self?.handleLoginResponse(loginResponse) + } catch { + print("Failed to decode response: \(error)") + self?.error.value = "Failed to decode response: \(error.localizedDescription)" + } + + case .failure(let error): + print("Network error: \(error)") + self?.error.value = "Network error: \(error.localizedDescription)" } } } + + private func handleLoginResponse(_ response: ResponseBodyDTO) { + print("Handling login response") + if response.success { + if let data = response.data { + if let name = data.name { + print("Login successful, user name: \(name)") + loginState.value = .login(userInfo: name) + } else { + print("Login successful, but no name provided. Needs onboarding.") + loginState.value = .needOnboarding + } + + let tokens = data.jwtTokenDTO + print("Received tokens - Access: \(tokens.accessToken), Refresh: \(tokens.refreshToken)") + // TODO: 토큰 저장 로직 구현 + } else { + print("Warning: No data received in response") + error.value = "No data received" + } + } else { + if let error = response.error { + print("Login failed: \(error.message)") + self.error.value = error.message + } else { + print("Login failed: Unknown error") + self.error.value = "Unknown error occurred" + } + } + } + } extension LoginViewModel: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { @@ -73,32 +136,30 @@ extension LoginViewModel: ASAuthorizationControllerDelegate, ASAuthorizationCont controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization ) { - guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential else { - print("Authorization failed: Credential is not of type ASAuthorizationAppleIDCredential") + print("Apple authorization completed") + guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential, + let identityToken = appleIDCredential.identityToken, + let tokenString = String(data: identityToken, encoding: .utf8) else { + print("Failed to get Apple ID Credential or identity token") return } - let userName = appleIDCredential.fullName?.givenName ?? "Apple user" - loginState.value = .loggedIn(userInfo: "Apple user: \(userName)") - - /// 액세스 토큰 출력 - if let identityToken = appleIDCredential.identityToken, - let tokenString = String(data: identityToken, encoding: .utf8) { - print("Apple Login Access Token: \(tokenString)") - } + print("Apple Login Successful, identity token: \(tokenString)") + loginToServer(with: .appleLogin(identityToken: tokenString, fcmToken: "dummy_fcm_token")) } - + func authorizationController( controller: ASAuthorizationController, didCompleteWithError error: Error ) { + print("Apple authorization error: \(error.localizedDescription)") self.error.value = error.localizedDescription } func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + print("Providing presentation anchor for Apple Login") let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene let window = windowScene?.windows.first - return window! } } diff --git a/KkuMulKum/Source/Onboarding/Login/ViewController/LoginViewController.swift b/KkuMulKum/Source/Onboarding/Login/ViewController/LoginViewController.swift index 87314318..a6a86acc 100644 --- a/KkuMulKum/Source/Onboarding/Login/ViewController/LoginViewController.swift +++ b/KkuMulKum/Source/Onboarding/Login/ViewController/LoginViewController.swift @@ -11,7 +11,16 @@ import AuthenticationServices class LoginViewController: BaseViewController { private let loginView = LoginView() - private let loginViewModel = LoginViewModel() + private let loginViewModel: LoginViewModel + + init(viewModel: LoginViewModel = LoginViewModel()) { + self.loginViewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } override func loadView() { view = loginView @@ -38,7 +47,6 @@ class LoginViewController: BaseViewController { ) loginView.kakaoLoginImageView.addGestureRecognizer(kakaoTapGesture) - /// 더미 버튼 loginView.dummyNextButton.addTarget( self, action: #selector(dummyNextButtonTapped), @@ -49,17 +57,21 @@ class LoginViewController: BaseViewController { private func bindViewModel() { loginViewModel.loginState.bind(with: self) { owner, state in switch state { - case .notLoggedIn: - print("Not logged in") - case .loggedIn(let userInfo): - print("Logged in: \(userInfo)") + case .notLogin: + print("Login State: Not logged in") + case .login(let userInfo): + print("Login State: Logged in with user info: \(userInfo)") + owner.navigateToMainScreen() + case .needOnboarding: + print("Login State: Need onboarding") + owner.navigateToOnboardingScreen() } } loginViewModel.error.bind(with: self) { owner, error in if !error.isEmpty { - // TODO: 추후 에러처리 추가예정 -> Keychain 연결 이후 - print("Error occurred: \(error)") + print("Login Error: \(error)") + owner.showErrorAlert(message: error) } } } @@ -69,23 +81,44 @@ class LoginViewController: BaseViewController { } @objc private func kakaoLoginTapped() { - loginViewModel.performKakaoLogin(presentationAnchor: view.window!) + loginViewModel.performKakaoLogin() } - - // TODO: 추후 서버연결후 삭제예정 + @objc private func dummyNextButtonTapped() { -// _ = NicknameViewController() -// let welcomeViewController = NicknameViewController() -// welcomeViewController.modalPresentationStyle = .fullScreen -// present(welcomeViewController, animated: true, completion: nil) - - // TODO: 프로필 설정부터 네비게이션으로 플로우 동작 - let viewController = MainTabBarController() - viewController.modalPresentationStyle = .fullScreen - present(viewController, animated: true) } + + private func navigateToMainScreen() { + DispatchQueue.main.async { + let mainTabBarController = MainTabBarController() + let navigationController = UINavigationController(rootViewController: mainTabBarController) + navigationController.isNavigationBarHidden = true + navigationController.modalPresentationStyle = .fullScreen + navigationController.modalTransitionStyle = .crossDissolve + self.present(navigationController, animated: true, completion: nil) + } + } + + private func navigateToOnboardingScreen() { + DispatchQueue.main.async { + let nicknameViewController = NicknameViewController() + if let navigationController = self.navigationController { + navigationController.pushViewController(nicknameViewController, animated: true) + } else { + let navigationController = UINavigationController(rootViewController: nicknameViewController) + navigationController.modalPresentationStyle = .fullScreen + navigationController.modalTransitionStyle = .crossDissolve + self.present(navigationController, animated: true, completion: nil) + } + } + } + + private func showErrorAlert(message: String) { + print("Showing error alert with message: \(message)") + let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + present(alert, animated: true) + } } -