diff --git a/KkuMulKum.xcodeproj/project.pbxproj b/KkuMulKum.xcodeproj/project.pbxproj index 080da50c..ab2e0589 100644 --- a/KkuMulKum.xcodeproj/project.pbxproj +++ b/KkuMulKum.xcodeproj/project.pbxproj @@ -47,6 +47,8 @@ 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 */; }; + 789196382C49697B00FF8CDF /* AuthError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789196372C49697B00FF8CDF /* AuthError.swift */; }; 789196342C486F6B00FF8CDF /* KeychainAccessible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789196332C486F6B00FF8CDF /* KeychainAccessible.swift */; }; 789873322C3D1A7B00435E96 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7898732F2C3D1A7B00435E96 /* LoginViewController.swift */; }; 789873332C3D1A7B00435E96 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789873302C3D1A7B00435E96 /* LoginViewModel.swift */; }; @@ -237,6 +239,8 @@ 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 = ""; }; + 789196372C49697B00FF8CDF /* AuthError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthError.swift; sourceTree = ""; }; 789196332C486F6B00FF8CDF /* KeychainAccessible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainAccessible.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 = ""; }; @@ -512,6 +516,15 @@ path = ViewModel; sourceTree = ""; }; + 789196392C49697F00FF8CDF /* Auth */ = { + isa = PBXGroup; + children = ( + 789196372C49697B00FF8CDF /* AuthError.swift */, + 789196352C492F8600FF8CDF /* AuthTargetType.swift */, + ); + path = Auth; + sourceTree = ""; + }; 789873352C3D1B3000435E96 /* View */ = { isa = PBXGroup; children = ( @@ -1120,6 +1133,7 @@ DDE7D2BE2C470A58005A921F /* TargetType */ = { isa = PBXGroup; children = ( + 789196392C49697F00FF8CDF /* Auth */, DDE7D2BF2C470A58005A921F /* LoginTargetType.swift */, DDE7D2C02C470A58005A921F /* ProfileTargetType.swift */, DDE7D2C12C470A58005A921F /* NicknameTargetType.swift */, @@ -1700,6 +1714,7 @@ DD8626622C4606A300E4F980 /* ReadyStatusViewModel.swift in Sources */, DD3F9DC22C481ED5008E1FF7 /* MeetingService.swift in Sources */, DE6D4D102C3F14D80005584B /* InvitationCodePopUpView.swift in Sources */, + 789196382C49697B00FF8CDF /* AuthError.swift in Sources */, A39F2B1B2C47C206008DA5F5 /* SetReadyCompletedViewController.swift in Sources */, DDFA50812C4693BD000A62E2 /* ProfileSetupViewController.swift in Sources */, DD30721A2C3C011600416D9F /* AddPromiseRequestModel.swift in Sources */, @@ -1804,6 +1819,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..4f19525c 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,52 @@ class AuthService: AuthServiceType { keychainService.refreshToken = nil return keychainService.accessToken == nil && keychainService.refreshToken == nil } + + func performRequest( + _ target: AuthTargetType, + completion: @escaping (Result) -> 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/Auth/AuthError.swift b/KkuMulKum/Network/TargetType/Auth/AuthError.swift new file mode 100644 index 00000000..7081a6f6 --- /dev/null +++ b/KkuMulKum/Network/TargetType/Auth/AuthError.swift @@ -0,0 +1,34 @@ +// +// AuthError.swift +// KkuMulKum +// +// Created by 이지훈 on 7/19/24. +// + +import Foundation + +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 + } + } +} diff --git a/KkuMulKum/Network/TargetType/Auth/AuthTargetType.swift b/KkuMulKum/Network/TargetType/Auth/AuthTargetType.swift new file mode 100644 index 00000000..77b2da79 --- /dev/null +++ b/KkuMulKum/Network/TargetType/Auth/AuthTargetType.swift @@ -0,0 +1,104 @@ +// +// AuthTargetType.swift +// KkuMulKum +// +// Created by 이지훈 on 7/18/24. +// + +import Foundation + +import Moya + +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 19a59c25..e11b38c3 100644 --- a/KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileSetupViewModel.swift +++ b/KkuMulKum/Source/Onboarding/Profile/ViewModel/ProfileSetupViewModel.swift @@ -5,6 +5,7 @@ // Created by 이지훈 on 7/10/24. // + import UIKit import RxSwift @@ -25,8 +26,30 @@ class ProfileSetupViewModel { private let disposeBag = DisposeBag() private let provider = MoyaProvider() - init(nickname: String) { + init(nickname: String, authService: AuthServiceType = AuthService()) { self.nickname = nickname + self.authService = authService + } + + func updateProfileImage(_ image: UIImage?) { + profileImage.value = image + isConfirmButtonEnabled.value = image != nil + } + + func uploadProfileImage(completion: @escaping (Bool) -> Void) { + print("uploadProfileImage 함수 호출됨") + guard let image = profileImage.value, + let imageData = image.jpegData(compressionQuality: 0.8) else { + print("이미지 변환 실패") + serverResponse.value = "이미지 변환 중 오류가 발생했습니다." + completion(false) + return + } + + print("이미지 데이터 크기: \(imageData.count) bytes") + + let fileName = "profile_image.jpg" + let mimeType = "image/jpeg" updateProfileImageTrigger .withLatestFrom(profileImage)