From efa3aab585cea30d82189e283478a1f5bb3e1c9f Mon Sep 17 00:00:00 2001 From: hooni Date: Thu, 18 Jul 2024 20:30:51 +0900 Subject: [PATCH] =?UTF-8?q?refactor/#225=20authtarget=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- KkuMulKum.xcodeproj/project.pbxproj | 4 + KkuMulKum/Network/Service/AuthService.swift | 62 ++++++++- .../Network/TargetType/AuthTargetType.swift | 130 ++++++++++++++++++ .../ViewModel/ProfileSetupViewModel.swift | 56 ++------ 4 files changed, 206 insertions(+), 46 deletions(-) create mode 100644 KkuMulKum/Network/TargetType/AuthTargetType.swift diff --git a/KkuMulKum.xcodeproj/project.pbxproj b/KkuMulKum.xcodeproj/project.pbxproj index 36e4a98a..6f81d4d2 100644 --- a/KkuMulKum.xcodeproj/project.pbxproj +++ b/KkuMulKum.xcodeproj/project.pbxproj @@ -47,6 +47,7 @@ 785AE1BE2C2E878600677CA0 /* FirebaseStorageCombine-Community in Frameworks */ = {isa = PBXBuildFile; productRef = 785AE1BD2C2E878600677CA0 /* FirebaseStorageCombine-Community */; }; 785AE1C02C2E878600677CA0 /* FirebaseVertexAI-Preview in Frameworks */ = {isa = PBXBuildFile; productRef = 785AE1BF2C2E878600677CA0 /* FirebaseVertexAI-Preview */; }; 785AE1D12C3B07A600677CA0 /* PrivacyInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 785AE1D02C3B07A600677CA0 /* PrivacyInfo.plist */; }; + 789196362C492F8600FF8CDF /* AuthTargetType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789196352C492F8600FF8CDF /* AuthTargetType.swift */; }; 789873322C3D1A7B00435E96 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7898732F2C3D1A7B00435E96 /* LoginViewController.swift */; }; 789873332C3D1A7B00435E96 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789873302C3D1A7B00435E96 /* LoginViewModel.swift */; }; 789873342C3D1A7B00435E96 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789873312C3D1A7B00435E96 /* LoginView.swift */; }; @@ -237,6 +238,7 @@ 782B407E2C3E44B7008B0CA7 /* WelcomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewModel.swift; sourceTree = ""; }; 782B40812C3E4925008B0CA7 /* NicknameViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NicknameViewModel.swift; sourceTree = ""; }; 785AE1D02C3B07A600677CA0 /* PrivacyInfo.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = PrivacyInfo.plist; sourceTree = ""; }; + 789196352C492F8600FF8CDF /* AuthTargetType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthTargetType.swift; sourceTree = ""; }; 7898732F2C3D1A7B00435E96 /* LoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; 789873302C3D1A7B00435E96 /* LoginViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; 789873312C3D1A7B00435E96 /* LoginView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; @@ -1120,6 +1122,7 @@ DDE7D2BE2C470A58005A921F /* TargetType */ = { isa = PBXGroup; children = ( + 789196352C492F8600FF8CDF /* AuthTargetType.swift */, DDE7D2BF2C470A58005A921F /* LoginTargetType.swift */, DDE7D2C02C470A58005A921F /* ProfileTargetType.swift */, DDE7D2C12C470A58005A921F /* NicknameTargetType.swift */, @@ -1805,6 +1808,7 @@ DD3976862C41C2AD00E2A4C4 /* HomeView.swift in Sources */, DD8626672C4606A300E4F980 /* SetReadyInfoView.swift in Sources */, DD41BEFC2C41D54D0095A068 /* TardyPenaltyView.swift in Sources */, + 789196362C492F8600FF8CDF /* AuthTargetType.swift in Sources */, 789873322C3D1A7B00435E96 /* LoginViewController.swift in Sources */, 78BD61342C45B4A7005752FD /* AuthService.swift in Sources */, DE32D1D22C3BF703006848DF /* LoginUserResponseModel.swift in Sources */, diff --git a/KkuMulKum/Network/Service/AuthService.swift b/KkuMulKum/Network/Service/AuthService.swift index 273c6a55..4aa6ed78 100644 --- a/KkuMulKum/Network/Service/AuthService.swift +++ b/KkuMulKum/Network/Service/AuthService.swift @@ -4,8 +4,8 @@ // // Created by 이지훈 on 7/14/24. // - import Foundation +import Moya protocol AuthServiceType { func saveAccessToken(_ token: String) -> Bool @@ -13,10 +13,17 @@ protocol AuthServiceType { func getAccessToken() -> String? func getRefreshToken() -> String? func clearTokens() -> Bool + func performRequest( + _ target: AuthTargetType, + completion: @escaping ( + Result + ) -> Void + ) } class AuthService: AuthServiceType { private var keychainService: KeychainService + private let provider = MoyaProvider() init(keychainService: KeychainService = DefaultKeychainService.shared) { self.keychainService = keychainService @@ -45,4 +52,57 @@ class AuthService: AuthServiceType { keychainService.refreshToken = nil return keychainService.accessToken == nil && keychainService.refreshToken == nil } + + func performRequest( + _ target: AuthTargetType, + completion: @escaping ( + Result< + T, + NetworkError + > + ) -> Void + ) { + provider.request(target) { result in + switch result { + case .success(let response): + do { + let decodedResponse = try JSONDecoder().decode( + ResponseBodyDTO.self, + from: response.data + ) + if decodedResponse.success { + if let data = decodedResponse.data { + completion(.success(data)) + } else { + completion(.failure(.decodingError)) + } + } else { + let networkError = self.handleErrorResponse(decodedResponse.error) + completion(.failure(networkError)) + } + } catch { + completion(.failure(.decodingError)) + } + case .failure: + completion(.failure(.networkError)) + } + } + } + + private func handleErrorResponse(_ error: ErrorResponse?) -> NetworkError { + guard let error = error else { + return .unknownError("Unknown error occurred") + } + + switch error.code { + case 40080: + return .invalidImageFormat + case 40081: + return .imageSizeExceeded + case 40420: + return .userNotFound + default: + return .unknownError(error.message) + } + } } diff --git a/KkuMulKum/Network/TargetType/AuthTargetType.swift b/KkuMulKum/Network/TargetType/AuthTargetType.swift new file mode 100644 index 00000000..7a144154 --- /dev/null +++ b/KkuMulKum/Network/TargetType/AuthTargetType.swift @@ -0,0 +1,130 @@ +// +// AuthTargetType.swift +// KkuMulKum +// +// Created by 이지훈 on 7/18/24. +// + +import Foundation + +import Moya + +enum NetworkError: Error { + case invalidImageFormat + case imageSizeExceeded + case userNotFound + case decodingError + case networkError + case unknownError(String) + + var message: String { + switch self { + case .invalidImageFormat: + return "이미지 확장자는 jpg, png, webp만 가능합니다." + case .imageSizeExceeded: + return "이미지 사이즈는 5MB를 넘을 수 없습니다." + case .userNotFound: + return "유저를 찾을 수 없습니다." + case .decodingError: + return "데이터 디코딩 중 오류가 발생했습니다." + case .networkError: + return "네트워크 오류가 발생했습니다." + case .unknownError(let message): + return message + } + } +} + +enum AuthTargetType { + case appleLogin(identityToken: String, fcmToken: String) + case kakaoLogin(accessToken: String, fcmToken: String) + case refreshToken(refreshToken: String) + case updateProfileImage(image: Data, fileName: String, mimeType: String) + case updateName(name: String) +} + +extension AuthTargetType: 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 { + switch self { + case .appleLogin, .kakaoLogin: + return "/api/v1/auth/signin" + case .refreshToken: + return "/api/v1/auth/reissue" + case .updateProfileImage: + return "/api/v1/users/me/image" + case .updateName: + return "/api/v1/users/me/name" + } + } + + var method: Moya.Method { + switch self { + case .appleLogin, .kakaoLogin, .refreshToken: + return .post + case .updateProfileImage, .updateName: + return .patch + } + } + + var task: Task { + switch self { + case let .appleLogin(_, fcmToken): + return .requestJSONEncodable(SocialLoginRequestModel(provider: "APPLE", fcmToken: fcmToken)) + case let .kakaoLogin(_, fcmToken): + return .requestJSONEncodable(SocialLoginRequestModel(provider: "KAKAO", fcmToken: fcmToken)) + case .refreshToken: + return .requestPlain + case let .updateProfileImage(imageData, fileName, mimeType): + let formData: [MultipartFormData] = [ + MultipartFormData( + provider: .data(imageData), + name: "image", + fileName: fileName, + mimeType: mimeType + ) + ] + return .uploadMultipart(formData) + case let .updateName(name): + return .requestParameters( + parameters: ["name": name], + 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"] + case .refreshToken(let refreshToken): + return ["Authorization": refreshToken, "Content-Type": "application/json"] + case .updateProfileImage: + guard let token = DefaultKeychainService.shared.accessToken else { + return ["Content-Type": "multipart/form-data"] + } + return [ + "Authorization": "Bearer \(token)", + "Content-Type": "multipart/form-data" + ] + case .updateName: + guard let token = DefaultKeychainService.shared.accessToken else { + fatalError("No access token available") + } + return [ + "Content-Type": "application/json", + "Authorization": "Bearer \(token)" + ] + } + } +} diff --git a/KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileSetupViewModel.swift b/KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileSetupViewModel.swift index ad41ffb8..c225d98b 100644 --- a/KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileSetupViewModel.swift +++ b/KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileSetupViewModel.swift @@ -5,10 +5,8 @@ // Created by 이지훈 on 7/10/24. // -import UIKit -import Moya -import MobileCoreServices +import UIKit class ProfileSetupViewModel { let profileImage = ObservablePattern(UIImage.imgProfile) @@ -16,10 +14,11 @@ class ProfileSetupViewModel { let nickname: String let serverResponse = ObservablePattern(nil) - private let provider = MoyaProvider() + private let authService: AuthServiceType - init(nickname: String) { + init(nickname: String, authService: AuthServiceType = AuthService()) { self.nickname = nickname + self.authService = authService } func updateProfileImage(_ image: UIImage?) { @@ -42,49 +41,16 @@ class ProfileSetupViewModel { let fileName = "profile_image.jpg" let mimeType = "image/jpeg" - provider.request(.updateProfileImage(image: imageData, fileName: fileName, mimeType: mimeType)) { [weak self] result in + authService.performRequest(.updateProfileImage(image: imageData, fileName: fileName, mimeType: mimeType)) { [weak self] (result: Result) in print("네트워크 요청 완료") switch result { - case .success(let response): - print("서버 응답 상태 코드: \(response.statusCode)") - print("서버 응답 데이터: \(String(data: response.data, encoding: .utf8) ?? "디코딩 불가")") - do { - let decodedResponse = try JSONDecoder().decode( - ResponseBodyDTO.self, - from: response.data - ) - if decodedResponse.success { - self?.serverResponse.value = "프로필 이미지가 성공적으로 업로드되었습니다." - print("프로필 이미지 업로드 성공") - completion(true) - } else { - if let errorCode = decodedResponse.error?.code { - switch errorCode { - case 40080: - self?.serverResponse.value = "이미지 확장자는 jpg, png, webp만 가능합니다." - case 40081: - self?.serverResponse.value = "이미지 사이즈는 5MB를 넘을 수 없습니다." - case 40420: - self?.serverResponse.value = "유저를 찾을 수 없습니다." - default: - self?.serverResponse.value = decodedResponse.error?.message ?? - "알 수 없는 오류가 발생했습니다." - } - } else { - self?.serverResponse.value = decodedResponse.error?.message ?? - "알 수 없는 오류가 발생했습니다." - } - print("프로필 이미지 업로드 실패: \(self?.serverResponse.value ?? "")") - completion(false) - } - } catch { - self?.serverResponse.value = "데이터 디코딩 중 오류가 발생했습니다." - print("데이터 디코딩 오류: \(error)") - completion(false) - } + case .success: + self?.serverResponse.value = "프로필 이미지가 성공적으로 업로드되었습니다." + print("프로필 이미지 업로드 성공") + completion(true) case .failure(let error): - self?.serverResponse.value = "네트워크 오류: \(error.localizedDescription)" - print("네트워크 오류: \(error.localizedDescription)") + self?.serverResponse.value = error.message + print("프로필 이미지 업로드 실패: \(error.message)") completion(false) } }