From 95a55a32936e3a9f290bc2afe3f46e059718f2a4 Mon Sep 17 00:00:00 2001 From: HELLOHIDI Date: Mon, 14 Oct 2024 09:54:47 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20=ED=8F=B4=EB=8D=94=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EB=B0=8F=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20#92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modules/Networks/Sources/Example.swift | 0 .../Sources/{ => Legacy}/Base/.gitkeep | 0 .../{ => Legacy}/Base/AuthInterceptor.swift | 0 .../Sources/{ => Legacy}/Base/BaseModel.swift | 0 .../{ => Legacy}/Base/BaseTargetType.swift | 0 .../{ => Legacy}/Base/NetworkProvider.swift | 0 .../Sources/{ => Legacy}/Foundation/.gitkeep | 0 .../{ => Legacy}/Foundation/Config.swift | 0 .../Foundation/MoyaLoggerPlugin.swift | 0 .../Foundation/NetworkHelper.swift | 0 .../Foundation/NetworkResult.swift | 0 .../Foundation/ResponseData.swift | 0 .../Sources/{ => Legacy}/Router/.gitkeep | 0 .../{ => Legacy}/Router/AuthRouter.swift | 10 +- .../{ => Legacy}/Router/ChallengeRouter.swift | 14 +- .../{ => Legacy}/Router/MyPageRouter.swift | 2 +- .../{ => Legacy}/Router/PointRouter.swift | 12 +- .../Sources/{ => Legacy}/Service/.gitkeep | 0 .../{ => Legacy}/Service/Providers.swift | 0 .../Refactor/Foundation/Base/BaseAPI.swift | 23 ++++ .../Foundation/Base/BaseService.swift | 100 ++++++++++++++ .../Constants/HMHNetworkError.swift | 126 ++++++++++++++++++ .../Refactor/Foundation/Constants/Paths.swift | 35 +++++ .../Foundation/Handler/ErrorHandler.swift | 33 +++++ .../Handler/NetworkLogHandler.swift | 62 +++++++++ .../Foundation/Handler/RequestHandler.swift | 52 ++++++++ .../Foundation/Models/GenericResponse.swift | 51 +++++++ .../Foundation/Models/NetworkResponse.swift | 21 +++ .../RequestBuilder/APIHeaders.swift} | 6 +- .../RequestBuilder/HTTPMethod.swift | 20 +++ .../RequestBuilder/ParameterEncoding.swift | 77 +++++++++++ .../Foundation/RequestBuilder/Task.swift | 41 ++++++ .../RequestBuilder/URLRequestTargetType.swift | 34 +++++ 33 files changed, 697 insertions(+), 22 deletions(-) delete mode 100644 HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Example.swift rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Base/.gitkeep (100%) rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Base/AuthInterceptor.swift (100%) rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Base/BaseModel.swift (100%) rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Base/BaseTargetType.swift (100%) rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Base/NetworkProvider.swift (100%) rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Foundation/.gitkeep (100%) rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Foundation/Config.swift (100%) rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Foundation/MoyaLoggerPlugin.swift (100%) rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Foundation/NetworkHelper.swift (100%) rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Foundation/NetworkResult.swift (100%) rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Foundation/ResponseData.swift (100%) rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Router/.gitkeep (100%) rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Router/AuthRouter.swift (88%) rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Router/ChallengeRouter.swift (87%) rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Router/MyPageRouter.swift (93%) rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Router/PointRouter.swift (86%) rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Service/.gitkeep (100%) rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{ => Legacy}/Service/Providers.swift (100%) create mode 100644 HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Base/BaseAPI.swift create mode 100644 HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Base/BaseService.swift create mode 100644 HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Constants/HMHNetworkError.swift create mode 100644 HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Constants/Paths.swift create mode 100644 HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Handler/ErrorHandler.swift create mode 100644 HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Handler/NetworkLogHandler.swift create mode 100644 HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Handler/RequestHandler.swift create mode 100644 HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Models/GenericResponse.swift create mode 100644 HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Models/NetworkResponse.swift rename HMH_Tuist_iOS/Projects/Modules/Networks/Sources/{Foundation/APIConstants.swift => Refactor/Foundation/RequestBuilder/APIHeaders.swift} (94%) create mode 100644 HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/HTTPMethod.swift create mode 100644 HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/ParameterEncoding.swift create mode 100644 HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/Task.swift create mode 100644 HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/URLRequestTargetType.swift diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Example.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Example.swift deleted file mode 100644 index e69de29b..00000000 diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Base/.gitkeep b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Base/.gitkeep similarity index 100% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Base/.gitkeep rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Base/.gitkeep diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Base/AuthInterceptor.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Base/AuthInterceptor.swift similarity index 100% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Base/AuthInterceptor.swift rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Base/AuthInterceptor.swift diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Base/BaseModel.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Base/BaseModel.swift similarity index 100% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Base/BaseModel.swift rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Base/BaseModel.swift diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Base/BaseTargetType.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Base/BaseTargetType.swift similarity index 100% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Base/BaseTargetType.swift rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Base/BaseTargetType.swift diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Base/NetworkProvider.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Base/NetworkProvider.swift similarity index 100% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Base/NetworkProvider.swift rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Base/NetworkProvider.swift diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Foundation/.gitkeep b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Foundation/.gitkeep similarity index 100% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Foundation/.gitkeep rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Foundation/.gitkeep diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Foundation/Config.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Foundation/Config.swift similarity index 100% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Foundation/Config.swift rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Foundation/Config.swift diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Foundation/MoyaLoggerPlugin.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Foundation/MoyaLoggerPlugin.swift similarity index 100% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Foundation/MoyaLoggerPlugin.swift rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Foundation/MoyaLoggerPlugin.swift diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Foundation/NetworkHelper.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Foundation/NetworkHelper.swift similarity index 100% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Foundation/NetworkHelper.swift rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Foundation/NetworkHelper.swift diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Foundation/NetworkResult.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Foundation/NetworkResult.swift similarity index 100% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Foundation/NetworkResult.swift rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Foundation/NetworkResult.swift diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Foundation/ResponseData.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Foundation/ResponseData.swift similarity index 100% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Foundation/ResponseData.swift rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Foundation/ResponseData.swift diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Router/.gitkeep b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Router/.gitkeep similarity index 100% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Router/.gitkeep rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Router/.gitkeep diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Router/AuthRouter.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Router/AuthRouter.swift similarity index 88% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Router/AuthRouter.swift rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Router/AuthRouter.swift index 531e38a9..5aa91722 100644 --- a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Router/AuthRouter.swift +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Router/AuthRouter.swift @@ -22,15 +22,15 @@ extension AuthRouter: BaseTargetType { var headers: Parameters? { switch self { case .socialLogin: - return APIConstants.hasSocialTokenHeader + return APIHeaders.hasSocialTokenHeader case .signUp: - return APIConstants.signUpHeader + return APIHeaders.signUpHeader case .tokenRefresh: - return APIConstants.hasRefreshTokenHeader + return APIHeaders.hasRefreshTokenHeader case .revoke: - return APIConstants.hasTokenHeader + return APIHeaders.hasTokenHeader case .logout: - return APIConstants.hasTokenHeader + return APIHeaders.hasTokenHeader } } diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Router/ChallengeRouter.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Router/ChallengeRouter.swift similarity index 87% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Router/ChallengeRouter.swift rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Router/ChallengeRouter.swift index b68d4f67..0c53536c 100644 --- a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Router/ChallengeRouter.swift +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Router/ChallengeRouter.swift @@ -24,19 +24,19 @@ extension ChallengeRouter: BaseTargetType { var headers: [String : String]? { switch self { case .createChallenge: - return APIConstants.hasTokenHeader + return APIHeaders.hasTokenHeader case .dailyChallengeFail : - return APIConstants.hasTokenHeader + return APIHeaders.hasTokenHeader case .getChallenge: - return APIConstants.hasTokenHeader + return APIHeaders.hasTokenHeader case .getdailyChallenge: - return APIConstants.hasTokenHeader + return APIHeaders.hasTokenHeader case .addApp: - return APIConstants.hasTokenHeader + return APIHeaders.hasTokenHeader case .deleteApp: - return APIConstants.hasTokenHeader + return APIHeaders.hasTokenHeader case .postDailyChallenge: - return APIConstants.hasTokenHeader + return APIHeaders.hasTokenHeader } } diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Router/MyPageRouter.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Router/MyPageRouter.swift similarity index 93% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Router/MyPageRouter.swift rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Router/MyPageRouter.swift index 9ff31f5d..0f27dc37 100644 --- a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Router/MyPageRouter.swift +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Router/MyPageRouter.swift @@ -18,7 +18,7 @@ extension MyPageRouter: BaseTargetType { var headers: [String : String]? { switch self { case .getUserData: - return APIConstants.hasTokenHeader + return APIHeaders.hasTokenHeader } } diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Router/PointRouter.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Router/PointRouter.swift similarity index 86% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Router/PointRouter.swift rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Router/PointRouter.swift index c5d03022..f36c337a 100644 --- a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Router/PointRouter.swift +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Router/PointRouter.swift @@ -23,17 +23,17 @@ extension PointRouter: BaseTargetType { var headers: [String : String]? { switch self { case .getUsagePoint: - return APIConstants.hasAccessTokenHeader + return APIHeaders.hasAccessTokenHeader case .patchEarnPoint : - return APIConstants.hasTokenHeader + return APIHeaders.hasTokenHeader case .getEarnPoint: - return APIConstants.hasAccessTokenHeader + return APIHeaders.hasAccessTokenHeader case .getPointList: - return APIConstants.hasAccessTokenHeader + return APIHeaders.hasAccessTokenHeader case .patchPointUse: - return APIConstants.hasAccessTokenHeader + return APIHeaders.hasAccessTokenHeader case .getCurrentPoint: - return APIConstants.hasAccessTokenHeader + return APIHeaders.hasAccessTokenHeader } } diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Service/.gitkeep b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Service/.gitkeep similarity index 100% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Service/.gitkeep rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Service/.gitkeep diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Service/Providers.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Service/Providers.swift similarity index 100% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Service/Providers.swift rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Legacy/Service/Providers.swift diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Base/BaseAPI.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Base/BaseAPI.swift new file mode 100644 index 00000000..b7c493b5 --- /dev/null +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Base/BaseAPI.swift @@ -0,0 +1,23 @@ +// +// BaseAPI.swift +// Networks +// +// Created by 류희재 on 10/14/24. +// Copyright © 2024 HMH-iOS. All rights reserved. +// + +import Foundation + +protocol BaseAPI: URLRequestTargetType { } + +extension BaseAPI { + public var url: String { + return Config.baseURL + } + + public var headers: [String: String]? { + return APIHeaders.noTokenHeader + } +} + + diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Base/BaseService.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Base/BaseService.swift new file mode 100644 index 00000000..0aa6e496 --- /dev/null +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Base/BaseService.swift @@ -0,0 +1,100 @@ +// +// BaseService.swift +// Networks +// +// Created by 류희재 on 10/14/24. +// Copyright © 2024 HMH-iOS. All rights reserved. +// + +import Foundation +import Combine + +final class BaseService { + + typealias API = Target + + private let requestHandler = RequestHandler.shared + + func requestWithResult(_ target: API, _ responseType: T.Type) -> AnyPublisher { + return fetchResponse(with: target) + .flatMap { response in + self.validate(response: response) + .map { _ in response.data! } + .mapError { ErrorHandler.handleError(target, error: $0) } + } + .flatMap { self.decode(data: $0, target: target) } + .eraseToAnyPublisher() + } + + func requestWithNoResult(_ target: API) -> AnyPublisher { + return fetchResponse(with: target) + .flatMap { response -> AnyPublisher in + self.validate(response: response) // validate 연결 + .map { _ in response.data! } // 성공 시 data 반환 + .eraseToAnyPublisher() + } + .mapError { ErrorHandler.handleError(target, error: $0) } + .flatMap { data -> AnyPublisher in + self.decode(data: data, target: target) + } + .map { _ in () } + .eraseToAnyPublisher() + } +} + + +extension BaseService { + /// 네트워크 응답 처리 메소드 + private func fetchResponse(with target: API) -> AnyPublisher { + return requestHandler.executeRequest(for: target) + .handleEvents(receiveSubscription: { _ in + NetworkLogHandler.requestLogging(target) + }, receiveOutput: { response in + NetworkLogHandler.responseSuccess(target, result: response) + }) + .mapError { ErrorHandler.handleError(target, error: $0) } + .eraseToAnyPublisher() + } + + /// 응답 유효성 검사 메서드 + private func validate(response: NetworkResponse) -> AnyPublisher { + guard response.response.isValidateStatus() else { + guard let data = response.data else { + return Fail(error: HMHNetworkError.invalidResponse(.invalidStatusCode(code: response.response.statusCode))) + .eraseToAnyPublisher() + } + + return Just(data) + .decode(type: ErrorResponse.self, decoder: JSONDecoder()) + .mapError { _ in HMHNetworkError.invalidResponse(.invalidStatusCode(code: response.response.statusCode)) } + .flatMap { response in + Fail(error: HMHNetworkError.invalidResponse(.invalidStatusCode( + code: response.statusCode, + data: response.data + ) + ) + ).eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } + return Just(()).setFailureType(to: HMHNetworkError.self).eraseToAnyPublisher() + } + + /// 디코딩 메소드 + private func decode(data: Data, target: API) -> AnyPublisher { + return Just(data) + .decode(type: GenericResponse.self, decoder: JSONDecoder()) + .mapError { _ in ErrorHandler.handleError(target, error: .decodingFailed(.failed)) } + .map { $0.data! } + .eraseToAnyPublisher() + } + +} + +// HTTP 상태코드 유효성 검사 +extension HTTPURLResponse { + func isValidateStatus() -> Bool { + return (200...299).contains(self.statusCode) + } +} + diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Constants/HMHNetworkError.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Constants/HMHNetworkError.swift new file mode 100644 index 00000000..f39357ee --- /dev/null +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Constants/HMHNetworkError.swift @@ -0,0 +1,126 @@ +// +// HMHNetworkError.swift +// Networks +// +// Created by 류희재 on 10/14/24. +// Copyright © 2024 HMH-iOS. All rights reserved. +// + +import Foundation + +@frozen public enum HMHNetworkError: Error { + case invalidRequest(RequestError) + case invalidResponse(ResponseError) + case decodingFailed(DecodeError) + case unknown(Error) + + var description: String { + switch self { + case .invalidRequest(let requestError): + return "요청 시 발생된" + requestError.description + case .invalidResponse(let responseError): + return "응답 시 발생된" + responseError.description + case .decodingFailed(let decodeError): + return decodeError.description + case .unknown(let error): + return "알 수 없는 오류 \(error)가 발생하였습니다!" + } + } +} + +extension HMHNetworkError { + public enum RequestError: Error { + case parameterEncodingFailed(ParameterEncoding) // 인코딩시 생기는 에러 + case invalidURL(String) // url이 유효하지 않을때 + case unknownErr // 그 외 예기치 못한 에러 + + var description: String { + switch self { + case .parameterEncodingFailed(let parameterEncoding): + return "인코딩 시 발생한" + parameterEncoding.description + case .invalidURL(let string): + return "\(string)은 유효한 url이 아닙니다" + case .unknownErr: + return "요청 시 발생한 알 수 없는 에러입니다." + } + } + } + + public enum ParameterEncoding: Error { + case emptyParameters // 파라미터가 비어있을 때 + case missingURL // url이 없을때 + case invalidJSON // json 형식에 맞지 않을때 + case jsonEncodingFailed // json으로 인코딩 할 시 + + var description: String { + switch self { + case .emptyParameters: + return "파라미터가 비어있는 에러입니다." + case .missingURL: + return "url이 없습니다" + case .invalidJSON: + return "json 형식에 맞지 않습니다." + case .jsonEncodingFailed: + return "json 인코딩 시 발생한 에러입니다." + } + } + } +} + +extension HMHNetworkError { + public enum ResponseError: Error { + case cancelled + case unhandled + case invalidStatusCode(code: Int, data: String? = nil) + + var description: String { + switch self { + case .cancelled: + return "취소되었습니다." + case .unhandled: + return "응답이 올바르지 않습니다" + case .invalidStatusCode(let code, let errMessage): + switch code { + case 401: + return "autheticationError: 인증오류입니다" + case 403: + return errMessage ?? "forbiddeError: 금지된 에러입니다" + case 404: + return errMessage ?? "notFoundError: 찾을 수 없습니다" + case 408: + return "timeoutError: 시간을 초과했습니다" + case 409: + return errMessage ?? "409 -> 해당 statuscode와 관련된 오류입니다" + case 500: + return "internalServerError: 서버 내부 오류입니다" + default: + return "\(code) -> 해당 statuscode와 관련된 오류입니다" + } + } + } + + var statusCode: Int? { + if case let .invalidStatusCode(code, _) = self { + return code + } + return nil + } + } +} + +extension HMHNetworkError { + public enum DecodeError: Error { + case failed + case dataIsNil + + var description: String { + switch self { + case .failed: + return "디코딩에 실패했습니다" + case .dataIsNil: + return "데이터가 존재하지 않습니다." + } + } + } +} + diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Constants/Paths.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Constants/Paths.swift new file mode 100644 index 00000000..27c2065e --- /dev/null +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Constants/Paths.swift @@ -0,0 +1,35 @@ +// +// Paths.swift +// Networks +// +// Created by 류희재 on 10/14/24. +// Copyright © 2024 HMH-iOS. All rights reserved. +// + +public enum Paths { + + //MARK: - Auth + + static let signUp = "/auth/sign-up" + static let signIn = "/auth/sign-in" + + //MARK: - User + + static let editNickName = "/users/my/nickname" + static let deleteAccount = "/users/my" + static let getUserData = "/users" + + //MARK: - Room + + static let createRoom = "/rooms" + static let getRooms = "/rooms" + static let getRoomDetail = "/rooms/{roomId}" + static let editRoomInfo = "/rooms/{roomId}" + static let deleteRoom = "/rooms/{roomId}" + static let getRoomMyInfo = "/rooms/{roomId}/my" + static let matchRoom = "/rooms/{roomId}/match" + static let enterRoom = "/rooms/enter" + static let exitRoom = "/rooms/{roomId}/exit" + static let deleteHistoryRoom = "/rooms/{roomId}/history" +} + diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Handler/ErrorHandler.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Handler/ErrorHandler.swift new file mode 100644 index 00000000..02b7f304 --- /dev/null +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Handler/ErrorHandler.swift @@ -0,0 +1,33 @@ +// +// ErrorHandler.swift +// Networks +// +// Created by 류희재 on 10/14/24. +// Copyright © 2024 HMH-iOS. All rights reserved. +// + +import Foundation +import Combine + +//class ErrorHandler { +// static let shared = ErrorHandler() +// private init () {} +// +// private let loggingHandler = NetworkLogHandler.shared +// +// func handleError(_ target: T, error: HMHNetworkError) -> HMHNetworkError { +//// guard let hmhNetworkError = error as? HMHNetworkError else { +//// return .unknown +//// } +// loggingHandler.responseError(target, result: error) +// +// return error +// } +//} + +struct ErrorHandler { + static func handleError(_ target: T, error: HMHNetworkError) -> HMHNetworkError { NetworkLogHandler.responseError(target, result: error) + return error + } +} + diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Handler/NetworkLogHandler.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Handler/NetworkLogHandler.swift new file mode 100644 index 00000000..e6985ec5 --- /dev/null +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Handler/NetworkLogHandler.swift @@ -0,0 +1,62 @@ +// +// NetworkLogHandler.swift +// Networks +// +// Created by 류희재 on 10/14/24. +// Copyright © 2024 HMH-iOS. All rights reserved. +// + +import Foundation + +struct NetworkLogHandler { + + // 네트워크 요청 로깅 함수 + static func requestLogging(_ endpoint: URLRequestTargetType) { + let url = endpoint.url + (endpoint.path ?? "") + let method = endpoint.method.rawValue + let headers = endpoint.headers ?? [:] + let parameters = endpoint.parameters ?? [:] + + print(""" + ================== 📤 Request ===================> + 📝 URL: \(url) + 📝 HTTP Method: \(method) + 📝 Header: \(headers) + 📝 Parameters: \(parameters) + ================================ + """) + } + + // 성공적인 응답 로깅 함수 + static func responseSuccess(_ endpoint: any URLRequestTargetType, result response: NetworkResponse) { + let url = endpoint.url + (endpoint.path ?? "") + let headers = endpoint.headers ?? [:] + let responseData = String(data: response.data ?? Data(), encoding: .utf8) ?? "No data" + + print(""" + ======================== 📥 Response <======================== + ========================= ✅ Success ========================= + ✌🏻 URL: \(url) + ✌🏻 Header: \(headers) + ✌🏻 Success Data: \(responseData) + ============================================================== + """) + } + + // 에러 응답 로깅 함수 + static func responseError(_ endpoint: any URLRequestTargetType, result error: HMHNetworkError) { + let url = endpoint.url + (endpoint.path ?? "") + let headers = endpoint.headers ?? [:] + + print(""" + ======================== 📥 Response <======================== + ========================= ❌ Error ========================== + ❗️ Error Type: \(error.description) + ❗️ URL: \(url) + ❗️ Header: \(headers) + ❗️ Error Data: \(error.localizedDescription) + ============================================================== + """) + } +} + diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Handler/RequestHandler.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Handler/RequestHandler.swift new file mode 100644 index 00000000..1cc8c270 --- /dev/null +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Handler/RequestHandler.swift @@ -0,0 +1,52 @@ +// +// RequestHandler.swift +// Networks +// +// Created by 류희재 on 10/14/24. +// Copyright © 2024 HMH-iOS. All rights reserved. +// + +import Foundation +import Combine + +class RequestHandler { + + static let shared = RequestHandler() + + private init() {} + + private lazy var session: URLSession = { + let configuration = URLSessionConfiguration.default + configuration.timeoutIntervalForRequest = 10 + configuration.timeoutIntervalForResource = 10 + // TODO: Interceptor 추가 + return URLSession(configuration: configuration) + }() + + func executeRequest(for target: T) -> AnyPublisher { + return target.asURLRequest() + .map { $0 } + .mapError { ErrorHandler.handleError(target, error: .invalidRequest($0)) } + .flatMap { urlRequest in + self.session.dataTaskPublisher(for: urlRequest) + .tryMap { data, response -> NetworkResponse in + guard let httpResponse = response as? HTTPURLResponse else { + throw HMHNetworkError.ResponseError.unhandled + } + return NetworkResponse(data: data, response: httpResponse, error: nil) + } + .mapError { error -> HMHNetworkError in + if let requestErr = error as? HMHNetworkError.ResponseError { + return .invalidResponse(requestErr) + } else { + return .unknown(error) + } + } + .eraseToAnyPublisher() + } + + .eraseToAnyPublisher() + } +} + + diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Models/GenericResponse.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Models/GenericResponse.swift new file mode 100644 index 00000000..8d69ad72 --- /dev/null +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Models/GenericResponse.swift @@ -0,0 +1,51 @@ +// +// GenericResponse.swift +// Networks +// +// Created by 류희재 on 10/14/24. +// Copyright © 2024 HMH-iOS. All rights reserved. +// + +import Foundation + +struct GenericResponse: Decodable { + var statusCode: Int + var message: String + var data: T? + + enum CodingKeys: CodingKey { + case statusCode + case message + case data + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.statusCode = (try? container.decode(Int.self, forKey: .statusCode)) ?? 500 + self.message = (try? container.decode(String.self, forKey: .message)) ?? "" + self.data = try container.decodeIfPresent(T.self, forKey: .data) + } +} + +struct VoidResult: Decodable { + +} + +struct ErrorResponse: Decodable { + var statusCode: Int + var message: String + var data: String? + + enum CodingKeys: CodingKey { + case statusCode + case message + case data + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.statusCode = (try? container.decode(Int.self, forKey: .statusCode)) ?? 500 + self.message = (try? container.decode(String.self, forKey: .message)) ?? "" + self.data = try container.decodeIfPresent(String.self, forKey: .data) + } +} diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Models/NetworkResponse.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Models/NetworkResponse.swift new file mode 100644 index 00000000..7d6437c7 --- /dev/null +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/Models/NetworkResponse.swift @@ -0,0 +1,21 @@ +// +// NetworkResponse.swift +// Networks +// +// Created by 류희재 on 10/14/24. +// Copyright © 2024 HMH-iOS. All rights reserved. +// + +import Foundation + +public struct NetworkResponse { + public let data: Data? + public let response: HTTPURLResponse + public let error: Error? + + public init(data: Data?, response: HTTPURLResponse, error: Error?) { + self.data = data + self.response = response + self.error = error + } +} diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Foundation/APIConstants.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/APIHeaders.swift similarity index 94% rename from HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Foundation/APIConstants.swift rename to HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/APIHeaders.swift index 332aa9ef..5b31386e 100644 --- a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Foundation/APIConstants.swift +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/APIHeaders.swift @@ -1,5 +1,5 @@ // -// APIConstants.swift +// APIHeaders.swift // HMH_iOS // // Created by 지희의 MAC on 1/11/24. @@ -10,7 +10,7 @@ import Moya import Core -public struct APIConstants { +public struct APIHeaders { static let contentType = "Content-Type" static let applicationJSON = "application/json" static let auth = "Authorization" @@ -31,7 +31,7 @@ public struct APIConstants { static let iOS = "iOS" } -public extension APIConstants { +public extension APIHeaders { static var hasSocialTokenHeader: [String: String] { return [contentType: applicationJSON, auth: appleAccessToken] diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/HTTPMethod.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/HTTPMethod.swift new file mode 100644 index 00000000..191f5b24 --- /dev/null +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/HTTPMethod.swift @@ -0,0 +1,20 @@ +// +// HTTPMethods.swift +// Networks +// +// Created by 류희재 on 10/14/24. +// Copyright © 2024 HMH-iOS. All rights reserved. +// + +import Foundation + +public typealias Parameters = [String: Any] + +@frozen public enum HTTPMethod: String { + case get = "GET" + case head = "HEAD" + case post = "POST" + case put = "PUT" + case patch = "PATCH" + case delete = "DELETE" +} diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/ParameterEncoding.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/ParameterEncoding.swift new file mode 100644 index 00000000..b3120f7f --- /dev/null +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/ParameterEncoding.swift @@ -0,0 +1,77 @@ +// +// ParameterEncoding.swift +// Networks +// +// Created by 류희재 on 10/14/24. +// Copyright © 2024 HMH-iOS. All rights reserved. +// + +import Foundation +import Combine + +protocol ParameterEncodable {} + +extension ParameterEncodable { + func checkValidURLData( + _ parameters: Parameters?, + _ url: URL? + ) -> AnyPublisher<(Parameters, URL), HMHNetworkError.ParameterEncoding> { + guard let parameters else { return Fail(error: .emptyParameters).eraseToAnyPublisher() } + guard let url else { return Fail(error: .missingURL).eraseToAnyPublisher() } + + return Just((parameters, url)) + .setFailureType(to: HMHNetworkError.ParameterEncoding.self) + .eraseToAnyPublisher() + } + + func checkValidURLData( + _ parameters: Encodable?, + _ url: URL? + ) -> AnyPublisher<(Encodable, URL), HMHNetworkError.ParameterEncoding> { + guard let parameters else { return Fail(error: .emptyParameters).eraseToAnyPublisher() } + guard let url else { return Fail(error: .missingURL).eraseToAnyPublisher() } + + return Just((parameters, url)) + .setFailureType(to: HMHNetworkError.ParameterEncoding.self) + .eraseToAnyPublisher() + } +} + +public struct URLEncoding: ParameterEncodable { + func encode(_ request: URLRequest, with parameters: Parameters?) -> AnyPublisher { + var request = request + + return checkValidURLData(parameters, request.url) + .map { parameters, url -> URLRequest in + if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) { + urlComponents.queryItems = parameters.compactMap { key, value in + URLQueryItem(name: key, value: "\(value)") + } + request.url = urlComponents.url + } + return request + } + .mapError { $0 } + .eraseToAnyPublisher() + } +} + + +public struct JSONEncoding: ParameterEncodable { + func encode(_ request: URLRequest, with parameters: Encodable?) -> AnyPublisher { + var request = request + return checkValidURLData(parameters, request.url) + .tryMap { parameters, _ -> URLRequest in + do { + let data = try JSONEncoder().encode(parameters) + request.httpBody = data + return request + } catch { + throw HMHNetworkError.invalidRequest(.parameterEncodingFailed(.jsonEncodingFailed)) + } + } + .mapError { $0 as! HMHNetworkError.ParameterEncoding } //TODO: 예외 상황이 없는거 같아서.. + .eraseToAnyPublisher() + } +} + diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/Task.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/Task.swift new file mode 100644 index 00000000..555da10e --- /dev/null +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/Task.swift @@ -0,0 +1,41 @@ +// +// Task.swift +// Networks +// +// Created by 류희재 on 10/14/24. +// Copyright © 2024 HMH-iOS. All rights reserved. +// + +import Foundation +import Combine + +enum Task { + case requestPlain + case requestParameters(Parameters) + case requestJSONEncodable(Encodable) +} + +extension Task { + func buildRequest(baseURL: URL, method: HTTPMethod, headers: [String: String]?) -> AnyPublisher { + var request = URLRequest(url: baseURL) + request.httpMethod = method.rawValue + request.allHTTPHeaderFields = headers + + switch self { + case .requestPlain: + return Just(request) + .setFailureType(to: HMHNetworkError.RequestError.self) + .eraseToAnyPublisher() + + case .requestParameters(let parameters): + return URLEncoding().encode(request, with: parameters) + .mapError { .parameterEncodingFailed($0) } + .eraseToAnyPublisher() + + case .requestJSONEncodable(let encodable): + return JSONEncoding().encode(request, with: encodable) + .mapError { .parameterEncodingFailed($0) } + .eraseToAnyPublisher() + } + } +} diff --git a/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/URLRequestTargetType.swift b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/URLRequestTargetType.swift new file mode 100644 index 00000000..a86c0b21 --- /dev/null +++ b/HMH_Tuist_iOS/Projects/Modules/Networks/Sources/Refactor/Foundation/RequestBuilder/URLRequestTargetType.swift @@ -0,0 +1,34 @@ +// +// URLRequestTargetType.swift +// Networks +// +// Created by 류희재 on 10/14/24. +// Copyright © 2024 HMH-iOS. All rights reserved. +// + +import Foundation +import Combine + +protocol URLRequestTargetType { + var url: String { get } + var path: String? { get } + var method: HTTPMethod { get } + var headers : [String : String]? { get } + var task: Task { get } + + func asURLRequest() -> AnyPublisher +} + +extension URLRequestTargetType { + func asURLRequest() -> AnyPublisher { + guard let url = URL(string: self.url) else { + return Fail(error: .invalidURL(self.url)).eraseToAnyPublisher() + } + + var baseURL = url + if let path = self.path { baseURL.appendPathComponent(path) } + + return task.buildRequest(baseURL: baseURL, method: self.method, headers: self.headers) + } +} +