From 80ce0d1eb1713d026f7b0153c49be965e75073fb Mon Sep 17 00:00:00 2001 From: 00yhsp <00yhsp@naver.com> Date: Mon, 12 Feb 2024 11:38:44 +0900 Subject: [PATCH] =?UTF-8?q?[#73]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20API?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EC=99=84=EB=A3=8C=20|=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20=ED=82=A4=EC=B2=B4?= =?UTF-8?q?=EC=9D=B8=20=ED=86=A0=ED=81=B0=20=EC=A0=80=EC=9E=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Spon-us.xcodeproj/project.pbxproj | 4 + Spon-us/Manager/KeyChainManager.swift | 124 ++++++++++++++++++ Spon-us/Model/Onboarding/LoginModel.swift | 7 +- Spon-us/Model/Onboarding/LoginViewModel.swift | 15 ++- Spon-us/View/Onboarding/LoginView.swift | 40 ++++-- 5 files changed, 170 insertions(+), 20 deletions(-) create mode 100644 Spon-us/Manager/KeyChainManager.swift diff --git a/Spon-us.xcodeproj/project.pbxproj b/Spon-us.xcodeproj/project.pbxproj index 860d3d3..28207ff 100644 --- a/Spon-us.xcodeproj/project.pbxproj +++ b/Spon-us.xcodeproj/project.pbxproj @@ -100,6 +100,7 @@ DF498F212B790BD000ADE078 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DF498F202B790BCF00ADE078 /* GoogleService-Info.plist */; }; DF498F232B791EA800ADE078 /* LoginModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF498F222B791EA800ADE078 /* LoginModel.swift */; }; DF498F252B791EB400ADE078 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF498F242B791EB400ADE078 /* LoginViewModel.swift */; }; + DF498F272B79B45D00ADE078 /* KeyChainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF498F262B79B45D00ADE078 /* KeyChainManager.swift */; }; DF90A5AB2B64EB5500BC54D0 /* TermsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF90A5AA2B64EB5500BC54D0 /* TermsView.swift */; }; DF90A5AD2B664E5A00BC54D0 /* ProcessingPolicyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF90A5AC2B664E5A00BC54D0 /* ProcessingPolicyView.swift */; }; DF90A5B02B664E9600BC54D0 /* GatherAndUsageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF90A5AF2B664E9600BC54D0 /* GatherAndUsageView.swift */; }; @@ -174,6 +175,7 @@ DF498F202B790BCF00ADE078 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; DF498F222B791EA800ADE078 /* LoginModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginModel.swift; sourceTree = ""; }; DF498F242B791EB400ADE078 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; + DF498F262B79B45D00ADE078 /* KeyChainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyChainManager.swift; sourceTree = ""; }; DF90A5AA2B64EB5500BC54D0 /* TermsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsView.swift; sourceTree = ""; }; DF90A5AC2B664E5A00BC54D0 /* ProcessingPolicyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessingPolicyView.swift; sourceTree = ""; }; DF90A5AF2B664E9600BC54D0 /* GatherAndUsageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GatherAndUsageView.swift; sourceTree = ""; }; @@ -443,6 +445,7 @@ 3B81BCB32B622ED00067E9CB /* StoreKitTestView.swift */, 3B81BCBD2B62312C0067E9CB /* Product.storekit */, 3B81BCBF2B6231680067E9CB /* ProductList.plist */, + DF498F262B79B45D00ADE078 /* KeyChainManager.swift */, ); path = Manager; sourceTree = ""; @@ -646,6 +649,7 @@ 100A1E322B73485800AAC1E8 /* SponusAPI.swift in Sources */, 807BF83F2B51BCD400A659B9 /* SearchView.swift in Sources */, DF90A5AB2B64EB5500BC54D0 /* TermsView.swift in Sources */, + DF498F272B79B45D00ADE078 /* KeyChainManager.swift in Sources */, 3B36F0A12B6FEBF60000ACFB /* Utils.swift in Sources */, 100D38A72B44836800498977 /* Spon_usApp.swift in Sources */, 100A1E382B734DB300AAC1E8 /* EmailModel.swift in Sources */, diff --git a/Spon-us/Manager/KeyChainManager.swift b/Spon-us/Manager/KeyChainManager.swift new file mode 100644 index 0000000..0d7b40e --- /dev/null +++ b/Spon-us/Manager/KeyChainManager.swift @@ -0,0 +1,124 @@ +// +// KeyChainManager.swift +// Spon-us +// +// Created by 박현수 on 2/12/24. +// + +import Foundation + +enum KeychainError: Error { + case itemNotFound + case duplicateItem + case invalidItemFormat + case unknown(OSStatus) +} + +class KeychainManager { + static let service = Bundle.main.bundleIdentifier + + static func save(account: String, value: String, isForce: Bool = false) throws { + try save(account: account, value: value.data(using: .utf8)!, isForce: isForce) + } + + static func save(account: String, value: Data, isForce: Bool = false) throws { + let query: [String: AnyObject] = [ + kSecAttrService as String: service as AnyObject, + kSecAttrAccount as String: account as AnyObject, + kSecClass as String: kSecClassGenericPassword, + kSecValueData as String: value as AnyObject, + ] + + let status = SecItemAdd(query as CFDictionary, nil) + + if status == errSecDuplicateItem { + if isForce { + try update(account: account, value: value) + return + } else { + throw KeychainError.duplicateItem + } + } + + guard status == errSecSuccess else { + throw KeychainError.unknown(status) + } + } + + // MARK: - Update + + static func update(account: String, value: String) throws { + try update(account: account, value: value.data(using: .utf8)!) + } + + static func update(account: String, value: Data) throws { + let query: [String: AnyObject] = [ + kSecAttrService as String: service as AnyObject, + kSecAttrAccount as String: account as AnyObject, + kSecClass as String: kSecClassGenericPassword, + kSecValueData as String: value as AnyObject, + ] + + let attributes: [String: AnyObject] = [ + kSecValueData as String: value as AnyObject + ] + + let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) + + guard status != errSecDuplicateItem else { + throw KeychainError.duplicateItem + } + + guard status == errSecSuccess else { + throw KeychainError.unknown(status) + } + } + + // MARK: - Load + + static func load(account: String) throws -> String { + try String(decoding: load(account: account), as: UTF8.self) + } + + static func load(account: String) throws -> Data { + let query: [String: AnyObject] = [ + kSecAttrService as String: service as AnyObject, + kSecAttrAccount as String: account as AnyObject, + kSecClass as String: kSecClassGenericPassword, + kSecMatchLimit as String: kSecMatchLimitOne, + kSecReturnData as String: kCFBooleanTrue, + ] + + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + guard status != errSecItemNotFound else { + throw KeychainError.itemNotFound + } + + guard status == errSecSuccess else { + throw KeychainError.unknown(status) + } + + guard let password = result as? Data else { + throw KeychainError.invalidItemFormat + } + + return password + } + + // MARK: - Delete + + static func delete(account: String) throws { + let query: [String: AnyObject] = [ + kSecAttrService as String: service as AnyObject, + kSecAttrAccount as String: account as AnyObject, + kSecClass as String: kSecClassGenericPassword + ] + + let status = SecItemDelete(query as CFDictionary) + + guard status == errSecSuccess else { + throw KeychainError.unknown(status) + } + } +} diff --git a/Spon-us/Model/Onboarding/LoginModel.swift b/Spon-us/Model/Onboarding/LoginModel.swift index ed70d92..ea08d93 100644 --- a/Spon-us/Model/Onboarding/LoginModel.swift +++ b/Spon-us/Model/Onboarding/LoginModel.swift @@ -7,21 +7,20 @@ import Foundation -struct LoginModelContent200: Codable { +struct LoginModelContent201: Codable { let accessToken: String let refreshToken: String } -struct LoginModel200: Codable { +struct LoginModel201: Codable { let statusCode: String let message: String - let content: LoginModelContent200 + let content: LoginModelContent201 } struct LoginModel401: Codable { let statusCode: String let message: String - let content: String } struct LoginRequestBody: Codable { diff --git a/Spon-us/Model/Onboarding/LoginViewModel.swift b/Spon-us/Model/Onboarding/LoginViewModel.swift index 8c004a7..61dab71 100644 --- a/Spon-us/Model/Onboarding/LoginViewModel.swift +++ b/Spon-us/Model/Onboarding/LoginViewModel.swift @@ -9,23 +9,28 @@ import Foundation import Moya class LoginViewModel: ObservableObject { - @Published var login200: LoginModel200? + @Published var login201: LoginModel201? @Published var login401: LoginModel401? + @Published var isBadRequest = false private let provider = MoyaProvider(plugins: [NetworkLoggerPlugin()]) - func postLogin(email: String, password: String, fcmToken: String) { + func postLogin(email: String, password: String, fcmToken: String, completion: @escaping (Bool) -> Void) { provider.request(.postLogin(email: email, password: password, fcmToken: fcmToken)) { result in switch result { case let .success(response): do { - if response.statusCode == 200 { - let loginResponse = try response.map(LoginModel200.self) - self.login200 = loginResponse + if response.statusCode == 201 { + let loginResponse = try response.map(LoginModel201.self) + self.login201 = loginResponse + self.isBadRequest = false + completion(true) } else { let loginResponse = try response.map(LoginModel401.self) self.login401 = loginResponse + self.isBadRequest = true + completion(false) } } catch { print("Error parsing response: \(error)") diff --git a/Spon-us/View/Onboarding/LoginView.swift b/Spon-us/View/Onboarding/LoginView.swift index 8246d95..94834b1 100644 --- a/Spon-us/View/Onboarding/LoginView.swift +++ b/Spon-us/View/Onboarding/LoginView.swift @@ -7,14 +7,18 @@ import SwiftUI import Firebase +import KeychainSwift struct LoginView: View { + @ObservedObject var loginViewModel = LoginViewModel() + @State var userID = "" @State private var userPW = "" @State private var isPWSecure = true @State var isEmailValid = false @State var goToContentView = false @State var goToTermsView = false + @State var disableButton = false @FocusState private var isEmailTextFieldFocused: Bool @FocusState private var isPWTextFieldFocused: Bool @@ -89,37 +93,51 @@ struct LoginView: View { } } }.padding(.bottom, 6) - if (isPWSecureFieldFocused || isPWTextFieldFocused) { + if loginViewModel.isBadRequest { Divider() - .background(.sponusPrimary) + .background(.sponusRed).padding(.bottom, 8) + HStack(spacing: 0) { + Image(.icWarning).resizable().frame(width: 14, height: 14).padding(.trailing, 4) + Text("존재하지 않는 이메일 혹은 비밀번호입니다") + .font(.system(size: 12)) + .foregroundStyle(.sponusRed) + Spacer() + } } - // else if (((userID.wholeMatch(of: emailRegexPattern)?.output) != nil) || userID.isEmpty) { - // Divider() - // .background(.sponusGrey200) - // } else { - Divider().background(.sponusGrey200) + Divider() + .background((isPWSecureFieldFocused || isPWTextFieldFocused) ? .sponusPrimary : .sponusGrey200) } }.padding(.horizontal, 20) .padding(.bottom, 48) Button { + disableButton = true Messaging.messaging().token { token, error in if let error = error { print("Error fetching FCM registration token: \(error)") } else if let token = token { print("FCM registration token: \(token)") - // 여기서 토큰을 사용하거나 저장합니다. + loginViewModel.postLogin(email: userID, password: userPW, fcmToken: token) { success in + if success { + print("access token : \n\(String(describing: loginViewModel.login201?.content.accessToken))") + print("refresh token : \n\(String(describing: loginViewModel.login201?.content.refreshToken))") + goToContentView = true + } + else { + print("401\n\(String(describing: loginViewModel.login401?.message))") + } + } } } - //goToContentView = true + disableButton = false } label: { Text("로그인") .font(.Body04).foregroundStyle(.sponusWhite) .frame(maxWidth: .infinity).frame(height: 56) - .background(.sponusPrimary) + .background(disableButton ? .sponusGrey600 : .sponusPrimary) .padding(.horizontal, 20) .padding(.bottom, 20) - }.fullScreenCover(isPresented: $goToContentView, content: { + }.disabled(disableButton).fullScreenCover(isPresented: $goToContentView, content: { ContentView() }) Button {