diff --git a/KkuMulKum.xcodeproj/project.pbxproj b/KkuMulKum.xcodeproj/project.pbxproj index 5b81fa12..3fd4d826 100644 --- a/KkuMulKum.xcodeproj/project.pbxproj +++ b/KkuMulKum.xcodeproj/project.pbxproj @@ -62,12 +62,16 @@ 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 */; }; - 78BD61232C440AD5005752FD /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BD61222C440AD5005752FD /* AuthService.swift */; }; + 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 */; }; A3DD9C402C41BAD000E58A13 /* MeetingListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DD9C382C41BAD000E58A13 /* MeetingListViewController.swift */; }; A3DD9C412C41BAD000E58A13 /* MeetingListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DD9C3A2C41BAD000E58A13 /* MeetingListViewModel.swift */; }; + A3DD9C5A2C43F99800E58A13 /* SetReadyInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DD9C592C43F99700E58A13 /* SetReadyInfoView.swift */; }; + A3DD9C5C2C43F9A800E58A13 /* SetReadyInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DD9C5B2C43F9A800E58A13 /* SetReadyInfoViewController.swift */; }; + A3DD9C5F2C441F8E00E58A13 /* SetReadyInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DD9C5E2C441F8E00E58A13 /* SetReadyInfoViewModel.swift */; }; A3FB184D2C3BF45F001483E5 /* MakeMeetingsRequestModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3FB184C2C3BF45F001483E5 /* MakeMeetingsRequestModel.swift */; }; A3FB184F2C3BF4BC001483E5 /* MakeMeetingsResponseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3FB184E2C3BF4BB001483E5 /* MakeMeetingsResponseModel.swift */; }; A3FB18512C3BF531001483E5 /* RegisterMeetingsResponseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3FB18502C3BF531001483E5 /* RegisterMeetingsResponseModel.swift */; }; @@ -204,12 +208,16 @@ 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 = ""; }; - 78BD61222C440AD5005752FD /* AuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthService.swift; 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 = ""; }; A3DD9C382C41BAD000E58A13 /* MeetingListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeetingListViewController.swift; sourceTree = ""; }; A3DD9C3A2C41BAD000E58A13 /* MeetingListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeetingListViewModel.swift; sourceTree = ""; }; + A3DD9C592C43F99700E58A13 /* SetReadyInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetReadyInfoView.swift; sourceTree = ""; }; + A3DD9C5B2C43F9A800E58A13 /* SetReadyInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetReadyInfoViewController.swift; sourceTree = ""; }; + A3DD9C5E2C441F8E00E58A13 /* SetReadyInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetReadyInfoViewModel.swift; sourceTree = ""; }; A3FB184C2C3BF45F001483E5 /* MakeMeetingsRequestModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MakeMeetingsRequestModel.swift; sourceTree = ""; }; A3FB184E2C3BF4BB001483E5 /* MakeMeetingsResponseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MakeMeetingsResponseModel.swift; sourceTree = ""; }; A3FB18502C3BF531001483E5 /* RegisterMeetingsResponseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterMeetingsResponseModel.swift; sourceTree = ""; }; @@ -524,12 +532,20 @@ path = KkuMulKum; sourceTree = ""; }; - 78BD61212C440AC8005752FD /* Service */ = { + 78BD61252C446A84005752FD /* TargetType */ = { isa = PBXGroup; children = ( - 78BD61222C440AD5005752FD /* AuthService.swift */, + 78BD61262C446A97005752FD /* LoginTargetType.swift */, ); - path = Service; + path = TargetType; + sourceTree = ""; + }; + 78BD612C2C455680005752FD /* Bundle */ = { + isa = PBXGroup; + children = ( + 78BD612A2C4550A6005752FD /* Bundle.swift */, + ); + path = Bundle; sourceTree = ""; }; A3DD9C332C41BAD000E58A13 /* Cell */ = { @@ -584,6 +600,14 @@ path = MeetingList; sourceTree = ""; }; + A3DD9C5D2C441F6600E58A13 /* ViewModel */ = { + isa = PBXGroup; + children = ( + A3DD9C5E2C441F8E00E58A13 /* SetReadyInfoViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; DD3976692C41769900E2A4C4 /* ViewModel */ = { isa = PBXGroup; children = ( @@ -750,6 +774,7 @@ DD3976952C41CC2200E2A4C4 /* ViewController */ = { isa = PBXGroup; children = ( + A3DD9C5B2C43F9A800E58A13 /* SetReadyInfoViewController.swift */, DDAF1C882C3D6E3D008A37D3 /* ReadyStatusViewController.swift */, ); path = ViewController; @@ -758,6 +783,7 @@ DD3976962C41CC2C00E2A4C4 /* View */ = { isa = PBXGroup; children = ( + A3DD9C592C43F99700E58A13 /* SetReadyInfoView.swift */, DD931B6A2C3D9EBB00526452 /* ReadyStatusView.swift */, DD931B712C3DA92700526452 /* EnterReadyInfoButtonView.swift */, DD931B732C3DAB9A00526452 /* ReadyPlanInfoView.swift */, @@ -941,6 +967,7 @@ DD931B672C3D9D9C00526452 /* ReadyStatus */ = { isa = PBXGroup; children = ( + A3DD9C5D2C441F6600E58A13 /* ViewModel */, DD3976952C41CC2200E2A4C4 /* ViewController */, DD3976962C41CC2C00E2A4C4 /* View */, ); @@ -1041,6 +1068,7 @@ DE254AA32C31107C00A4015E /* Resource */ = { isa = PBXGroup; children = ( + 78BD612C2C455680005752FD /* Bundle */, DEBA03302C3C2972002ED8F2 /* ViewController.swift */, DD39768B2C41C36B00E2A4C4 /* Color.xcassets */, 78B928742C29402E006D9942 /* Assets.xcassets */, @@ -1175,6 +1203,7 @@ DE9E18852C3BC8F000DB76B4 /* DTO */ = { isa = PBXGroup; children = ( + 78BD61252C446A84005752FD /* TargetType */, DE9E18872C3BC90300DB76B4 /* Model */, DE9E18862C3BC8F900DB76B4 /* ResponseBody */, ); @@ -1460,6 +1489,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 */, @@ -1495,6 +1525,7 @@ DD3072162C3BFE4E00416D9F /* UpcomingPromiseListResponseModel.swift in Sources */, A3FB18572C3BF704001483E5 /* MeetingListResponseModel.swift in Sources */, DE254AB22C31197B00A4015E /* UIButton+.swift in Sources */, + A3DD9C5F2C441F8E00E58A13 /* SetReadyInfoViewModel.swift in Sources */, DE6D4D162C3F14D80005584B /* MeetingInfoViewController.swift in Sources */, DE159D322C406E1600425101 /* MyPageAlarmSettingView.swift in Sources */, 78B9286C2C29402C006D9942 /* AppDelegate.swift in Sources */, @@ -1507,6 +1538,7 @@ DD39766F2C41B54400E2A4C4 /* InviteCodeService.swift in Sources */, DE6D4D172C3F14D80005584B /* MeetingInfoViewModel.swift in Sources */, 78AED1372C3D98D1000AD80A /* NicknameView.swift in Sources */, + A3DD9C5C2C43F9A800E58A13 /* SetReadyInfoViewController.swift in Sources */, A3FB185B2C3BF7DF001483E5 /* MeetingMembersResponseModel.swift in Sources */, DD3072222C3C0DA300416D9F /* PromiseParticipantListResponseModel.swift in Sources */, DD3976862C41C2AD00E2A4C4 /* HomeView.swift in Sources */, @@ -1530,6 +1562,7 @@ DD931B762C3DC16100526452 /* PromiseInfoView.swift in Sources */, DD3072242C3C0EB200416D9F /* MyPromiseReadyInfoRequestModel.swift in Sources */, DD3976872C41C2AD00E2A4C4 /* TodayPromiseView.swift in Sources */, + A3DD9C5A2C43F99800E58A13 /* SetReadyInfoView.swift in Sources */, 789873332C3D1A7B00435E96 /* LoginViewModel.swift in Sources */, 782B40752C3DBFBA008B0CA7 /* ProfileSetupView.swift in Sources */, DED5DBEE2C34529A006ECE7E /* BaseView.swift in Sources */, @@ -1542,6 +1575,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 */, ); @@ -1617,6 +1651,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)"; @@ -1674,6 +1709,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 b3347ed2..e07306af 100644 --- a/KkuMulKum/Network/DTO/ResponseBody/ResponseBodyDTO.swift +++ b/KkuMulKum/Network/DTO/ResponseBody/ResponseBodyDTO.swift @@ -9,6 +9,7 @@ import Foundation /// 제네릭 ResponseBody 구조체 정의 struct ResponseBodyDTO: Codable { + let success: Bool let data: T? let error: ErrorResponse? } @@ -18,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/Resource/Component/CustomTextField.swift b/KkuMulKum/Resource/Component/CustomTextField.swift index bdcfc24d..1a21b976 100644 --- a/KkuMulKum/Resource/Component/CustomTextField.swift +++ b/KkuMulKum/Resource/Component/CustomTextField.swift @@ -37,9 +37,9 @@ private extension CustomTextField { textColor: .black, backgroundColor: .white, placeholderColor: .gray3, - style: .body04 + style: .body02 ) - addPadding(left: 12) + addPadding(left: 18) setLayer(borderWidth: 1, borderColor: .gray3, cornerRadius: 8) setAutoType() } diff --git a/KkuMulKum/Resource/Util/Toast.swift b/KkuMulKum/Resource/Util/Toast.swift index 68a7a14e..7eed5875 100644 --- a/KkuMulKum/Resource/Util/Toast.swift +++ b/KkuMulKum/Resource/Util/Toast.swift @@ -39,7 +39,7 @@ private extension Toast { $0.textColor = .white $0.textAlignment = .center $0.text = message - $0.font = .pretendard(.body02) + $0.font = .pretendard(.body06) $0.clipsToBounds = true $0.numberOfLines = 0 $0.sizeToFit() diff --git a/KkuMulKum/Source/Home/View/TodayPromiseView.swift b/KkuMulKum/Source/Home/View/TodayPromiseView.swift index ba67fde6..f0f9512f 100644 --- a/KkuMulKum/Source/Home/View/TodayPromiseView.swift +++ b/KkuMulKum/Source/Home/View/TodayPromiseView.swift @@ -106,7 +106,7 @@ final class TodayPromiseView: BaseView { } let prepareButton = UIButton().then { - $0.setTitle("준비 중", style: .body05, color: .maincolor) + $0.setTitle("준비 시작", style: .body05, color: .maincolor) $0.setLayer(borderWidth: 1, borderColor: .maincolor, cornerRadius: 16) } diff --git a/KkuMulKum/Source/Home/ViewController/HomeViewController.swift b/KkuMulKum/Source/Home/ViewController/HomeViewController.swift index 976bc923..b8e1fa13 100644 --- a/KkuMulKum/Source/Home/ViewController/HomeViewController.swift +++ b/KkuMulKum/Source/Home/ViewController/HomeViewController.swift @@ -199,6 +199,7 @@ private extension HomeViewController { func setPrepareUI() { setProgressButton(rootView.todayPromiseView.prepareButton) + rootView.todayPromiseView.moveButton.setTitle("준비 중", for: .normal) setEnableButton(rootView.todayPromiseView.moveButton) setDisableButton(rootView.todayPromiseView.arriveButton) diff --git a/KkuMulKum/Source/Onboarding/Login/VIewModel/LoginViewModel.swift b/KkuMulKum/Source/Onboarding/Login/VIewModel/LoginViewModel.swift index 499f2725..9babd369 100644 --- a/KkuMulKum/Source/Onboarding/Login/VIewModel/LoginViewModel.swift +++ b/KkuMulKum/Source/Onboarding/Login/VIewModel/LoginViewModel.swift @@ -6,28 +6,39 @@ // import UIKit - import AuthenticationServices + import KakaoSDKUser import KakaoSDKAuth +import Moya enum LoginState { - case loggedIn(userInfo: String) - case notLoggedIn + case notLogin + case login(userInfo: String) + case needOnboarding } class LoginViewModel: NSObject { - private let authService: AuthServiceType - - var loginState: ObservablePattern = ObservablePattern(.notLoggedIn) + var loginState: ObservablePattern = ObservablePattern(.notLogin) var error: ObservablePattern = ObservablePattern("") - init(authService: AuthServiceType) { - self.authService = authService + 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] @@ -39,10 +50,12 @@ class LoginViewModel: NSObject { 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) } @@ -51,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, @@ -80,27 +137,30 @@ extension LoginViewModel: ASAuthorizationControllerDelegate, controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization ) { - guard let appleIDCredential = authorization.credential as? - ASAuthorizationAppleIDCredential else { - error.value = "Failed to get Apple ID Credential" + 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 userIdentifier = appleIDCredential.user - loginState.value = .loggedIn(userInfo: "Apple user: \(userIdentifier)") + 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 981140fd..1f6e993c 100644 --- a/KkuMulKum/Source/Onboarding/Login/ViewController/LoginViewController.swift +++ b/KkuMulKum/Source/Onboarding/Login/ViewController/LoginViewController.swift @@ -10,10 +10,9 @@ import UIKit class LoginViewController: BaseViewController { private let loginView = LoginView() private let loginViewModel: LoginViewModel - - init() { - let authService = AuthService() - self.loginViewModel = LoginViewModel(authService: authService) + + init(viewModel: LoginViewModel = LoginViewModel()) { + self.loginViewModel = viewModel super.init(nibName: nil, bundle: nil) } @@ -39,23 +38,31 @@ class LoginViewController: BaseViewController { let kakaoTapGesture = UITapGestureRecognizer(target: self, action: #selector(kakaoLoginTapped)) loginView.kakaoLoginImageView.addGestureRecognizer(kakaoTapGesture) - - loginView.dummyNextButton.addTarget(self, action: #selector(dummyNextButtonTapped), for: .touchUpInside) + + loginView.dummyNextButton.addTarget( + self, + action: #selector(dummyNextButtonTapped), + for: .touchUpInside + ) } 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 { + print("Login Error: \(error)") owner.showErrorAlert(message: error) } } @@ -68,28 +75,40 @@ class LoginViewController: BaseViewController { @objc private func kakaoLoginTapped() { 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) diff --git a/KkuMulKum/Source/Promise/ReadyStatus/View/SetReadyInfoView.swift b/KkuMulKum/Source/Promise/ReadyStatus/View/SetReadyInfoView.swift new file mode 100644 index 00000000..e93d7234 --- /dev/null +++ b/KkuMulKum/Source/Promise/ReadyStatus/View/SetReadyInfoView.swift @@ -0,0 +1,139 @@ +// +// SetReadyInfoView.swift +// KkuMulKum +// +// Created by 예삐 on 7/14/24. +// + +import UIKit + +import SnapKit +import Then + +final class SetReadyInfoView: BaseView { + + + // MARK: - Property + + private let infoLabel = UILabel().then { + $0.setText("준비 정보를 입력해보세요!", style: .head01, color: .gray8) + } + + private let readyTimeLabel = UILabel().then { + $0.setText("준비 시간", style: .body03, color: .gray8) + } + + private let readyTimeView = UIStackView(axis: .horizontal).then { + $0.spacing = 8 + $0.alignment = .fill + $0.distribution = .fill + } + + let readyHourTextField = CustomTextField(placeHolder: "00") + + private let readyHourLabel = UILabel().then { + $0.setText("시간", style: .body03, color: .gray8) + } + + let readyMinuteTextField = CustomTextField(placeHolder: "00") + + private let readyMinuteLabel = UILabel().then { + $0.setText("분", style: .body03, color: .gray8) + } + + private let moveTimeLabel = UILabel().then { + $0.setText("이동 시간", style: .body03, color: .gray8) + } + + private let moveTimeView = UIStackView(axis: .horizontal).then { + $0.spacing = 8 + $0.alignment = .fill + $0.distribution = .fill + } + + let moveHourTextField = CustomTextField(placeHolder: "00") + + private let moveHourLabel = UILabel().then { + $0.setText("시간", style: .body03, color: .gray8) + } + + let moveMinuteTextField = CustomTextField(placeHolder: "00") + + private let moveMinuteLabel = UILabel().then { + $0.setText("분", style: .body03, color: .gray8) + } + + let doneButton = CustomButton(title: "완료", isEnabled: false) + + + // MARK: - UI Setting + + override func setupView() { + addSubviews(infoLabel, readyTimeLabel, readyTimeView, moveTimeView, moveTimeLabel, doneButton) + readyTimeView.addArrangedSubviews( + readyHourTextField, + readyHourLabel, + readyMinuteTextField, + readyMinuteLabel + ) + moveTimeView.addArrangedSubviews( + moveHourTextField, + moveHourLabel, + moveMinuteTextField, + moveMinuteLabel + ) + } + + override func setupAutoLayout() { + infoLabel.snp.makeConstraints { + $0.top.equalTo(self.safeAreaLayoutGuide.snp.top).offset(24) + $0.leading.equalToSuperview().offset(20) + } + + doneButton.snp.makeConstraints { + $0.leading.trailing.equalToSuperview().inset(20) + $0.bottom.equalToSuperview().offset(-64) + $0.height.equalTo(Screen.height(52)) + } + + readyTimeLabel.snp.makeConstraints { + $0.top.equalTo(infoLabel.snp.bottom).offset(24) + $0.leading.equalToSuperview().offset(20) + } + + readyTimeView.snp.makeConstraints { + $0.top.equalTo(readyTimeLabel.snp.bottom).offset(16) + $0.leading.equalToSuperview().offset(20) + } + + readyHourTextField.snp.makeConstraints { + $0.width.equalTo(56) + $0.height.equalTo(44) + } + + readyMinuteTextField.snp.makeConstraints { + $0.width.equalTo(56) + $0.height.equalTo(44) + } + + moveTimeLabel.snp.makeConstraints { + $0.top.equalTo(readyTimeView.snp.bottom).offset(20) + $0.leading.equalToSuperview().offset(20) + } + + moveTimeView.snp.makeConstraints { + $0.top.equalTo(moveTimeLabel.snp.bottom).offset(16) + $0.leading.equalToSuperview().offset(20) + } + + moveHourTextField.snp.makeConstraints { + $0.width.equalTo(56) + $0.height.equalTo(44) + } + + moveMinuteTextField.snp.makeConstraints { + $0.width.equalTo(56) + $0.height.equalTo(44) + } + } +} diff --git a/KkuMulKum/Source/Promise/ReadyStatus/ViewController/SetReadyInfoViewController.swift b/KkuMulKum/Source/Promise/ReadyStatus/ViewController/SetReadyInfoViewController.swift new file mode 100644 index 00000000..47126132 --- /dev/null +++ b/KkuMulKum/Source/Promise/ReadyStatus/ViewController/SetReadyInfoViewController.swift @@ -0,0 +1,151 @@ +// +// SetReadyInfoViewController.swift +// KkuMulKum +// +// Created by 예삐 on 7/14/24. +// + +import UIKit + +final class SetReadyInfoViewController: BaseViewController { + + + // MARK: - Property + + private let rootView = SetReadyInfoView() + private let viewModel = SetReadyInfoViewModel() + + + // MARK: - LifeCycle + + override func loadView() { + self.view = rootView + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .white + bindViewModel() + } + + override func setupDelegate() { + setTextFieldDelegate() + } + + override func setupAction() { + rootView.readyHourTextField.addTarget( + self, + action: #selector(textFieldDidChange), + for: .editingChanged + ) + rootView.readyMinuteTextField.addTarget( + self, + action: #selector(textFieldDidChange), + for: .editingChanged + ) + rootView.moveHourTextField.addTarget( + self, + action: #selector(textFieldDidChange), + for: .editingChanged + ) + rootView.moveMinuteTextField.addTarget( + self, + action: #selector(textFieldDidChange), + for: .editingChanged + ) + } + + @objc + private func textFieldDidChange(_ textField: UITextField) { + viewModel.checkValid( + readyHourText: rootView.readyHourTextField.text ?? "", + readyMinuteText: rootView.readyMinuteTextField.text ?? "", + moveHourText: rootView.moveHourTextField.text ?? "", + moveMinuteText: rootView.moveMinuteTextField.text ?? "" + ) + } +} + + +// MARK: - UITextFieldDelegate + +extension SetReadyInfoViewController: UITextFieldDelegate { + func textFieldDidBeginEditing(_ textField: UITextField) { + textField.layer.borderColor = UIColor.maincolor.cgColor + } + + func textFieldDidEndEditing(_ textField: UITextField) { + textField.layer.borderColor = UIColor.gray3.cgColor + viewModel.updateTime( + textField: textField.accessibilityIdentifier ?? "", + time: textField.text ?? "" + ) + } + + func textField( + _ textField: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { + let allowedCharacters = CharacterSet.decimalDigits + let characterSet = CharacterSet(charactersIn: string) + return allowedCharacters.isSuperset(of: characterSet) + } +} + + +// MARK: - Function + +private extension SetReadyInfoViewController { + func setTextFieldDelegate() { + let textFields: [(UITextField, String)] = [ + (rootView.readyHourTextField, "readyHour"), + (rootView.readyMinuteTextField, "readyMinute"), + (rootView.moveHourTextField, "moveHour"), + (rootView.moveMinuteTextField, "moveMinute") + ] + + textFields.forEach { (textField, identifier) in + textField.delegate = self + textField.keyboardType = .numberPad + textField.accessibilityIdentifier = identifier + } + } + + func showToast(_ message: String, bottomInset: CGFloat = 128) { + guard let view else { return } + Toast().show(message: message, view: view, position: .bottom, inset: bottomInset) + } + + + // MARK: - Data Bind + + func bindViewModel() { + viewModel.readyHour.bind { [weak self] readyHour in + self?.rootView.readyHourTextField.text = readyHour + } + + viewModel.readyMinute.bind { [weak self] readyMinute in + self?.rootView.readyMinuteTextField.text = readyMinute + } + + viewModel.moveHour.bind { [weak self] moveHour in + self?.rootView.moveHourTextField.text = moveHour + } + + viewModel.moveMinute.bind { [weak self] moveMinute in + self?.rootView.moveMinuteTextField.text = moveMinute + } + + viewModel.isValid.bind { [weak self] isValid in + self?.rootView.doneButton.isEnabled = isValid + } + + viewModel.errMessage.bind { [weak self] err in + if !err.isEmpty { + self?.showToast(err) + } + } + } +} diff --git a/KkuMulKum/Source/Promise/ReadyStatus/ViewModel/SetReadyInfoViewModel.swift b/KkuMulKum/Source/Promise/ReadyStatus/ViewModel/SetReadyInfoViewModel.swift new file mode 100644 index 00000000..8e40a3c0 --- /dev/null +++ b/KkuMulKum/Source/Promise/ReadyStatus/ViewModel/SetReadyInfoViewModel.swift @@ -0,0 +1,62 @@ +// +// SetReadyInfoViewModel.swift +// KkuMulKum +// +// Created by 예삐 on 7/15/24. +// + +import Foundation + +final class SetReadyInfoViewModel { + var isValid = ObservablePattern(false) + var errMessage = ObservablePattern("") + + var readyHour = ObservablePattern("") + var readyMinute = ObservablePattern("") + var moveHour = ObservablePattern("") + var moveMinute = ObservablePattern("") + + //TODO: 준비 및 이동 시간 분 단위로 계산 + var readyTime: Int = 0 + var moveTime: Int = 0 + + private func validTime(time: Int, range: ClosedRange, defaultValue: String) -> String { + if range.contains(time) { + return String(time) + } else { + errMessage.value = "시간은 23시간 59분까지만 입력할 수 있어요!" + return defaultValue + } + } + + func updateTime(textField: String, time: String) { + guard let time = Int(time) else { return } + + switch textField { + case "readyHour": + readyHour.value = validTime(time: time, range: 0...23, defaultValue: "23") + case "readyMinute": + readyMinute.value = validTime(time: time, range: 0...59, defaultValue: "59") + case "moveHour": + moveHour.value = validTime(time: time, range: 0...23, defaultValue: "23") + case "moveMinute": + moveMinute.value = validTime(time: time, range: 0...59, defaultValue: "59") + default: + break + } + } + + func checkValid( + readyHourText: String, + readyMinuteText: String, + moveHourText: String, + moveMinuteText: String + ) { + if !readyHourText.isEmpty && !readyMinuteText.isEmpty + && !moveHourText.isEmpty && !moveMinuteText.isEmpty { + isValid.value = true + } else { + isValid.value = false + } + } +}