Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#84] Concurrency를 사용하여 앱에서 사용하는 네트워크 공통 로직 구현 #85

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
40 changes: 40 additions & 0 deletions FogFog-iOS/FogFog-iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@
1DDE6DAB87E9F15EAEAD8A1B /* Pods_FogFog_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 44BDDCB91FAFFFC51A0C1A4C /* Pods_FogFog_iOS.framework */; };
35919B344F294722AA448E4E /* Pods_FogFog_iOS_FogFog_iOSUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D67B489E657987D4A7AD92C7 /* Pods_FogFog_iOS_FogFog_iOSUITests.framework */; };
56E6DAD19F1058E2F6B779B7 /* Pods_FogFog_iOSTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 848DD0928406FC10714344E4 /* Pods_FogFog_iOSTests.framework */; };
6D69A85A2D364949007B6538 /* ModernHTTPTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D69A8592D364943007B6538 /* ModernHTTPTask.swift */; };
6DCD47AD2D2E0E2E009FD6DF /* ModernHttpMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DCD47AC2D2E0E18009FD6DF /* ModernHttpMethod.swift */; };
6DCD47AF2D2E0E5F009FD6DF /* ModernNetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DCD47AE2D2E0E59009FD6DF /* ModernNetworkError.swift */; };
6DCD47B72D2E1432009FD6DF /* ModernEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DCD47B62D2E142E009FD6DF /* ModernEndpoint.swift */; };
6DCD47B92D2E153C009FD6DF /* ModernURLRequestBuildable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DCD47B82D2E152C009FD6DF /* ModernURLRequestBuildable.swift */; };
6DCD47C22D2E42D8009FD6DF /* ModernNetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DCD47C12D2E42CB009FD6DF /* ModernNetworkSession.swift */; };
6DCD47C42D2E4340009FD6DF /* JSONResponseDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DCD47C32D2E432F009FD6DF /* JSONResponseDecoder.swift */; };
6DCD47C62D2E4366009FD6DF /* ModernNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DCD47C52D2E435E009FD6DF /* ModernNetworkService.swift */; };
77148CCA2AC69DB8008E24B2 /* NotificationCenterKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77148CC92AC69DB8008E24B2 /* NotificationCenterKey.swift */; };
77148CDB2ADEB67E008E24B2 /* UserDefaultsKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77148CDA2ADEB67E008E24B2 /* UserDefaultsKey.swift */; };
77148CDF2AE50008008E24B2 /* UserDefaults + Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77148CDE2AE50008008E24B2 /* UserDefaults + Extension.swift */; };
Expand Down Expand Up @@ -170,6 +178,14 @@
44BDDCB91FAFFFC51A0C1A4C /* Pods_FogFog_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FogFog_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
47D49EE273F775CA2202B593 /* Pods-FogFog-iOS-FogFog-iOSUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FogFog-iOS-FogFog-iOSUITests.debug.xcconfig"; path = "Target Support Files/Pods-FogFog-iOS-FogFog-iOSUITests/Pods-FogFog-iOS-FogFog-iOSUITests.debug.xcconfig"; sourceTree = "<group>"; };
568E5A05A08D8D049D3D8E62 /* Pods-FogFog-iOSTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FogFog-iOSTests.release.xcconfig"; path = "Target Support Files/Pods-FogFog-iOSTests/Pods-FogFog-iOSTests.release.xcconfig"; sourceTree = "<group>"; };
6D69A8592D364943007B6538 /* ModernHTTPTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModernHTTPTask.swift; sourceTree = "<group>"; };
6DCD47AC2D2E0E18009FD6DF /* ModernHttpMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModernHttpMethod.swift; sourceTree = "<group>"; };
6DCD47AE2D2E0E59009FD6DF /* ModernNetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModernNetworkError.swift; sourceTree = "<group>"; };
6DCD47B62D2E142E009FD6DF /* ModernEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModernEndpoint.swift; sourceTree = "<group>"; };
6DCD47B82D2E152C009FD6DF /* ModernURLRequestBuildable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModernURLRequestBuildable.swift; sourceTree = "<group>"; };
6DCD47C12D2E42CB009FD6DF /* ModernNetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModernNetworkSession.swift; sourceTree = "<group>"; };
6DCD47C32D2E432F009FD6DF /* JSONResponseDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONResponseDecoder.swift; sourceTree = "<group>"; };
6DCD47C52D2E435E009FD6DF /* ModernNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModernNetworkService.swift; sourceTree = "<group>"; };
77148CC92AC69DB8008E24B2 /* NotificationCenterKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenterKey.swift; sourceTree = "<group>"; };
77148CDA2ADEB67E008E24B2 /* UserDefaultsKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsKey.swift; sourceTree = "<group>"; };
77148CDE2AE50008008E24B2 /* UserDefaults + Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults + Extension.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -371,6 +387,21 @@
path = Pods;
sourceTree = "<group>";
};
6DCD47372D2D0B9E009FD6DF /* ModernNetwork */ = {
isa = PBXGroup;
children = (
6DCD47AC2D2E0E18009FD6DF /* ModernHttpMethod.swift */,
6DCD47AE2D2E0E59009FD6DF /* ModernNetworkError.swift */,
6DCD47B62D2E142E009FD6DF /* ModernEndpoint.swift */,
6D69A8592D364943007B6538 /* ModernHTTPTask.swift */,
6DCD47B82D2E152C009FD6DF /* ModernURLRequestBuildable.swift */,
6DCD47C12D2E42CB009FD6DF /* ModernNetworkSession.swift */,
6DCD47C32D2E432F009FD6DF /* JSONResponseDecoder.swift */,
6DCD47C52D2E435E009FD6DF /* ModernNetworkService.swift */,
);
path = ModernNetwork;
sourceTree = "<group>";
};
775AC1952A735FCC00A2A47E /* Class */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -700,6 +731,7 @@
ECA4252B29224DB8004DFDAF /* FogFog-iOS */ = {
isa = PBXGroup;
children = (
6DCD47372D2D0B9E009FD6DF /* ModernNetwork */,
BD9AA41529929A7A00D4E7CE /* FogFog-iOS.entitlements */,
ECA4255D292251D8004DFDAF /* App */,
BD9AA41629929AB200D4E7CE /* Manager */,
Expand Down Expand Up @@ -1337,6 +1369,7 @@
BD4D62AF2A1A100B00532778 /* KakaoOAuthService.swift in Sources */,
77E70F4A2930D0FA00F55CD7 /* MakeNicknameViewModel.swift in Sources */,
77650210295462D7002BF7AD /* UIViewController + Extension.swift in Sources */,
6DCD47B72D2E1432009FD6DF /* ModernEndpoint.swift in Sources */,
BDDAA3952A24BA130055859E /* OAuthAuthentication.swift in Sources */,
7765020E29546239002BF7AD /* SettingListTableViewCell.swift in Sources */,
BDB6E1FA2A30546D00AE6228 /* Published+Rx.swift in Sources */,
Expand All @@ -1352,9 +1385,11 @@
BD834A622A8A073300A6DCBD /* ExternalMapModalViewModel.swift in Sources */,
BDF434672AB1887C00FFAA45 /* MapAPIService.swift in Sources */,
ECA425E629262A42004DFDAF /* DefaultLoginCoordinator.swift in Sources */,
6DCD47C22D2E42D8009FD6DF /* ModernNetworkSession.swift in Sources */,
BD4EA69E2AB19A73004CE0BB /* QuitAPIService.swift in Sources */,
ECA425DC29253259004DFDAF /* CoordinatorFinishDelegate.swift in Sources */,
ECA4252D29224DB8004DFDAF /* AppDelegate.swift in Sources */,
6DCD47C62D2E4366009FD6DF /* ModernNetworkService.swift in Sources */,
ECA425EA29262A80004DFDAF /* LoginCoordinator.swift in Sources */,
ECA425DE29253292004DFDAF /* CoordinatorCase.swift in Sources */,
BDF4346E2AB1900200FFAA45 /* SmokingAreaEntity.swift in Sources */,
Expand All @@ -1364,6 +1399,7 @@
BD4B550F292A29EC00ECFD04 /* SmokingAreaResponseModel.swift in Sources */,
ECA425F6292634C9004DFDAF /* LoginViewModel.swift in Sources */,
EC1562312A6D7D8200163875 /* LocationService.swift in Sources */,
6DCD47AF2D2E0E5F009FD6DF /* ModernNetworkError.swift in Sources */,
779672AF2A58034E003064A8 /* ASAuthorizationControllerProxy.swift in Sources */,
BD90E9E22976C4F800C7CB6B /* Networking.swift in Sources */,
BDE3EEB22A1D124300632662 /* OAuthService.swift in Sources */,
Expand All @@ -1383,9 +1419,11 @@
77F1D82229269CC800F31D0C /* UIColor + Extension.swift in Sources */,
77F1D82429269E1C00F31D0C /* UIView + Extension.swift in Sources */,
BD4E13652973DAC400AEA757 /* NetworkError.swift in Sources */,
6DCD47AD2D2E0E2E009FD6DF /* ModernHttpMethod.swift in Sources */,
77E70F462930951100F55CD7 /* UITextField + Extension.swift in Sources */,
BDC50ED6292A09BD002378C6 /* BaseView.swift in Sources */,
BD90E9DE2976C11E00C7CB6B /* FogAPI.swift in Sources */,
6D69A85A2D364949007B6538 /* ModernHTTPTask.swift in Sources */,
BD5D0D7029F12E0600190471 /* MessageModel.swift in Sources */,
BD347C5E292FD2BE009F52BA /* FogButtonStyle.swift in Sources */,
ECA4259B2922A4A6004DFDAF /* FogImage.swift in Sources */,
Expand All @@ -1395,9 +1433,11 @@
77D32D41292A0EDD006E6314 /* ViewModelType.swift in Sources */,
BD2D276B297D17E400984B97 /* UserAPI.swift in Sources */,
BD5D0D7F29F15E3A00190471 /* BottomViewType.swift in Sources */,
6DCD47B92D2E153C009FD6DF /* ModernURLRequestBuildable.swift in Sources */,
ECA425E2292532CA004DFDAF /* DefaultAppCoordinator.swift in Sources */,
ECA4252F29224DB8004DFDAF /* SceneDelegate.swift in Sources */,
BDB6E1F72A30511C00AE6228 /* NetworkConnectionView.swift in Sources */,
6DCD47C42D2E4340009FD6DF /* JSONResponseDecoder.swift in Sources */,
776502182954A3BA002BF7AD /* SettingTitleTableViewCell.swift in Sources */,
ECA425EC29262CF8004DFDAF /* MapViewModel.swift in Sources */,
7761D4F4292F3471003971DC /* MakeNicknameViewController.swift in Sources */,
Expand Down
28 changes: 28 additions & 0 deletions FogFog-iOS/FogFog-iOS/ModernNetwork/JSONResponseDecoder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// JSONResponseDecoder.swift
// FogFog-iOS
//
// Created by MEGA_Mac on 1/8/25.
//

import Foundation

/// 데이터 디코딩을 위한 프로토콜
/// - Note: 데이터 디코딩 추상화
protocol ResponseDecoding {
func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T
}

/// JSON 데이터 디코딩을 담당하는 구조체
/// - Note: ResponseDecoding 프로토콜 기본 구현체
struct JSONResponseDecoder: ResponseDecoding {
private let decoder: JSONDecoder

init(decoder: JSONDecoder = JSONDecoder()) {
self.decoder = decoder
}

func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {
try decoder.decode(type, from: data)
}
}
15 changes: 15 additions & 0 deletions FogFog-iOS/FogFog-iOS/ModernNetwork/ModernEndpoint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// ModernEndpoint.swift
// FogFog-iOS
//
// Created by MEGA_Mac on 1/8/25.
//

import Foundation

protocol ModernEndpoint {
var path: String { get }
var method: ModernHttpMethod { get }
var task: ModernHTTPTask { get }
var headers: [String: String]? { get }
}
15 changes: 15 additions & 0 deletions FogFog-iOS/FogFog-iOS/ModernNetwork/ModernHTTPTask.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// ModernHTTPTask.swift
// FogFog-iOS
//
// Created by MEGA_Mac on 1/14/25.
//

import Foundation

enum ModernHTTPTask {
case query(_ query: [String: Any])
case queryBody(_ query: [String: Any], _ body: [String: Any])
case requestBody(_ body: [String: Any])
case requestPlain
}
15 changes: 15 additions & 0 deletions FogFog-iOS/FogFog-iOS/ModernNetwork/ModernHttpMethod.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// ModernHttpMethod.swift
// FogFog-iOS
//
// Created by MEGA_Mac on 1/8/25.
//

import Foundation

enum ModernHttpMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
57 changes: 57 additions & 0 deletions FogFog-iOS/FogFog-iOS/ModernNetwork/ModernHttpStatusCode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// ModernHttpStatusCode.swift
// FogFog-iOS
//
// Created by MEGA_Mac on 1/8/25.
//

/// ModernNetworkError의 HTTP 상태 코드를 정의하고 관리
/// - Note: 서버 응답의 상태 코드에 따른 NetworkError 생성
/// - Author: seungchan
public enum ModernHTTPStatusCode: Int {
/// 400: 잘못된 요청 (Bad Request)
/// - 클라이언트의 요청이 유효하지 않은 경우
case invalidRequest = 400

/// 401: 인증 실패 (Unauthorized)
/// - 소셜 로그인 토큰이 없거나 유효하지 않은 경우
case unauthorized = 401

/// 403: 접근 권한 없음 (Forbidden)
/// - 요청 id와 accessToken 정보가 매칭되지 않는 경우
case forbidden = 403

/// 404: 리소스를 찾을 수 없음 (Not Found)
/// - 유효한 유저 정보가 없는 경우
case notFound = 404

/// 409: 리소스 충돌 (Conflict)
/// - 중복된 닉네임일 경우
case duplicated = 409

/// 500: 서버 내부 오류 (Internal Server Error)
/// - 서버에서 처리 중 예기치 않은 오류가 발생한 경우
case serverError = 500

/// HTTP 상태 코드에 해당하는 NetworkError를 생성
/// - Parameter data: 서버로부터 받은 에러 응답 데이터
/// - Returns: 상태 코드와 에러 메시지를 포함한 NetworkError
public func createError(with data: Data) -> NetworkError {
let message = getErrorMessage(from: data)
switch self {
case .invalidRequest: return .invalidRequest(message: message)
case .unauthorized: return .unauthorized(message: message)
case .forbidden: return .forbidden(message: message)
case .notFound: return .notFound(message: message)
case .duplicated: return .duplicated(message: message)
case .serverError: return .serverError(message: message)
}
}

/// 서버 응답 데이터에서 에러 메시지를 추출
/// - Parameter data: 서버로부터 받은 에러 응답 데이터
/// - Returns: 에러 메시지 문자열. 디코딩 실패 시 nil 반환
private func getErrorMessage(from data: Data) -> String? {
try? JSONDecoder().decode(ErrorResponse.self, from: data).message
}
}
93 changes: 93 additions & 0 deletions FogFog-iOS/FogFog-iOS/ModernNetwork/ModernNetworkError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//
// ModernNetworkError.swift
// FogFog-iOS
//
// Created by MEGA_Mac on 1/8/25.
//

import Foundation

/// 네트워크 작업 중 발생할 수 있는 에러를 정의하는 열거형
/// - Note: HTTP 상태 코드 및 클라이언트 에러 처리
public enum ModernNetworkError: Error {
/// - Parameters:
/// - statusCode: HTTP 상태 코드
/// - message: 서버에서 전달된 에러 메시지
case http(statusCode: Int, message: String?)

/// 잘못된 URL 형식
case invalidURL
/// JSON 디코딩 실패
case decodingError(Error)
/// 요청 시간 초과
case timeout
/// 인터넷 연결 없음
case noInternetConnection

struct ErrorResponse: Decodable {
let message: String
}
}

public extension ModernNetworkError {
/// 400 Bad Request 에러 생성
func invalidRequest(message: String? = nil) -> Self {
.http(statusCode: 400, message: message)
}

/// 401 Unauthorized 에러 생성
func unauthorized(message: String? = nil) -> Self {
.http(statusCode: 401, message: message)
}

/// 403 Forbidden 에러 생성
func forbidden(message: String? = nil) -> Self {
.http(statusCode: 403, message: message)
}

/// 404 Not Found 에러 생성
func notFound(message: String? = nil) -> Self {
.http(statusCode: 404, message: message)
}

/// 409 Conflict 에러 생성
func duplicated(message: String? = nil) -> Self {
.http(statusCode: 409, message: message)
}

/// 500 Internal Server Error 생성
func serverError(message: String? = nil) -> Self {
.http(statusCode: 500, message: message)
}

/// HTTP 상태 코드와 응답 데이터로부터 에러 생성
/// - Parameters:
/// - statusCode: HTTP 상태 코드
/// - data: 서버 응답 데이터
func from(statusCode: Int, data: Data) -> Self {
let message = try? JSONDecoder()
.decode(ErrorResponse.self, from: data)
.message
return .http(statusCode: statusCode, message: message)
}

/// 에러 요약
var errorDescription: String {
switch self {
case .http(let statusCode, let message):
switch statusCode {
case 400: return message ?? "잘못된 요청입니다."
case 401: return message ?? "소셜 로그인 토큰이 유효하지 않습니다."
case 403: return message ?? "해당 요청에 대한 권한이 없습니다."
case 404: return message ?? "유효한 유저 정보가 없습니다."
case 409: return message ?? "중복된 닉네임입니다."
case 500: return message ?? "서버 내부 오류가 발생했습니다."
default: return message ?? "알 수 없는 오류가 발생했습니다. (Status: \(statusCode))"
}
case .invalidURL: return "유효하지 않은 URL입니다."
case .decodingError(let error): return "데이터 디코딩 실패: \(error.localizedDescription)"
case .timeout: return "요청 시간이 초과되었습니다."
case .noInternetConnection: return "인터넷 연결을 확인해주세요."
}
}
}
Loading