From 4919908816353bd36be0bac479b8f410686fa216 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 16 Sep 2025 20:56:41 +0900 Subject: [PATCH 01/18] =?UTF-8?q?=E2=9C=A8[feat]:=20=20API=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EA=B4=80=EB=A0=A8=20=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Model/Error/DataError.swift | 0 AppleStoreProductKiosk/Network/API/BaseAPI.swift | 0 .../Network/API/KioskProductAPI.swift | 8 ++++++++ .../Network/API/KioskProductDomain.swift | 0 .../Common/NetworkManger/Data/Extension+Data.swift | 0 .../NetworkManger/Logger/URLSessionLogger.swift | 0 .../Common/NetworkManger/Method/HTTPMethod.swift | 0 .../Common/NetworkManger/Provider/AsyncProvider.swift | 0 .../NetworkManger/TargeType/BaseTargetType.swift | 0 .../TargeType/CustomParameterEncoding.swift | 0 .../Common/NetworkManger/TargeType/NetworkTask.swift | 0 .../Common/NetworkManger/TargeType/TargetType.swift | 0 .../URLRequestBuilder/URLRequestBuilder.swift | 7 +++++++ .../Common/NetworkManger/header/APIHeader.swift | 8 ++++++++ .../Common/NetworkManger/header/APIHeaderManger.swift | 0 .../Network/Service/KioskProductService.swift | 0 AppleStoreProductKiosk/Resources/Info.plist | 11 +++++++++++ 17 files changed, 34 insertions(+) create mode 100644 AppleStoreProductKiosk/Data/Model/Error/DataError.swift create mode 100644 AppleStoreProductKiosk/Network/API/BaseAPI.swift create mode 100644 AppleStoreProductKiosk/Network/API/KioskProductAPI.swift create mode 100644 AppleStoreProductKiosk/Network/API/KioskProductDomain.swift create mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/Data/Extension+Data.swift create mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/Logger/URLSessionLogger.swift create mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/Method/HTTPMethod.swift create mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/Provider/AsyncProvider.swift create mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/BaseTargetType.swift create mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/CustomParameterEncoding.swift create mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/NetworkTask.swift create mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/TargetType.swift create mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/URLRequestBuilder/URLRequestBuilder.swift create mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeader.swift create mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeaderManger.swift create mode 100644 AppleStoreProductKiosk/Network/Service/KioskProductService.swift create mode 100644 AppleStoreProductKiosk/Resources/Info.plist diff --git a/AppleStoreProductKiosk/Data/Model/Error/DataError.swift b/AppleStoreProductKiosk/Data/Model/Error/DataError.swift new file mode 100644 index 0000000..e69de29 diff --git a/AppleStoreProductKiosk/Network/API/BaseAPI.swift b/AppleStoreProductKiosk/Network/API/BaseAPI.swift new file mode 100644 index 0000000..e69de29 diff --git a/AppleStoreProductKiosk/Network/API/KioskProductAPI.swift b/AppleStoreProductKiosk/Network/API/KioskProductAPI.swift new file mode 100644 index 0000000..9974053 --- /dev/null +++ b/AppleStoreProductKiosk/Network/API/KioskProductAPI.swift @@ -0,0 +1,8 @@ +// +// kioskproductapi.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation diff --git a/AppleStoreProductKiosk/Network/API/KioskProductDomain.swift b/AppleStoreProductKiosk/Network/API/KioskProductDomain.swift new file mode 100644 index 0000000..e69de29 diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/Data/Extension+Data.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/Data/Extension+Data.swift new file mode 100644 index 0000000..e69de29 diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/Logger/URLSessionLogger.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/Logger/URLSessionLogger.swift new file mode 100644 index 0000000..e69de29 diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/Method/HTTPMethod.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/Method/HTTPMethod.swift new file mode 100644 index 0000000..e69de29 diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/Provider/AsyncProvider.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/Provider/AsyncProvider.swift new file mode 100644 index 0000000..e69de29 diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/BaseTargetType.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/BaseTargetType.swift new file mode 100644 index 0000000..e69de29 diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/CustomParameterEncoding.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/CustomParameterEncoding.swift new file mode 100644 index 0000000..e69de29 diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/NetworkTask.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/NetworkTask.swift new file mode 100644 index 0000000..e69de29 diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/TargetType.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/TargetType.swift new file mode 100644 index 0000000..e69de29 diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/URLRequestBuilder/URLRequestBuilder.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/URLRequestBuilder/URLRequestBuilder.swift new file mode 100644 index 0000000..b8b46c4 --- /dev/null +++ b/AppleStoreProductKiosk/Network/Common/NetworkManger/URLRequestBuilder/URLRequestBuilder.swift @@ -0,0 +1,7 @@ +// +// Untitled.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeader.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeader.swift new file mode 100644 index 0000000..941d1d5 --- /dev/null +++ b/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeader.swift @@ -0,0 +1,8 @@ +// +// APIHeader.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeaderManger.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeaderManger.swift new file mode 100644 index 0000000..e69de29 diff --git a/AppleStoreProductKiosk/Network/Service/KioskProductService.swift b/AppleStoreProductKiosk/Network/Service/KioskProductService.swift new file mode 100644 index 0000000..e69de29 diff --git a/AppleStoreProductKiosk/Resources/Info.plist b/AppleStoreProductKiosk/Resources/Info.plist new file mode 100644 index 0000000..6a6654d --- /dev/null +++ b/AppleStoreProductKiosk/Resources/Info.plist @@ -0,0 +1,11 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + From c7a87625d2525d99ec7d738ded87a109c6d44a66 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 16 Sep 2025 20:56:58 +0900 Subject: [PATCH 02/18] =?UTF-8?q?=E2=9C=A8[feat]:=20=20networkmange=20=20d?= =?UTF-8?q?ata=20=EB=94=94=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NetworkManger/Data/Extension+Data.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/Data/Extension+Data.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/Data/Extension+Data.swift index e69de29..0b3d1ab 100644 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/Data/Extension+Data.swift +++ b/AppleStoreProductKiosk/Network/Common/NetworkManger/Data/Extension+Data.swift @@ -0,0 +1,21 @@ +// +// Extension+Data.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +/// Data 타입에 대한 JSON 디코딩 확장입니다. +extension Data { + /// Data를 지정한 Decodable 타입으로 디코딩합니다. + /// + /// - Parameter type: 디코딩할 타입 + /// - Returns: 디코딩된 객체 + /// - Throws: 디코딩 실패 시 에러 발생 + func decoded(as type: T.Type) throws -> T { + let decoder = JSONDecoder() + return try decoder.decode(T.self, from: self) + } +} From 1c7ab835b3d79e22a3b2852c82635be3949878d1 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 16 Sep 2025 20:57:13 +0900 Subject: [PATCH 03/18] =?UTF-8?q?=E2=9C=A8[feat]:=20=20=ED=86=B5=EC=8B=A0?= =?UTF-8?q?=20=EB=A1=9C=EA=B1=B0=20=EA=B5=AC=ED=98=84=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Logger/URLSessionLogger.swift | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/Logger/URLSessionLogger.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/Logger/URLSessionLogger.swift index e69de29..f1f8614 100644 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/Logger/URLSessionLogger.swift +++ b/AppleStoreProductKiosk/Network/Common/NetworkManger/Logger/URLSessionLogger.swift @@ -0,0 +1,113 @@ +// +// URLSessionLogger.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation +import LogMacro + +/// URLSession 네트워크 요청/응답을 콘솔에 로그로 출력하는 싱글턴 유틸리티입니다. +/// +/// - DEBUG 모드에서만 동작하며, 요청/응답의 메서드, URL, 헤더, 바디, 에러 등을 보기 좋게 출력합니다. +final class URLSessionLogger { + /// 싱글턴 인스턴스 + static let shared = URLSessionLogger() + + /// 외부에서 직접 생성하지 못하도록 private init + private init() {} + + // MARK: - 요청 로그 + + /// 네트워크 요청 정보를 콘솔에 출력합니다. + /// - Parameter request: 로깅할 URLRequest + @MainActor + func logRequest(_ request: URLRequest) { +#if DEBUG + Log.debug("⎡--------------------- REQUEST ---------------------⎤") + + // HTTP 메서드 + if let method = request.httpMethod { + Log.network("[Method]", method) + } + + // 요청 URL + if let url = request.url?.absoluteString { + Log.network("[URL]", url) + } + + // 헤더 + if let headers = request.allHTTPHeaderFields { + Log.network("[Headers]") + for (key, value) in headers { + Log.network(" \(key): \(value)") + } + } + + // 바디 + if let body = request.httpBody, + let bodyString = String(data: body, encoding: .utf8) { + Log.network("[Body]") + Log.network(" \(bodyString)") + } + + Log.network("⎣------------------ END REQUEST --------------------⎦") +#endif + } + + // MARK: - 응답 로그 + + /// 네트워크 응답 정보를 콘솔에 출력합니다. + /// - Parameters: + /// - data: 응답 데이터 + /// - response: URLResponse 객체 + /// - error: 네트워크 에러 + @MainActor + func logResponse(data: Data?, response: URLResponse?, error: Error?) { +#if DEBUG + Log.network("⎡--------------------- RESPONSE --------------------⎤") + + // 응답 URL + if let url = response?.url?.absoluteString { + Log.network("[URL]", url) + } + + // HTTP 상태 코드 및 헤더 + if let httpResponse = response as? HTTPURLResponse { + Log.network("[Status Code] \(httpResponse.statusCode)") + Log.network("[Response Headers]") + for (key, value) in httpResponse.allHeaderFields { + Log.network(" \(key): \(value)") + } + } + + // 바디 (JSON pretty print 시도, 실패 시 raw 출력) + if let data = data { + do { + let json = try JSONSerialization.jsonObject(with: data, options: []) + let prettyData = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted]) + if let prettyString = String(data: prettyData, encoding: .utf8) { + Log.network("[Body (Map)]") + Log.network(prettyString) + } + } catch { + // JSON 디코딩 실패 시 원본 문자열 출력 + if let rawString = String(data: data, encoding: .utf8) { + Log.network("[Body (Raw)]") + Log.network(rawString) + } else { + Log.network("[Body] Cannot decode data") + } + } + } + + // 에러 + if let error = error { + Log.network("[Error] \(error.localizedDescription)") + } + + Log.network("⎣------------------ END RESPONSE -------------------⎦") +#endif + } +} From 8f422079d720fa78759d64cdeb7f339eb16b7d64 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 16 Sep 2025 20:57:31 +0900 Subject: [PATCH 04/18] =?UTF-8?q?=E2=9C=A8[feat]:=20=20mettod=20type?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NetworkManger/Method/HTTPMethod.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/Method/HTTPMethod.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/Method/HTTPMethod.swift index e69de29..e023d44 100644 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/Method/HTTPMethod.swift +++ b/AppleStoreProductKiosk/Network/Common/NetworkManger/Method/HTTPMethod.swift @@ -0,0 +1,21 @@ +// +// HTTPMethod.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +/// HTTP 요청에서 사용되는 메서드 타입을 정의한 enum입니다. +enum HTTPMethod: String { + /// GET: 데이터 조회 + case get = "GET" + /// POST: 데이터 생성 + case post = "POST" + /// PUT: 데이터 전체 수정 + case put = "PUT" + /// DELETE: 데이터 삭제 + case delete = "DELETE" +} + From 40398725c4a35c439895551aa0ab43852f441950 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 16 Sep 2025 20:58:47 +0900 Subject: [PATCH 05/18] =?UTF-8?q?=E2=9C=A8[feat]:=20=20TargeType=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TargeType/BaseTargetType.swift | 59 +++++++++++++++++++ .../TargeType/CustomParameterEncoding.swift | 48 +++++++++++++++ .../NetworkManger/TargeType/NetworkTask.swift | 28 +++++++++ .../NetworkManger/TargeType/TargetType.swift | 30 ++++++++++ 4 files changed, 165 insertions(+) diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/BaseTargetType.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/BaseTargetType.swift index e69de29..4df56b6 100644 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/BaseTargetType.swift +++ b/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/BaseTargetType.swift @@ -0,0 +1,59 @@ +// +// BaseTargetType.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +/// API 타겟의 공통 속성을 정의하는 프로토콜입니다. +/// +/// - 도메인, URL 경로, 파라미터 등 API 요청에 필요한 정보를 제공합니다. +protocol BaseTargetType: TargetType { + /// 도메인 구분값 + var domain: KioskProductDomain { get } + /// API URL 경로 + var urlPath: String { get } + /// 요청 파라미터 + var parameters:[String: Any]? { get } +} + +// MARK: - BaseTargetType 기본 구현 + + +extension BaseTargetType { + /// API의 baseURL + var baseURL: URL { + return URL(string: BaseAPI.baseURL.description)! + } + + /// 전체 path (도메인 + urlPath) + var path: String { + return domain.url + urlPath + } + + /// 기본 헤더 (인증 없는 헤더 사용) + var headers: [String : String]? { + return APIHeader.baseHeader + } + + /// 네트워크 요청 Task (파라미터 유무/메서드에 따라 인코딩 방식 분기) + var task: NetworkTask { + if let parameters = parameters { + if method == .get { + return .requestParameters( + parameters: parameters, + encoding: .url + ) + } else { + return .requestParameters( + parameters: parameters, + encoding: .json + ) + } + } else { + return .requestPlain + } + } +} diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/CustomParameterEncoding.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/CustomParameterEncoding.swift index e69de29..454d161 100644 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/CustomParameterEncoding.swift +++ b/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/CustomParameterEncoding.swift @@ -0,0 +1,48 @@ +// +// CustomParameterEncoding.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +/// URLRequest의 파라미터 인코딩 방식을 정의하는 enum입니다. +/// +/// - url: 쿼리 파라미터로 인코딩 (GET 등) +/// - json: HTTP Body에 JSON으로 인코딩 (POST, PUT 등) +enum CustomParameterEncoding { + case url + case json + + /// 파라미터를 주어진 URLRequest에 인코딩하여 반환합니다. + /// + /// - Parameters: + /// - request: 인코딩할 URLRequest + /// - parameters: 인코딩할 파라미터 딕셔너리 + /// - Returns: 파라미터가 인코딩된 URLRequest + /// - Throws: 인코딩 실패 시 에러(DataError) + func encode(_ request: URLRequest, with parameters: [String: Any]) throws -> URLRequest { + var request = request + switch self { + case .url: + // URL 쿼리 파라미터로 인코딩 + guard let url = request.url else { + throw DataError.customError("Invalid URL") + } + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) + components?.queryItems = parameters + .map { URLQueryItem(name: $0.key, value: "\($0.value)") } + request.url = components?.url + case .json: + // HTTP Body에 JSON으로 인코딩 + let jsonData = try JSONSerialization.data( + withJSONObject: parameters, + options: [] + ) + request.httpBody = jsonData + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + } + return request + } +} diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/NetworkTask.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/NetworkTask.swift index e69de29..e91a7d4 100644 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/NetworkTask.swift +++ b/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/NetworkTask.swift @@ -0,0 +1,28 @@ +// +// NetworkTask.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +/// 네트워크 요청 시 사용할 파라미터 인코딩 및 전송 방식을 정의하는 enum입니다. +enum NetworkTask { + /// 파라미터 없이 단순 요청 (GET, DELETE 등) + case requestPlain + + /// 파라미터와 인코딩 방식이 있는 요청 (주로 GET, POST 등) + case requestParameters( + parameters: [String: Any], + encoding: CustomParameterEncoding + ) + + /// URL 파라미터와 Body 파라미터를 모두 포함하는 복합 요청 + /// (예: 쿼리 + 바디 동시 필요 시) + case requestCompositeParameters( + bodyParameters: [String: Any], + bodyEncoding: CustomParameterEncoding, + urlParameters: [String: Any] + ) +} diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/TargetType.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/TargetType.swift index e69de29..baa3672 100644 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/TargetType.swift +++ b/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/TargetType.swift @@ -0,0 +1,30 @@ +// +// TargetType.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + + +/// 네트워크 API 요청을 추상화하는 프로토콜입니다. +/// +/// - 각 API 엔드포인트별로 이 프로토콜을 채택한 enum/struct를 만들면 +/// 공통적인 네트워크 요청 빌드가 가능합니다. +protocol TargetType { + /// 기본 도메인 URL (예: https://api.example.com) + var baseURL: URL { get } + + /// 엔드포인트 경로 (예: "/books/search") + var path: String { get } + + /// HTTP 메서드 (GET, POST 등) + var method: HTTPMethod { get } + + /// HTTP 헤더 (필요 시) + var headers: [String: String]? { get } + + /// 파라미터 및 인코딩 방식 등 요청 작업 + var task: NetworkTask { get } +} From 9315147667ca8832cc245bfda9009ae33cf5a2c5 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 16 Sep 2025 20:59:06 +0900 Subject: [PATCH 06/18] =?UTF-8?q?=E2=9C=A8[feat]:=20provider=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Provider/AsyncProvider.swift | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/Provider/AsyncProvider.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/Provider/AsyncProvider.swift index e69de29..fc4fcad 100644 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/Provider/AsyncProvider.swift +++ b/AppleStoreProductKiosk/Network/Common/NetworkManger/Provider/AsyncProvider.swift @@ -0,0 +1,156 @@ +// +// AsyncProvider.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation +import LogMacro +import SwiftUI + +/// URLSession을 이용해 비동기 네트워크 요청 및 재시도, 로깅, 에러 처리를 담당하는 Provider입니다. +/// +/// - 제네릭 TargetType을 받아 다양한 API 요청을 처리할 수 있습니다. +/// - 응답 디코딩, 상태 코드별 에러 처리, 500 에러시 재시도, 로깅 기능을 포함합니다. +final class AsyncProvider { + /// 사용할 URLSession (기본값: .shared) + private let session: URLSession + + /// 최대 재시도 횟수 + private let maxRetryCount = 3 + + /// 재시도 간격(초) + private let retryDelay: TimeInterval = 2.0 + + /// Provider 초기화 + /// - Parameter session: 사용할 URLSession (기본값: .shared) + init(session: URLSession = .shared) { + self.session = session + } + + /// 비동기 네트워크 요청을 실행하고 응답을 디코딩합니다. + /// + /// - Parameters: + /// - target: API 요청 정보를 담은 TargetType + /// - type: 디코딩할 Decodable 타입 + /// - Returns: 디코딩된 객체 + /// - Throws: 네트워크/디코딩/상태코드 에러 등 + func requestAsync( + _ target: T, + decodeTo type: D.Type + ) async throws -> D { + let request = URLRequestBuilder.buildRequest(from: target) + // ✅ 요청 로깅 + await URLSessionLogger.shared.logRequest(request) + + return try await executeWithRetry( + request: request, + decodeTo: type, + retryCount: 0 + ) + } + + /// 재시도 로직을 포함한 네트워크 요청 실행 함수입니다. + /// + /// - Parameters: + /// - request: URLRequest + /// - type: 디코딩할 타입 + /// - retryCount: 현재 재시도 횟수 + /// - Returns: 디코딩된 객체 + /// - Throws: 네트워크/디코딩/상태코드 에러 등 + private func executeWithRetry( + request: URLRequest, + decodeTo type: D.Type, + retryCount: Int + ) async throws -> D { + do { + let (data, response) = try await session.data(for: request) + // ✅ 응답 로그 + await URLSessionLogger.shared.logResponse(data: data, response: response, error: nil) + guard let httpResponse = response as? HTTPURLResponse else { + Log.error("No HTTP response received") + throw DataError.noData + } + + switch httpResponse.statusCode { + case 200...299: + // 성공 응답 처리 + return try data.decoded(as: D.self) + + case 400: + Log.error("Bad Request (400) for URL: \(request.url?.absoluteString ?? "No URL")") + throw DataError.customError("Bad Request (400)") + + case 404: + Log.error("Not Found (404) for URL: \(request.url?.absoluteString ?? "No URL")") + throw DataError.customError("Not Found (404)") + + case 500: + Log.error("Internal Server Error (500), attempting to decode response (Retry Count: \(retryCount + 1))") + if retryCount < maxRetryCount { + // 500 에러일 때도 디코딩을 시도합니다. + if let decodedData = try? decodeErrorResponseData( + data: data, + decodeTo: type + ) { + // 디코딩이 성공하면 반환 + return decodedData + } + + // 대기 후 재시도 + try await Task.sleep(nanoseconds: UInt64(retryDelay * 1_000_000_000)) + return try await executeWithRetry( + request: request, + decodeTo: type, + retryCount: retryCount + 1 + ) + } else { + Log.error("Failed after \(maxRetryCount) retries for 500 error response") + throw DataError.unhandledStatusCode(httpResponse.statusCode) + } + + default: + Log.error("Unhandled status code: \(httpResponse.statusCode) for URL: \(request.url?.absoluteString ?? "No URL")") + throw DataError.unhandledStatusCode(httpResponse.statusCode) + } + } catch { + await URLSessionLogger.shared.logResponse(data: nil, response: nil, error: error) + Log.error("Network request failed with error: \(error.localizedDescription)") + if retryCount < maxRetryCount { + // 대기 후 재시도 + try await Task.sleep(nanoseconds: UInt64(retryDelay * 1_000_000_000)) + return try await executeWithRetry( + request: request, + decodeTo: type, + retryCount: retryCount + 1 + ) + } else { + throw error // 재시도 횟수를 초과한 경우 원래 에러를 던짐 + } + } + } + + /// 500 에러 응답에서도 디코딩을 시도하는 함수입니다. + /// + /// - Parameters: + /// - data: 응답 데이터 + /// - type: 디코딩할 타입 + /// - Returns: 디코딩된 객체 + /// - Throws: 디코딩 실패 시 에러 + private func decodeErrorResponseData(data: Data, decodeTo type: D.Type) throws -> D { + let decoder = JSONDecoder() + + // 데이터를 제네릭 D 타입으로 디코딩 시도 + if let decodedData = try? decoder.decode(D.self, from: data) { + Log.debug("Successfully decoded response: \(decodedData)") + return decodedData + } else { + Log.error("Failed to decode response as type \(D.self)") + throw URLError(.cannotParseResponse) + } + } +} + +// AsyncProvider는 @unchecked Sendable로 선언 (내부 상태 없음) +extension AsyncProvider: @unchecked Sendable {} From 0ba9edb676d59a89cc8f8ccf0e0503e34efb87c3 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 16 Sep 2025 20:59:16 +0900 Subject: [PATCH 07/18] =?UTF-8?q?=E2=9C=A8[feat]:header=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NetworkManger/header/APIHeader.swift | 19 +++++++++++++++++++ .../header/APIHeaderManger.swift | 16 ++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeader.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeader.swift index 941d1d5..b532f34 100644 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeader.swift +++ b/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeader.swift @@ -6,3 +6,22 @@ // import Foundation + +// API 요청에 사용되는 공통 헤더 키를 정의하는 구조체입니다. +enum APIHeader { + /// Content-Type 헤더 키 + static let contentType = "Content-Type" + +} + + +extension APIHeader { + + /// 인증 토큰이 필요 없는 기본 헤더 + public static var baseHeader: [String: String] { + [ + contentType: APIHeaderManger.contentType + ] + } + +} diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeaderManger.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeaderManger.swift index e69de29..2daed58 100644 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeaderManger.swift +++ b/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeaderManger.swift @@ -0,0 +1,16 @@ +// +// APIHeaderManger.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +/// API 헤더의 값(컨텐츠 타입 등)을 관리하는 enum입니다. +enum APIHeaderManger { + /// 앱 패키지명(필요 시 사용, 기본값 "-") + static let appPackageName: String = "-" + /// Content-Type 값 (기본: application/json) + static let contentType: String = "application/json" +} From a50645806f279271b4968cb1f4dce0e6b8ce9134 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 16 Sep 2025 20:59:42 +0900 Subject: [PATCH 08/18] =?UTF-8?q?=E2=9C=A8[feat]:=20URLRequestBuilder=20?= =?UTF-8?q?=ED=86=B5=EC=8B=A0=20=EB=B6=80=EB=B6=84=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../URLRequestBuilder/URLRequestBuilder.swift | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/URLRequestBuilder/URLRequestBuilder.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/URLRequestBuilder/URLRequestBuilder.swift index b8b46c4..a0edac0 100644 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/URLRequestBuilder/URLRequestBuilder.swift +++ b/AppleStoreProductKiosk/Network/Common/NetworkManger/URLRequestBuilder/URLRequestBuilder.swift @@ -1,7 +1,65 @@ // -// Untitled.swift +// URLRequestBuilder.swift // AppleStoreProductKiosk // // Created by Wonji Suh on 9/16/25. // +import Foundation +import LogMacro + +/// TargetType을 기반으로 URLRequest를 생성하는 빌더 클래스입니다. +/// +/// - 파라미터 인코딩, 헤더 설정, URL 조합 등 네트워크 요청 생성의 모든 과정을 담당합니다. +class URLRequestBuilder { + /// 기본 생성자 + public init() {} + + /// TargetType을 받아 URLRequest를 생성합니다. + /// + /// - Parameter target: 네트워크 요청 정보가 담긴 TargetType + /// - Returns: 완성된 URLRequest + static func buildRequest(from target: TargetType) -> URLRequest { + // URL 생성 시 불필요한 공백을 제거하기 위한 trim 적용 + let url = target.baseURL.appendingPathComponent( + target.path.trimmingCharacters(in: .whitespaces) + ) + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) + var request = URLRequest(url: components?.url ?? url) + request.httpMethod = target.method.rawValue + + // 헤더 추가 + if let headers = target.headers { + headers.forEach { request.setValue($1, forHTTPHeaderField: $0) } + } + + // 파라미터 및 인코딩 처리 + switch target.task { + case .requestParameters(let parameters, let encoding): + do { + request = try encoding.encode(request, with: parameters) + } catch { + Log.error("Failed to encode parameters: %{public}@", error.localizedDescription) + } + case .requestCompositeParameters( + let bodyParameters, + let bodyEncoding, + let urlParameters + ): + do { + components?.queryItems = urlParameters + .map { URLQueryItem(name: $0.key, value: "\($0.value)") } + request.url = components?.url + request = try bodyEncoding.encode(request, with: bodyParameters) + } catch { + Log.error("Failed to encode composite parameters: %{public}@", error.localizedDescription) + } + case .requestPlain: + // 파라미터 없이 단순 요청 + components?.queryItems = nil + request.url = components?.url + } + + return request + } +} From 1bd09524540ffed98131d5b17ef2bd1a7b0afc0e Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 16 Sep 2025 21:00:00 +0900 Subject: [PATCH 09/18] =?UTF-8?q?=E2=9C=A8[feat]:=20data=20error=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Model/Error/DataError.swift | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/AppleStoreProductKiosk/Data/Model/Error/DataError.swift b/AppleStoreProductKiosk/Data/Model/Error/DataError.swift index e69de29..d3620e1 100644 --- a/AppleStoreProductKiosk/Data/Model/Error/DataError.swift +++ b/AppleStoreProductKiosk/Data/Model/Error/DataError.swift @@ -0,0 +1,37 @@ +// +// DataError.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +/// 네트워크 및 데이터 처리 과정에서 발생할 수 있는 오류를 정의한 enum입니다. +enum DataError: Error, LocalizedError, Sendable, Equatable { + /// 데이터가 없음 + case noData + + /// 커스텀 에러 메시지 + case customError(String) + + /// 처리하지 않은 HTTP 상태 코드 + case unhandledStatusCode(Int) + + /// HTTP 응답 오류 (응답 객체, 에러 메시지) + case httpResponseError(HTTPURLResponse, String) + + /// 사용자에게 표시할 에러 메시지 + var errorDescription: String? { + switch self { + case .noData: + return "데이터를 받지 못했습니다" + case .customError(let message): + return message + case .unhandledStatusCode(let code): + return "처리되지 않은 상태 코드: \(code)" + case .httpResponseError(let response, let message): + return "HTTP \(response.statusCode): \(message)" + } + } +} From 929f500de3375e5dcaaaa8e50562753af14c2374 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 16 Sep 2025 21:00:20 +0900 Subject: [PATCH 10/18] =?UTF-8?q?=E2=9C=A8[feat]:=20kiosstk=20service=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project.pbxproj | 15 +++++++ .../Network/Service/KioskProductService.swift | 42 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/AppleStoreProductKiosk.xcodeproj/project.pbxproj b/AppleStoreProductKiosk.xcodeproj/project.pbxproj index 42f0bbb..aaaba52 100644 --- a/AppleStoreProductKiosk.xcodeproj/project.pbxproj +++ b/AppleStoreProductKiosk.xcodeproj/project.pbxproj @@ -34,9 +34,22 @@ C8F725F52E790FA100C2A1DE /* AppleStoreProductKioskUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AppleStoreProductKioskUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 7F41C5542E798756007C4017 /* Exceptions for "AppleStoreProductKiosk" folder in "AppleStoreProductKiosk" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Resources/Info.plist, + ); + target = C8F725DD2E790F9F00C2A1DE /* AppleStoreProductKiosk */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ C8F725E02E790F9F00C2A1DE /* AppleStoreProductKiosk */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 7F41C5542E798756007C4017 /* Exceptions for "AppleStoreProductKiosk" folder in "AppleStoreProductKiosk" target */, + ); path = AppleStoreProductKiosk; sourceTree = ""; }; @@ -415,6 +428,7 @@ DEVELOPMENT_TEAM = N94CS4N6VR; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AppleStoreProductKiosk/Resources/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -448,6 +462,7 @@ DEVELOPMENT_TEAM = N94CS4N6VR; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AppleStoreProductKiosk/Resources/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/AppleStoreProductKiosk/Network/Service/KioskProductService.swift b/AppleStoreProductKiosk/Network/Service/KioskProductService.swift index e69de29..9f176d0 100644 --- a/AppleStoreProductKiosk/Network/Service/KioskProductService.swift +++ b/AppleStoreProductKiosk/Network/Service/KioskProductService.swift @@ -0,0 +1,42 @@ +// +// KioskProductService.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + + +import Foundation + + +enum KioskProductService { + case proudctList +} + + +extension KioskProductService: BaseTargetType { + var domain: KioskProductDomain { + return .producut + } + + var urlPath: String { + switch self { + case .proudctList: + return KioskProductAPI.productLists.description + } + } + + var parameters: [String : Any]? { + switch self { + case .proudctList: + return nil + } + } + + var method: HTTPMethod { + switch self { + case .proudctList: + return .get + } + } +} From e9307722f6cc650f2a5203767192d4ab9414dd95 Mon Sep 17 00:00:00 2001 From: Roy Date: Tue, 16 Sep 2025 21:00:37 +0900 Subject: [PATCH 11/18] =?UTF-8?q?=E2=9C=A8[feat]:=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/API/BaseAPI.swift | 19 ++++++++++++++++ .../Network/API/KioskProductAPI.swift | 13 ++++++++++- .../Network/API/KioskProductDomain.swift | 22 +++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/AppleStoreProductKiosk/Network/API/BaseAPI.swift b/AppleStoreProductKiosk/Network/API/BaseAPI.swift index e69de29..75cc65b 100644 --- a/AppleStoreProductKiosk/Network/API/BaseAPI.swift +++ b/AppleStoreProductKiosk/Network/API/BaseAPI.swift @@ -0,0 +1,19 @@ +// +// BaseAPI.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +enum BaseAPI { + case baseURL + + var description: String { + switch self { + case .baseURL: + return "https://applestoreproductkiosk.free.beeceptor.com" + } + } +} diff --git a/AppleStoreProductKiosk/Network/API/KioskProductAPI.swift b/AppleStoreProductKiosk/Network/API/KioskProductAPI.swift index 9974053..f16f29c 100644 --- a/AppleStoreProductKiosk/Network/API/KioskProductAPI.swift +++ b/AppleStoreProductKiosk/Network/API/KioskProductAPI.swift @@ -1,8 +1,19 @@ // -// kioskproductapi.swift +// KioskProductAPI.swift // AppleStoreProductKiosk // // Created by Wonji Suh on 9/16/25. // import Foundation + +enum KioskProductAPI { + case productLists + + var description: String { + switch self { + case .productLists: + return "/products" + } + } +} diff --git a/AppleStoreProductKiosk/Network/API/KioskProductDomain.swift b/AppleStoreProductKiosk/Network/API/KioskProductDomain.swift index e69de29..b135235 100644 --- a/AppleStoreProductKiosk/Network/API/KioskProductDomain.swift +++ b/AppleStoreProductKiosk/Network/API/KioskProductDomain.swift @@ -0,0 +1,22 @@ +// +// KioskProductDomain.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +enum KioskProductDomain { + case producut +} + + +extension KioskProductDomain { + var url: String { + switch self { + case .producut: + return "/api/apple-store-kiosk" + } + } +} From 54fe1eb8d2b735a98cabe1eaea5e721a60b81628 Mon Sep 17 00:00:00 2001 From: Roy Date: Wed, 17 Sep 2025 14:01:34 +0900 Subject: [PATCH 12/18] =?UTF-8?q?=E2=9C=A8[feat]:=20common=20=EC=9D=B4?= =?UTF-8?q?=EB=9D=BC=EB=8A=94=20=EA=B1=B4=20=EC=9D=B4=EC=83=81=20=ED=95=B4?= =?UTF-8?q?=EC=84=9C=20core=20=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NetworkManger/Data/Extension+Data.swift | 21 --- .../Logger/URLSessionLogger.swift | 113 ------------- .../NetworkManger/Method/HTTPMethod.swift | 21 --- .../Provider/AsyncProvider.swift | 156 ------------------ .../TargeType/BaseTargetType.swift | 59 ------- .../TargeType/CustomParameterEncoding.swift | 48 ------ .../NetworkManger/TargeType/NetworkTask.swift | 28 ---- .../NetworkManger/TargeType/TargetType.swift | 30 ---- .../URLRequestBuilder/URLRequestBuilder.swift | 65 -------- .../NetworkManger/header/APIHeader.swift | 27 --- .../header/APIHeaderManger.swift | 16 -- 11 files changed, 584 deletions(-) delete mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/Data/Extension+Data.swift delete mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/Logger/URLSessionLogger.swift delete mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/Method/HTTPMethod.swift delete mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/Provider/AsyncProvider.swift delete mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/BaseTargetType.swift delete mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/CustomParameterEncoding.swift delete mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/NetworkTask.swift delete mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/TargetType.swift delete mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/URLRequestBuilder/URLRequestBuilder.swift delete mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeader.swift delete mode 100644 AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeaderManger.swift diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/Data/Extension+Data.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/Data/Extension+Data.swift deleted file mode 100644 index 0b3d1ab..0000000 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/Data/Extension+Data.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Extension+Data.swift -// AppleStoreProductKiosk -// -// Created by Wonji Suh on 9/16/25. -// - -import Foundation - -/// Data 타입에 대한 JSON 디코딩 확장입니다. -extension Data { - /// Data를 지정한 Decodable 타입으로 디코딩합니다. - /// - /// - Parameter type: 디코딩할 타입 - /// - Returns: 디코딩된 객체 - /// - Throws: 디코딩 실패 시 에러 발생 - func decoded(as type: T.Type) throws -> T { - let decoder = JSONDecoder() - return try decoder.decode(T.self, from: self) - } -} diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/Logger/URLSessionLogger.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/Logger/URLSessionLogger.swift deleted file mode 100644 index f1f8614..0000000 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/Logger/URLSessionLogger.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// URLSessionLogger.swift -// AppleStoreProductKiosk -// -// Created by Wonji Suh on 9/16/25. -// - -import Foundation -import LogMacro - -/// URLSession 네트워크 요청/응답을 콘솔에 로그로 출력하는 싱글턴 유틸리티입니다. -/// -/// - DEBUG 모드에서만 동작하며, 요청/응답의 메서드, URL, 헤더, 바디, 에러 등을 보기 좋게 출력합니다. -final class URLSessionLogger { - /// 싱글턴 인스턴스 - static let shared = URLSessionLogger() - - /// 외부에서 직접 생성하지 못하도록 private init - private init() {} - - // MARK: - 요청 로그 - - /// 네트워크 요청 정보를 콘솔에 출력합니다. - /// - Parameter request: 로깅할 URLRequest - @MainActor - func logRequest(_ request: URLRequest) { -#if DEBUG - Log.debug("⎡--------------------- REQUEST ---------------------⎤") - - // HTTP 메서드 - if let method = request.httpMethod { - Log.network("[Method]", method) - } - - // 요청 URL - if let url = request.url?.absoluteString { - Log.network("[URL]", url) - } - - // 헤더 - if let headers = request.allHTTPHeaderFields { - Log.network("[Headers]") - for (key, value) in headers { - Log.network(" \(key): \(value)") - } - } - - // 바디 - if let body = request.httpBody, - let bodyString = String(data: body, encoding: .utf8) { - Log.network("[Body]") - Log.network(" \(bodyString)") - } - - Log.network("⎣------------------ END REQUEST --------------------⎦") -#endif - } - - // MARK: - 응답 로그 - - /// 네트워크 응답 정보를 콘솔에 출력합니다. - /// - Parameters: - /// - data: 응답 데이터 - /// - response: URLResponse 객체 - /// - error: 네트워크 에러 - @MainActor - func logResponse(data: Data?, response: URLResponse?, error: Error?) { -#if DEBUG - Log.network("⎡--------------------- RESPONSE --------------------⎤") - - // 응답 URL - if let url = response?.url?.absoluteString { - Log.network("[URL]", url) - } - - // HTTP 상태 코드 및 헤더 - if let httpResponse = response as? HTTPURLResponse { - Log.network("[Status Code] \(httpResponse.statusCode)") - Log.network("[Response Headers]") - for (key, value) in httpResponse.allHeaderFields { - Log.network(" \(key): \(value)") - } - } - - // 바디 (JSON pretty print 시도, 실패 시 raw 출력) - if let data = data { - do { - let json = try JSONSerialization.jsonObject(with: data, options: []) - let prettyData = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted]) - if let prettyString = String(data: prettyData, encoding: .utf8) { - Log.network("[Body (Map)]") - Log.network(prettyString) - } - } catch { - // JSON 디코딩 실패 시 원본 문자열 출력 - if let rawString = String(data: data, encoding: .utf8) { - Log.network("[Body (Raw)]") - Log.network(rawString) - } else { - Log.network("[Body] Cannot decode data") - } - } - } - - // 에러 - if let error = error { - Log.network("[Error] \(error.localizedDescription)") - } - - Log.network("⎣------------------ END RESPONSE -------------------⎦") -#endif - } -} diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/Method/HTTPMethod.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/Method/HTTPMethod.swift deleted file mode 100644 index e023d44..0000000 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/Method/HTTPMethod.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// HTTPMethod.swift -// AppleStoreProductKiosk -// -// Created by Wonji Suh on 9/16/25. -// - -import Foundation - -/// HTTP 요청에서 사용되는 메서드 타입을 정의한 enum입니다. -enum HTTPMethod: String { - /// GET: 데이터 조회 - case get = "GET" - /// POST: 데이터 생성 - case post = "POST" - /// PUT: 데이터 전체 수정 - case put = "PUT" - /// DELETE: 데이터 삭제 - case delete = "DELETE" -} - diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/Provider/AsyncProvider.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/Provider/AsyncProvider.swift deleted file mode 100644 index fc4fcad..0000000 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/Provider/AsyncProvider.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// AsyncProvider.swift -// AppleStoreProductKiosk -// -// Created by Wonji Suh on 9/16/25. -// - -import Foundation -import LogMacro -import SwiftUI - -/// URLSession을 이용해 비동기 네트워크 요청 및 재시도, 로깅, 에러 처리를 담당하는 Provider입니다. -/// -/// - 제네릭 TargetType을 받아 다양한 API 요청을 처리할 수 있습니다. -/// - 응답 디코딩, 상태 코드별 에러 처리, 500 에러시 재시도, 로깅 기능을 포함합니다. -final class AsyncProvider { - /// 사용할 URLSession (기본값: .shared) - private let session: URLSession - - /// 최대 재시도 횟수 - private let maxRetryCount = 3 - - /// 재시도 간격(초) - private let retryDelay: TimeInterval = 2.0 - - /// Provider 초기화 - /// - Parameter session: 사용할 URLSession (기본값: .shared) - init(session: URLSession = .shared) { - self.session = session - } - - /// 비동기 네트워크 요청을 실행하고 응답을 디코딩합니다. - /// - /// - Parameters: - /// - target: API 요청 정보를 담은 TargetType - /// - type: 디코딩할 Decodable 타입 - /// - Returns: 디코딩된 객체 - /// - Throws: 네트워크/디코딩/상태코드 에러 등 - func requestAsync( - _ target: T, - decodeTo type: D.Type - ) async throws -> D { - let request = URLRequestBuilder.buildRequest(from: target) - // ✅ 요청 로깅 - await URLSessionLogger.shared.logRequest(request) - - return try await executeWithRetry( - request: request, - decodeTo: type, - retryCount: 0 - ) - } - - /// 재시도 로직을 포함한 네트워크 요청 실행 함수입니다. - /// - /// - Parameters: - /// - request: URLRequest - /// - type: 디코딩할 타입 - /// - retryCount: 현재 재시도 횟수 - /// - Returns: 디코딩된 객체 - /// - Throws: 네트워크/디코딩/상태코드 에러 등 - private func executeWithRetry( - request: URLRequest, - decodeTo type: D.Type, - retryCount: Int - ) async throws -> D { - do { - let (data, response) = try await session.data(for: request) - // ✅ 응답 로그 - await URLSessionLogger.shared.logResponse(data: data, response: response, error: nil) - guard let httpResponse = response as? HTTPURLResponse else { - Log.error("No HTTP response received") - throw DataError.noData - } - - switch httpResponse.statusCode { - case 200...299: - // 성공 응답 처리 - return try data.decoded(as: D.self) - - case 400: - Log.error("Bad Request (400) for URL: \(request.url?.absoluteString ?? "No URL")") - throw DataError.customError("Bad Request (400)") - - case 404: - Log.error("Not Found (404) for URL: \(request.url?.absoluteString ?? "No URL")") - throw DataError.customError("Not Found (404)") - - case 500: - Log.error("Internal Server Error (500), attempting to decode response (Retry Count: \(retryCount + 1))") - if retryCount < maxRetryCount { - // 500 에러일 때도 디코딩을 시도합니다. - if let decodedData = try? decodeErrorResponseData( - data: data, - decodeTo: type - ) { - // 디코딩이 성공하면 반환 - return decodedData - } - - // 대기 후 재시도 - try await Task.sleep(nanoseconds: UInt64(retryDelay * 1_000_000_000)) - return try await executeWithRetry( - request: request, - decodeTo: type, - retryCount: retryCount + 1 - ) - } else { - Log.error("Failed after \(maxRetryCount) retries for 500 error response") - throw DataError.unhandledStatusCode(httpResponse.statusCode) - } - - default: - Log.error("Unhandled status code: \(httpResponse.statusCode) for URL: \(request.url?.absoluteString ?? "No URL")") - throw DataError.unhandledStatusCode(httpResponse.statusCode) - } - } catch { - await URLSessionLogger.shared.logResponse(data: nil, response: nil, error: error) - Log.error("Network request failed with error: \(error.localizedDescription)") - if retryCount < maxRetryCount { - // 대기 후 재시도 - try await Task.sleep(nanoseconds: UInt64(retryDelay * 1_000_000_000)) - return try await executeWithRetry( - request: request, - decodeTo: type, - retryCount: retryCount + 1 - ) - } else { - throw error // 재시도 횟수를 초과한 경우 원래 에러를 던짐 - } - } - } - - /// 500 에러 응답에서도 디코딩을 시도하는 함수입니다. - /// - /// - Parameters: - /// - data: 응답 데이터 - /// - type: 디코딩할 타입 - /// - Returns: 디코딩된 객체 - /// - Throws: 디코딩 실패 시 에러 - private func decodeErrorResponseData(data: Data, decodeTo type: D.Type) throws -> D { - let decoder = JSONDecoder() - - // 데이터를 제네릭 D 타입으로 디코딩 시도 - if let decodedData = try? decoder.decode(D.self, from: data) { - Log.debug("Successfully decoded response: \(decodedData)") - return decodedData - } else { - Log.error("Failed to decode response as type \(D.self)") - throw URLError(.cannotParseResponse) - } - } -} - -// AsyncProvider는 @unchecked Sendable로 선언 (내부 상태 없음) -extension AsyncProvider: @unchecked Sendable {} diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/BaseTargetType.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/BaseTargetType.swift deleted file mode 100644 index 4df56b6..0000000 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/BaseTargetType.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// BaseTargetType.swift -// AppleStoreProductKiosk -// -// Created by Wonji Suh on 9/16/25. -// - -import Foundation - -/// API 타겟의 공통 속성을 정의하는 프로토콜입니다. -/// -/// - 도메인, URL 경로, 파라미터 등 API 요청에 필요한 정보를 제공합니다. -protocol BaseTargetType: TargetType { - /// 도메인 구분값 - var domain: KioskProductDomain { get } - /// API URL 경로 - var urlPath: String { get } - /// 요청 파라미터 - var parameters:[String: Any]? { get } -} - -// MARK: - BaseTargetType 기본 구현 - - -extension BaseTargetType { - /// API의 baseURL - var baseURL: URL { - return URL(string: BaseAPI.baseURL.description)! - } - - /// 전체 path (도메인 + urlPath) - var path: String { - return domain.url + urlPath - } - - /// 기본 헤더 (인증 없는 헤더 사용) - var headers: [String : String]? { - return APIHeader.baseHeader - } - - /// 네트워크 요청 Task (파라미터 유무/메서드에 따라 인코딩 방식 분기) - var task: NetworkTask { - if let parameters = parameters { - if method == .get { - return .requestParameters( - parameters: parameters, - encoding: .url - ) - } else { - return .requestParameters( - parameters: parameters, - encoding: .json - ) - } - } else { - return .requestPlain - } - } -} diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/CustomParameterEncoding.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/CustomParameterEncoding.swift deleted file mode 100644 index 454d161..0000000 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/CustomParameterEncoding.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// CustomParameterEncoding.swift -// AppleStoreProductKiosk -// -// Created by Wonji Suh on 9/16/25. -// - -import Foundation - -/// URLRequest의 파라미터 인코딩 방식을 정의하는 enum입니다. -/// -/// - url: 쿼리 파라미터로 인코딩 (GET 등) -/// - json: HTTP Body에 JSON으로 인코딩 (POST, PUT 등) -enum CustomParameterEncoding { - case url - case json - - /// 파라미터를 주어진 URLRequest에 인코딩하여 반환합니다. - /// - /// - Parameters: - /// - request: 인코딩할 URLRequest - /// - parameters: 인코딩할 파라미터 딕셔너리 - /// - Returns: 파라미터가 인코딩된 URLRequest - /// - Throws: 인코딩 실패 시 에러(DataError) - func encode(_ request: URLRequest, with parameters: [String: Any]) throws -> URLRequest { - var request = request - switch self { - case .url: - // URL 쿼리 파라미터로 인코딩 - guard let url = request.url else { - throw DataError.customError("Invalid URL") - } - var components = URLComponents(url: url, resolvingAgainstBaseURL: false) - components?.queryItems = parameters - .map { URLQueryItem(name: $0.key, value: "\($0.value)") } - request.url = components?.url - case .json: - // HTTP Body에 JSON으로 인코딩 - let jsonData = try JSONSerialization.data( - withJSONObject: parameters, - options: [] - ) - request.httpBody = jsonData - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - } - return request - } -} diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/NetworkTask.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/NetworkTask.swift deleted file mode 100644 index e91a7d4..0000000 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/NetworkTask.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// NetworkTask.swift -// AppleStoreProductKiosk -// -// Created by Wonji Suh on 9/16/25. -// - -import Foundation - -/// 네트워크 요청 시 사용할 파라미터 인코딩 및 전송 방식을 정의하는 enum입니다. -enum NetworkTask { - /// 파라미터 없이 단순 요청 (GET, DELETE 등) - case requestPlain - - /// 파라미터와 인코딩 방식이 있는 요청 (주로 GET, POST 등) - case requestParameters( - parameters: [String: Any], - encoding: CustomParameterEncoding - ) - - /// URL 파라미터와 Body 파라미터를 모두 포함하는 복합 요청 - /// (예: 쿼리 + 바디 동시 필요 시) - case requestCompositeParameters( - bodyParameters: [String: Any], - bodyEncoding: CustomParameterEncoding, - urlParameters: [String: Any] - ) -} diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/TargetType.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/TargetType.swift deleted file mode 100644 index baa3672..0000000 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/TargetType.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// TargetType.swift -// AppleStoreProductKiosk -// -// Created by Wonji Suh on 9/16/25. -// - -import Foundation - - -/// 네트워크 API 요청을 추상화하는 프로토콜입니다. -/// -/// - 각 API 엔드포인트별로 이 프로토콜을 채택한 enum/struct를 만들면 -/// 공통적인 네트워크 요청 빌드가 가능합니다. -protocol TargetType { - /// 기본 도메인 URL (예: https://api.example.com) - var baseURL: URL { get } - - /// 엔드포인트 경로 (예: "/books/search") - var path: String { get } - - /// HTTP 메서드 (GET, POST 등) - var method: HTTPMethod { get } - - /// HTTP 헤더 (필요 시) - var headers: [String: String]? { get } - - /// 파라미터 및 인코딩 방식 등 요청 작업 - var task: NetworkTask { get } -} diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/URLRequestBuilder/URLRequestBuilder.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/URLRequestBuilder/URLRequestBuilder.swift deleted file mode 100644 index a0edac0..0000000 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/URLRequestBuilder/URLRequestBuilder.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// URLRequestBuilder.swift -// AppleStoreProductKiosk -// -// Created by Wonji Suh on 9/16/25. -// - -import Foundation -import LogMacro - -/// TargetType을 기반으로 URLRequest를 생성하는 빌더 클래스입니다. -/// -/// - 파라미터 인코딩, 헤더 설정, URL 조합 등 네트워크 요청 생성의 모든 과정을 담당합니다. -class URLRequestBuilder { - /// 기본 생성자 - public init() {} - - /// TargetType을 받아 URLRequest를 생성합니다. - /// - /// - Parameter target: 네트워크 요청 정보가 담긴 TargetType - /// - Returns: 완성된 URLRequest - static func buildRequest(from target: TargetType) -> URLRequest { - // URL 생성 시 불필요한 공백을 제거하기 위한 trim 적용 - let url = target.baseURL.appendingPathComponent( - target.path.trimmingCharacters(in: .whitespaces) - ) - var components = URLComponents(url: url, resolvingAgainstBaseURL: false) - var request = URLRequest(url: components?.url ?? url) - request.httpMethod = target.method.rawValue - - // 헤더 추가 - if let headers = target.headers { - headers.forEach { request.setValue($1, forHTTPHeaderField: $0) } - } - - // 파라미터 및 인코딩 처리 - switch target.task { - case .requestParameters(let parameters, let encoding): - do { - request = try encoding.encode(request, with: parameters) - } catch { - Log.error("Failed to encode parameters: %{public}@", error.localizedDescription) - } - case .requestCompositeParameters( - let bodyParameters, - let bodyEncoding, - let urlParameters - ): - do { - components?.queryItems = urlParameters - .map { URLQueryItem(name: $0.key, value: "\($0.value)") } - request.url = components?.url - request = try bodyEncoding.encode(request, with: bodyParameters) - } catch { - Log.error("Failed to encode composite parameters: %{public}@", error.localizedDescription) - } - case .requestPlain: - // 파라미터 없이 단순 요청 - components?.queryItems = nil - request.url = components?.url - } - - return request - } -} diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeader.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeader.swift deleted file mode 100644 index b532f34..0000000 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeader.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// APIHeader.swift -// AppleStoreProductKiosk -// -// Created by Wonji Suh on 9/16/25. -// - -import Foundation - -// API 요청에 사용되는 공통 헤더 키를 정의하는 구조체입니다. -enum APIHeader { - /// Content-Type 헤더 키 - static let contentType = "Content-Type" - -} - - -extension APIHeader { - - /// 인증 토큰이 필요 없는 기본 헤더 - public static var baseHeader: [String: String] { - [ - contentType: APIHeaderManger.contentType - ] - } - -} diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeaderManger.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeaderManger.swift deleted file mode 100644 index 2daed58..0000000 --- a/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeaderManger.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// APIHeaderManger.swift -// AppleStoreProductKiosk -// -// Created by Wonji Suh on 9/16/25. -// - -import Foundation - -/// API 헤더의 값(컨텐츠 타입 등)을 관리하는 enum입니다. -enum APIHeaderManger { - /// 앱 패키지명(필요 시 사용, 기본값 "-") - static let appPackageName: String = "-" - /// Content-Type 값 (기본: application/json) - static let contentType: String = "application/json" -} From b95f0e6faa3bde720673a36912eea031db5024b8 Mon Sep 17 00:00:00 2001 From: Roy Date: Wed, 17 Sep 2025 14:01:57 +0900 Subject: [PATCH 13/18] =?UTF-8?q?=E2=9C=A8[feat]:=20API=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=20=ED=99=95=EC=8B=A4=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/API/APIDomain.swift | 24 +++++++++++++ .../Network/API/APIEndpoint.swift | 35 +++++++++++++++++++ .../Network/API/BaseAPI.swift | 19 ---------- .../Network/API/KioskProductAPI.swift | 27 ++++++++++---- .../Network/API/KioskProductDomain.swift | 22 ------------ 5 files changed, 80 insertions(+), 47 deletions(-) create mode 100644 AppleStoreProductKiosk/Network/API/APIDomain.swift create mode 100644 AppleStoreProductKiosk/Network/API/APIEndpoint.swift delete mode 100644 AppleStoreProductKiosk/Network/API/BaseAPI.swift delete mode 100644 AppleStoreProductKiosk/Network/API/KioskProductDomain.swift diff --git a/AppleStoreProductKiosk/Network/API/APIDomain.swift b/AppleStoreProductKiosk/Network/API/APIDomain.swift new file mode 100644 index 0000000..e95fd86 --- /dev/null +++ b/AppleStoreProductKiosk/Network/API/APIDomain.swift @@ -0,0 +1,24 @@ +// +// APIDomain.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +// MARK: - API Domain Types + +/// API 도메인 타입 정의 +/// +/// 각 도메인별 basePath를 관리하는 단일 책임을 가집니다. +enum APIDomain { + case kiosk + + var basePath: String { + switch self { + case .kiosk: + return "/api/apple-store-kiosk" + } + } +} diff --git a/AppleStoreProductKiosk/Network/API/APIEndpoint.swift b/AppleStoreProductKiosk/Network/API/APIEndpoint.swift new file mode 100644 index 0000000..825591b --- /dev/null +++ b/AppleStoreProductKiosk/Network/API/APIEndpoint.swift @@ -0,0 +1,35 @@ +// +// APIEndpoint.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +// MARK: - APIEndpoint Protocol + +/// API 엔드포인트의 공통 속성을 정의하는 프로토콜 +protocol APIEndpoint { + var baseURL: String { get } + var basePath: String { get } + var path: String { get } + var fullPath: String { get } + var fullURL: String { get } +} + +// MARK: - APIEndpoint Default Implementation + +extension APIEndpoint { + var baseURL: String { + "https://applestoreproductkiosk.free.beeceptor.com" + } + + var fullPath: String { + basePath + path + } + + var fullURL: String { + baseURL + fullPath + } +} diff --git a/AppleStoreProductKiosk/Network/API/BaseAPI.swift b/AppleStoreProductKiosk/Network/API/BaseAPI.swift deleted file mode 100644 index 75cc65b..0000000 --- a/AppleStoreProductKiosk/Network/API/BaseAPI.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// BaseAPI.swift -// AppleStoreProductKiosk -// -// Created by Wonji Suh on 9/16/25. -// - -import Foundation - -enum BaseAPI { - case baseURL - - var description: String { - switch self { - case .baseURL: - return "https://applestoreproductkiosk.free.beeceptor.com" - } - } -} diff --git a/AppleStoreProductKiosk/Network/API/KioskProductAPI.swift b/AppleStoreProductKiosk/Network/API/KioskProductAPI.swift index f16f29c..ad044cd 100644 --- a/AppleStoreProductKiosk/Network/API/KioskProductAPI.swift +++ b/AppleStoreProductKiosk/Network/API/KioskProductAPI.swift @@ -7,13 +7,28 @@ import Foundation -enum KioskProductAPI { - case productLists +// MARK: - KioskAPI Namespace - var description: String { - switch self { - case .productLists: - return "/products" +/// Kiosk API의 네임스페이스 +/// +/// Kiosk 관련 API 엔드포인트만을 관리하는 단일 책임을 가집니다. +enum KioskAPI { + + // MARK: - Product API + + /// 상품 관련 API 엔드포인트 + enum Product: APIEndpoint { + case list + + var basePath: String { + return APIDomain.kiosk.basePath + } + + var path: String { + switch self { + case .list: + return "/products" + } } } } diff --git a/AppleStoreProductKiosk/Network/API/KioskProductDomain.swift b/AppleStoreProductKiosk/Network/API/KioskProductDomain.swift deleted file mode 100644 index b135235..0000000 --- a/AppleStoreProductKiosk/Network/API/KioskProductDomain.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// KioskProductDomain.swift -// AppleStoreProductKiosk -// -// Created by Wonji Suh on 9/16/25. -// - -import Foundation - -enum KioskProductDomain { - case producut -} - - -extension KioskProductDomain { - var url: String { - switch self { - case .producut: - return "/api/apple-store-kiosk" - } - } -} From b790498822a8b70b32364ecc7ac2fb46a8f066dd Mon Sep 17 00:00:00 2001 From: Roy Date: Wed, 17 Sep 2025 14:02:18 +0900 Subject: [PATCH 14/18] =?UTF-8?q?=E2=9C=A8[feat]:=20common=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20CORE=20=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Core/Data/Extension+Data.swift | 21 ++ .../Core/Logger/URLSessionLogger.swift | 113 ++++++++ .../Network/Core/Method/HTTPMethod.swift | 21 ++ .../Network/Core/Provider/AsyncProvider.swift | 253 ++++++++++++++++++ .../Network/Core/Provider/NetworkConfig.swift | 45 ++++ .../Core/Provider/ResponseHandler.swift | 153 +++++++++++ .../Network/Core/Provider/RetryConfig.swift | 142 ++++++++++ .../Network/Core/Provider/RetryStrategy.swift | 132 +++++++++ .../Core/TargeType/BaseTargetType.swift | 57 ++++ .../TargeType/CustomParameterEncoding.swift | 48 ++++ .../Network/Core/TargeType/NetworkTask.swift | 28 ++ .../Network/Core/TargeType/TargetType.swift | 30 +++ .../URLRequestBuilder/URLRequestBuilder.swift | 65 +++++ .../Network/Core/header/APIHeader.swift | 27 ++ .../Network/Core/header/APIHeaderManger.swift | 16 ++ 15 files changed, 1151 insertions(+) create mode 100644 AppleStoreProductKiosk/Network/Core/Data/Extension+Data.swift create mode 100644 AppleStoreProductKiosk/Network/Core/Logger/URLSessionLogger.swift create mode 100644 AppleStoreProductKiosk/Network/Core/Method/HTTPMethod.swift create mode 100644 AppleStoreProductKiosk/Network/Core/Provider/AsyncProvider.swift create mode 100644 AppleStoreProductKiosk/Network/Core/Provider/NetworkConfig.swift create mode 100644 AppleStoreProductKiosk/Network/Core/Provider/ResponseHandler.swift create mode 100644 AppleStoreProductKiosk/Network/Core/Provider/RetryConfig.swift create mode 100644 AppleStoreProductKiosk/Network/Core/Provider/RetryStrategy.swift create mode 100644 AppleStoreProductKiosk/Network/Core/TargeType/BaseTargetType.swift create mode 100644 AppleStoreProductKiosk/Network/Core/TargeType/CustomParameterEncoding.swift create mode 100644 AppleStoreProductKiosk/Network/Core/TargeType/NetworkTask.swift create mode 100644 AppleStoreProductKiosk/Network/Core/TargeType/TargetType.swift create mode 100644 AppleStoreProductKiosk/Network/Core/URLRequestBuilder/URLRequestBuilder.swift create mode 100644 AppleStoreProductKiosk/Network/Core/header/APIHeader.swift create mode 100644 AppleStoreProductKiosk/Network/Core/header/APIHeaderManger.swift diff --git a/AppleStoreProductKiosk/Network/Core/Data/Extension+Data.swift b/AppleStoreProductKiosk/Network/Core/Data/Extension+Data.swift new file mode 100644 index 0000000..0b3d1ab --- /dev/null +++ b/AppleStoreProductKiosk/Network/Core/Data/Extension+Data.swift @@ -0,0 +1,21 @@ +// +// Extension+Data.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +/// Data 타입에 대한 JSON 디코딩 확장입니다. +extension Data { + /// Data를 지정한 Decodable 타입으로 디코딩합니다. + /// + /// - Parameter type: 디코딩할 타입 + /// - Returns: 디코딩된 객체 + /// - Throws: 디코딩 실패 시 에러 발생 + func decoded(as type: T.Type) throws -> T { + let decoder = JSONDecoder() + return try decoder.decode(T.self, from: self) + } +} diff --git a/AppleStoreProductKiosk/Network/Core/Logger/URLSessionLogger.swift b/AppleStoreProductKiosk/Network/Core/Logger/URLSessionLogger.swift new file mode 100644 index 0000000..f1f8614 --- /dev/null +++ b/AppleStoreProductKiosk/Network/Core/Logger/URLSessionLogger.swift @@ -0,0 +1,113 @@ +// +// URLSessionLogger.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation +import LogMacro + +/// URLSession 네트워크 요청/응답을 콘솔에 로그로 출력하는 싱글턴 유틸리티입니다. +/// +/// - DEBUG 모드에서만 동작하며, 요청/응답의 메서드, URL, 헤더, 바디, 에러 등을 보기 좋게 출력합니다. +final class URLSessionLogger { + /// 싱글턴 인스턴스 + static let shared = URLSessionLogger() + + /// 외부에서 직접 생성하지 못하도록 private init + private init() {} + + // MARK: - 요청 로그 + + /// 네트워크 요청 정보를 콘솔에 출력합니다. + /// - Parameter request: 로깅할 URLRequest + @MainActor + func logRequest(_ request: URLRequest) { +#if DEBUG + Log.debug("⎡--------------------- REQUEST ---------------------⎤") + + // HTTP 메서드 + if let method = request.httpMethod { + Log.network("[Method]", method) + } + + // 요청 URL + if let url = request.url?.absoluteString { + Log.network("[URL]", url) + } + + // 헤더 + if let headers = request.allHTTPHeaderFields { + Log.network("[Headers]") + for (key, value) in headers { + Log.network(" \(key): \(value)") + } + } + + // 바디 + if let body = request.httpBody, + let bodyString = String(data: body, encoding: .utf8) { + Log.network("[Body]") + Log.network(" \(bodyString)") + } + + Log.network("⎣------------------ END REQUEST --------------------⎦") +#endif + } + + // MARK: - 응답 로그 + + /// 네트워크 응답 정보를 콘솔에 출력합니다. + /// - Parameters: + /// - data: 응답 데이터 + /// - response: URLResponse 객체 + /// - error: 네트워크 에러 + @MainActor + func logResponse(data: Data?, response: URLResponse?, error: Error?) { +#if DEBUG + Log.network("⎡--------------------- RESPONSE --------------------⎤") + + // 응답 URL + if let url = response?.url?.absoluteString { + Log.network("[URL]", url) + } + + // HTTP 상태 코드 및 헤더 + if let httpResponse = response as? HTTPURLResponse { + Log.network("[Status Code] \(httpResponse.statusCode)") + Log.network("[Response Headers]") + for (key, value) in httpResponse.allHeaderFields { + Log.network(" \(key): \(value)") + } + } + + // 바디 (JSON pretty print 시도, 실패 시 raw 출력) + if let data = data { + do { + let json = try JSONSerialization.jsonObject(with: data, options: []) + let prettyData = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted]) + if let prettyString = String(data: prettyData, encoding: .utf8) { + Log.network("[Body (Map)]") + Log.network(prettyString) + } + } catch { + // JSON 디코딩 실패 시 원본 문자열 출력 + if let rawString = String(data: data, encoding: .utf8) { + Log.network("[Body (Raw)]") + Log.network(rawString) + } else { + Log.network("[Body] Cannot decode data") + } + } + } + + // 에러 + if let error = error { + Log.network("[Error] \(error.localizedDescription)") + } + + Log.network("⎣------------------ END RESPONSE -------------------⎦") +#endif + } +} diff --git a/AppleStoreProductKiosk/Network/Core/Method/HTTPMethod.swift b/AppleStoreProductKiosk/Network/Core/Method/HTTPMethod.swift new file mode 100644 index 0000000..e023d44 --- /dev/null +++ b/AppleStoreProductKiosk/Network/Core/Method/HTTPMethod.swift @@ -0,0 +1,21 @@ +// +// HTTPMethod.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +/// HTTP 요청에서 사용되는 메서드 타입을 정의한 enum입니다. +enum HTTPMethod: String { + /// GET: 데이터 조회 + case get = "GET" + /// POST: 데이터 생성 + case post = "POST" + /// PUT: 데이터 전체 수정 + case put = "PUT" + /// DELETE: 데이터 삭제 + case delete = "DELETE" +} + diff --git a/AppleStoreProductKiosk/Network/Core/Provider/AsyncProvider.swift b/AppleStoreProductKiosk/Network/Core/Provider/AsyncProvider.swift new file mode 100644 index 0000000..318e66e --- /dev/null +++ b/AppleStoreProductKiosk/Network/Core/Provider/AsyncProvider.swift @@ -0,0 +1,253 @@ +// +// AsyncProvider.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation +import LogMacro + +/// 개선된 비동기 네트워크 Provider +/// +/// SOLID 원칙을 준수하며 책임을 분리한 네트워크 요청 처리 클래스입니다. +/// - 설정 분리: NetworkConfig +/// - 재시도 전략 분리: RetryStrategy +/// - 응답 처리 분리: ResponseHandler +/// - 재시도 설정 분리: RetryConfig +/// +/// ## 사용 예시: +/// ```swift +/// // 1. 기본 설정 (보수적 재시도 - 500 에러 제외) +/// let provider = AsyncProvider() +/// +/// // 2. 공격적 재시도 (500 에러 포함) +/// let aggressiveProvider = AsyncProvider.aggressive() +/// +/// // 3. 재시도 없음 +/// let noRetryProvider = AsyncProvider.noRetry() +/// +/// // 4. 커스텀 설정 +/// let customProvider = AsyncProvider( +/// retryConfig: .aggressive.addingStatusCodes(408) +/// ) +/// ``` +final class AsyncProvider { + + // MARK: - Properties + + /// 사용할 URLSession + private let session: URLSession + + /// 네트워크 설정 + private let config: NetworkConfig + + /// 재시도 전략 + private let retryStrategy: RetryStrategy + + /// 응답 처리기 + private let responseHandler: ResponseHandler + + // MARK: - Initialization + + /// Provider 초기화 + /// - Parameters: + /// - session: URLSession (기본값: .shared) + /// - config: 네트워크 설정 (기본값: .default) + /// - retryConfig: 재시도 설정 (기본값: .default) + /// - retryStrategy: 재시도 전략 (기본값: nil, retryConfig 기반으로 자동 생성) + /// - responseHandler: 응답 처리기 (기본값: DefaultResponseHandler) + init( + session: URLSession = .shared, + config: NetworkConfig = .default, + retryConfig: RetryConfig = .default, + retryStrategy: RetryStrategy? = nil, + responseHandler: ResponseHandler = DefaultResponseHandler() + ) { + self.session = session + self.config = config + self.retryStrategy = retryStrategy ?? DefaultRetryStrategy(retryConfig: retryConfig) + self.responseHandler = responseHandler + } + + // MARK: - Convenience Initializers + + /// 공격적 재시도 전략으로 Provider 생성 (500 에러도 재시도) + /// - Parameters: + /// - session: URLSession (기본값: .shared) + /// - config: 네트워크 설정 (기본값: .default) + /// - responseHandler: 응답 처리기 (기본값: DefaultResponseHandler) + static func aggressive( + session: URLSession = .shared, + config: NetworkConfig = .default, + responseHandler: ResponseHandler = DefaultResponseHandler() + ) -> AsyncProvider { + return AsyncProvider( + session: session, + config: config, + retryConfig: .aggressive, + responseHandler: responseHandler + ) + } + + /// 보수적 재시도 전략으로 Provider 생성 (500 에러 제외) + /// - Parameters: + /// - session: URLSession (기본값: .shared) + /// - config: 네트워크 설정 (기본값: .default) + /// - responseHandler: 응답 처리기 (기본값: DefaultResponseHandler) + static func conservative( + session: URLSession = .shared, + config: NetworkConfig = .default, + responseHandler: ResponseHandler = DefaultResponseHandler() + ) -> AsyncProvider { + return AsyncProvider( + session: session, + config: config, + retryConfig: .conservative, + responseHandler: responseHandler + ) + } + + /// 최소 재시도 전략으로 Provider 생성 + /// - Parameters: + /// - session: URLSession (기본값: .shared) + /// - config: 네트워크 설정 (기본값: .default) + /// - responseHandler: 응답 처리기 (기본값: DefaultResponseHandler) + static func minimal( + session: URLSession = .shared, + config: NetworkConfig = .default, + responseHandler: ResponseHandler = DefaultResponseHandler() + ) -> AsyncProvider { + return AsyncProvider( + session: session, + config: config, + retryConfig: .minimal, + responseHandler: responseHandler + ) + } + + /// 재시도 없는 Provider 생성 + /// - Parameters: + /// - session: URLSession (기본값: .shared) + /// - config: 네트워크 설정 (기본값: .default) + /// - responseHandler: 응답 처리기 (기본값: DefaultResponseHandler) + static func noRetry( + session: URLSession = .shared, + config: NetworkConfig = .default, + responseHandler: ResponseHandler = DefaultResponseHandler() + ) -> AsyncProvider { + return AsyncProvider( + session: session, + config: config, + retryConfig: .none, + responseHandler: responseHandler + ) + } + + // MARK: - Public Methods + + /// 비동기 네트워크 요청을 실행하고 응답을 디코딩합니다. + /// + /// - Parameters: + /// - target: API 요청 정보를 담은 TargetType + /// - type: 디코딩할 Decodable 타입 + /// - Returns: 디코딩된 객체 + /// - Throws: 네트워크/디코딩/상태코드 에러 등 + func requestAsync( + _ target: T, + decodeTo type: D.Type + ) async throws -> D { + let request = URLRequestBuilder.buildRequest(from: target) + + // 요청 로깅 + await URLSessionLogger.shared.logRequest(request) + + return try await executeWithRetry( + request: request, + decodeTo: type, + attemptCount: 0 + ) + } + + // MARK: - Private Methods + + /// 재시도 로직을 포함한 네트워크 요청 실행 + private func executeWithRetry( + request: URLRequest, + decodeTo type: D.Type, + attemptCount: Int + ) async throws -> D { + do { + // 네트워크 요청 실행 + let (data, response) = try await session.data(for: request) + + // 응답 로깅 + await URLSessionLogger.shared.logResponse( + data: data, + response: response, + error: nil + ) + + // 응답 처리 및 디코딩 + return try responseHandler.handle(data: data, response: response) + + } catch { + // 에러 로깅 + await URLSessionLogger.shared.logResponse( + data: nil, + response: nil, + error: error + ) + + Log.error("Network request failed (attempt \(attemptCount + 1)): \(error.localizedDescription)") + + // 재시도 여부 판단 + let statusCode = extractStatusCode(from: error) + let shouldRetry = retryStrategy.shouldRetry( + statusCode: statusCode, + error: error, + attemptCount: attemptCount, + maxRetryCount: config.maxRetryCount + ) + + if shouldRetry { + // 재시도 대기 + let delay = retryStrategy.delayForRetry( + attemptCount: attemptCount, + baseDelay: config.baseRetryDelay + ) + + Log.info("Retrying request after \(delay) seconds (attempt \(attemptCount + 2))") + try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) + + // 재귀 호출로 재시도 + return try await executeWithRetry( + request: request, + decodeTo: type, + attemptCount: attemptCount + 1 + ) + } else { + // 재시도 불가능한 경우 원래 에러 던지기 + Log.error("Request failed permanently after \(attemptCount + 1) attempts") + throw error + } + } + } + + /// 에러에서 상태 코드 추출 (있는 경우) + private func extractStatusCode(from error: Error) -> Int? { + if let dataError = error as? DataError { + switch dataError { + case .unhandledStatusCode(let code): + return code + default: + return nil + } + } + return nil + } +} + +// MARK: - Sendable Conformance + +extension AsyncProvider: @unchecked Sendable {} \ No newline at end of file diff --git a/AppleStoreProductKiosk/Network/Core/Provider/NetworkConfig.swift b/AppleStoreProductKiosk/Network/Core/Provider/NetworkConfig.swift new file mode 100644 index 0000000..f0f5c1a --- /dev/null +++ b/AppleStoreProductKiosk/Network/Core/Provider/NetworkConfig.swift @@ -0,0 +1,45 @@ +// +// NetworkConfig.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +// MARK: - Network Configuration + +/// 네트워크 관련 설정을 관리하는 구조체 +/// +/// 하드코딩된 설정값들을 분리하여 유연성과 테스트 용이성을 제공합니다. +struct NetworkConfig { + /// 최대 재시도 횟수 + let maxRetryCount: Int + + /// 기본 재시도 간격(초) - Exponential Backoff의 기본값 + let baseRetryDelay: TimeInterval + + /// 요청 타임아웃 시간(초) + let timeoutInterval: TimeInterval + + /// 기본 설정값으로 초기화 + static let `default` = NetworkConfig( + maxRetryCount: 3, + baseRetryDelay: 1.0, + timeoutInterval: 30.0 + ) + + /// 개발/테스트용 빠른 설정 + static let fastConfig = NetworkConfig( + maxRetryCount: 2, + baseRetryDelay: 0.5, + timeoutInterval: 10.0 + ) + + /// 프로덕션용 안정적 설정 + static let productionConfig = NetworkConfig( + maxRetryCount: 5, + baseRetryDelay: 2.0, + timeoutInterval: 60.0 + ) +} \ No newline at end of file diff --git a/AppleStoreProductKiosk/Network/Core/Provider/ResponseHandler.swift b/AppleStoreProductKiosk/Network/Core/Provider/ResponseHandler.swift new file mode 100644 index 0000000..ec3ce0c --- /dev/null +++ b/AppleStoreProductKiosk/Network/Core/Provider/ResponseHandler.swift @@ -0,0 +1,153 @@ +// +// ResponseHandler.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation +import LogMacro + +// MARK: - Response Handler Protocol + +/// HTTP 응답 처리를 담당하는 프로토콜 +/// +/// 상태 코드 검증, 데이터 디코딩 등의 응답 처리 책임을 분리합니다. +protocol ResponseHandler { + /// HTTP 응답을 처리하고 디코딩합니다 + /// - Parameters: + /// - data: 응답 데이터 + /// - response: URLResponse + /// - Returns: 디코딩된 객체 + /// - Throws: 상태 코드, 디코딩 에러 등 + func handle( + data: Data, + response: URLResponse + ) throws -> T +} + +// MARK: - Default Response Handler + +/// 기본 응답 처리 구현체 +struct DefaultResponseHandler: ResponseHandler { + + func handle( + data: Data, + response: URLResponse + ) throws -> T { + guard let httpResponse = response as? HTTPURLResponse else { + Log.error("No HTTP response received") + throw DataError.noData + } + + let statusCode = httpResponse.statusCode + Log.debug("Received HTTP status code: \(statusCode)") + + // 상태 코드별 처리 + switch statusCode { + case 200...299: + return try decodeSuccessResponse(data: data, type: T.self) + + case 400: + Log.error("Bad Request (400) for URL: \(response.url?.absoluteString ?? "No URL")") + throw DataError.customError("Bad Request (400)") + + case 401: + Log.error("Unauthorized (401) - Invalid credentials") + throw DataError.customError("Unauthorized (401)") + + case 403: + Log.error("Forbidden (403) - Access denied") + throw DataError.customError("Forbidden (403)") + + case 404: + Log.error("Not Found (404) for URL: \(response.url?.absoluteString ?? "No URL")") + throw DataError.customError("Not Found (404)") + + case 429: + Log.error("Too Many Requests (429) - Rate limited") + throw DataError.customError("Too Many Requests (429)") + + case 500...599: + Log.error("Server Error (\(statusCode))") + throw DataError.unhandledStatusCode(statusCode) + + default: + Log.error("Unhandled status code: \(statusCode)") + throw DataError.unhandledStatusCode(statusCode) + } + } + + // MARK: - Private Methods + + /// 성공 응답 디코딩 + private func decodeSuccessResponse( + data: Data, + type: T.Type + ) throws -> T { + do { + let decodedData = try data.decoded(as: T.self) + Log.debug("Successfully decoded response as \(T.self)") + return decodedData + } catch { + Log.error("Failed to decode response: \(error.localizedDescription)") + throw DataError.decodingError(error) + } + } +} + +// MARK: - Lenient Response Handler + +/// 관대한 응답 처리 구현체 (500 에러에서도 디코딩 시도) +struct LenientResponseHandler: ResponseHandler { + + func handle( + data: Data, + response: URLResponse + ) throws -> T { + guard let httpResponse = response as? HTTPURLResponse else { + Log.error("No HTTP response received") + throw DataError.noData + } + + let statusCode = httpResponse.statusCode + + switch statusCode { + case 200...299: + return try decodeResponse(data: data, type: T.self) + + case 400...499: + Log.error("Client Error (\(statusCode))") + throw DataError.unhandledStatusCode(statusCode) + + case 500...599: + Log.error("Server Error (\(statusCode)), attempting to decode response") + // 500 에러에서도 디코딩 시도 (서버가 에러와 함께 데이터를 보낼 수 있음) + if let decodedData = try? decodeResponse(data: data, type: T.self) { + Log.debug("Successfully decoded response despite server error") + return decodedData + } else { + Log.error("Failed to decode server error response") + throw DataError.unhandledStatusCode(statusCode) + } + + default: + Log.error("Unhandled status code: \(statusCode)") + throw DataError.unhandledStatusCode(statusCode) + } + } + + // MARK: - Private Methods + + private func decodeResponse( + data: Data, + type: T.Type + ) throws -> T { + do { + return try data.decoded(as: T.self) + } catch { + Log.error("Decoding failed: \(error.localizedDescription)") + throw DataError.decodingError(error) + } + } +} \ No newline at end of file diff --git a/AppleStoreProductKiosk/Network/Core/Provider/RetryConfig.swift b/AppleStoreProductKiosk/Network/Core/Provider/RetryConfig.swift new file mode 100644 index 0000000..9276ddf --- /dev/null +++ b/AppleStoreProductKiosk/Network/Core/Provider/RetryConfig.swift @@ -0,0 +1,142 @@ +// +// RetryConfig.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +// MARK: - Retry Configuration + +/// 재시도 관련 설정을 관리하는 구조체 +/// +/// 어떤 상태 코드와 에러에서 재시도할지 설정 가능합니다. +struct RetryConfig { + /// 재시도 가능한 HTTP 상태 코드들 + let retryableStatusCodes: Set + + /// 재시도 가능한 URLError 코드들 + let retryableURLErrorCodes: Set + + /// 커스텀 초기화 + /// - Parameters: + /// - retryableStatusCodes: 재시도할 HTTP 상태 코드 집합 + /// - retryableURLErrorCodes: 재시도할 URLError 코드 집합 + init( + retryableStatusCodes: Set = [], + retryableURLErrorCodes: Set = [] + ) { + self.retryableStatusCodes = retryableStatusCodes + self.retryableURLErrorCodes = retryableURLErrorCodes + } +} + +// MARK: - Predefined Configurations + +extension RetryConfig { + + /// 공격적 재시도 설정 (500 포함) + /// - 일시적 문제일 가능성이 있는 모든 에러에서 재시도 + /// - 개발/테스트 환경에 적합 + static let aggressive = RetryConfig( + retryableStatusCodes: [ + 500, // Internal Server Error - 서버 내부 오류 + 502, // Bad Gateway - 게이트웨이 오류 + 503, // Service Unavailable - 서비스 이용 불가 + 504, // Gateway Timeout - 게이트웨이 타임아웃 + 429 // Too Many Requests - 요청 과다 + ], + retryableURLErrorCodes: [ + .timedOut, // 타임아웃 + .networkConnectionLost, // 네트워크 연결 끊김 + .notConnectedToInternet, // 인터넷 연결 없음 + .cannotConnectToHost // 호스트 연결 불가 + ] + ) + + /// 보수적 재시도 설정 (500 제외) + /// - 명확히 일시적인 문제로 판단되는 경우만 재시도 + /// - 프로덕션 환경에 적합 + static let conservative = RetryConfig( + retryableStatusCodes: [ + 502, // Bad Gateway - 게이트웨이 문제 + 503, // Service Unavailable - 서버 과부하/점검 + 504, // Gateway Timeout - 게이트웨이 타임아웃 + 429 // Too Many Requests - Rate limiting + ], + retryableURLErrorCodes: [ + .timedOut, // 타임아웃만 + .networkConnectionLost // 네트워크 연결 끊김만 + ] + ) + + /// 최소 재시도 설정 + /// - 네트워크 타임아웃과 서비스 이용 불가만 재시도 + /// - 매우 안정적인 서비스에 적합 + static let minimal = RetryConfig( + retryableStatusCodes: [ + 503, // Service Unavailable만 + 429 // Rate limiting만 + ], + retryableURLErrorCodes: [ + .timedOut // 타임아웃만 + ] + ) + + /// 재시도 없음 + /// - 모든 에러에서 즉시 실패 + /// - 디버깅이나 특수한 경우에 사용 + static let none = RetryConfig( + retryableStatusCodes: [], + retryableURLErrorCodes: [] + ) + + /// 기본 설정 (보수적 설정 사용) + static let `default` = conservative +} + +// MARK: - Convenience Methods + +extension RetryConfig { + + /// 주어진 상태 코드가 재시도 가능한지 확인 + /// - Parameter statusCode: HTTP 상태 코드 + /// - Returns: 재시도 가능 여부 + func shouldRetryStatusCode(_ statusCode: Int) -> Bool { + return retryableStatusCodes.contains(statusCode) + } + + /// 주어진 URLError가 재시도 가능한지 확인 + /// - Parameter error: URLError + /// - Returns: 재시도 가능 여부 + func shouldRetryURLError(_ error: URLError) -> Bool { + return retryableURLErrorCodes.contains(error.code) + } + + /// 재시도 가능한 상태 코드를 추가한 새로운 설정 반환 + /// - Parameter statusCodes: 추가할 상태 코드들 + /// - Returns: 새로운 RetryConfig 인스턴스 + func addingStatusCodes(_ statusCodes: Int...) -> RetryConfig { + var newStatusCodes = retryableStatusCodes + statusCodes.forEach { newStatusCodes.insert($0) } + + return RetryConfig( + retryableStatusCodes: newStatusCodes, + retryableURLErrorCodes: retryableURLErrorCodes + ) + } + + /// 재시도 가능한 상태 코드를 제거한 새로운 설정 반환 + /// - Parameter statusCodes: 제거할 상태 코드들 + /// - Returns: 새로운 RetryConfig 인스턴스 + func removingStatusCodes(_ statusCodes: Int...) -> RetryConfig { + var newStatusCodes = retryableStatusCodes + statusCodes.forEach { newStatusCodes.remove($0) } + + return RetryConfig( + retryableStatusCodes: newStatusCodes, + retryableURLErrorCodes: retryableURLErrorCodes + ) + } +} \ No newline at end of file diff --git a/AppleStoreProductKiosk/Network/Core/Provider/RetryStrategy.swift b/AppleStoreProductKiosk/Network/Core/Provider/RetryStrategy.swift new file mode 100644 index 0000000..3750c46 --- /dev/null +++ b/AppleStoreProductKiosk/Network/Core/Provider/RetryStrategy.swift @@ -0,0 +1,132 @@ +// +// RetryStrategy.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +// MARK: - Retry Strategy Protocol + +/// 재시도 전략을 정의하는 프로토콜 +/// +/// 언제 재시도할지와 얼마나 대기할지를 결정하는 책임을 분리합니다. +protocol RetryStrategy { + /// 주어진 상황에서 재시도를 해야 하는지 판단 + /// - Parameters: + /// - statusCode: HTTP 상태 코드 + /// - error: 발생한 에러 (옵셔널) + /// - attemptCount: 현재 시도 횟수 + /// - maxRetryCount: 최대 재시도 횟수 + /// - Returns: 재시도 여부 + func shouldRetry( + statusCode: Int?, + error: Error?, + attemptCount: Int, + maxRetryCount: Int + ) -> Bool + + /// 재시도 전 대기 시간을 계산 + /// - Parameters: + /// - attemptCount: 현재 시도 횟수 + /// - baseDelay: 기본 대기 시간 + /// - Returns: 실제 대기 시간 + func delayForRetry(attemptCount: Int, baseDelay: TimeInterval) -> TimeInterval +} + +// MARK: - Default Retry Strategy + +/// 기본 재시도 전략 구현체 +struct DefaultRetryStrategy: RetryStrategy { + + /// 재시도 설정 + private let retryConfig: RetryConfig + + /// 초기화 + /// - Parameter retryConfig: 재시도 설정 (기본값: .default) + init(retryConfig: RetryConfig = .default) { + self.retryConfig = retryConfig + } + + func shouldRetry( + statusCode: Int?, + error: Error?, + attemptCount: Int, + maxRetryCount: Int + ) -> Bool { + // 최대 재시도 횟수 초과 체크 + guard attemptCount < maxRetryCount else { return false } + + // 상태 코드 기반 재시도 판단 + if let statusCode = statusCode { + return retryConfig.shouldRetryStatusCode(statusCode) + } + + // 네트워크 에러 기반 재시도 판단 + if let error = error { + return shouldRetryForError(error) + } + + return false + } + + func delayForRetry(attemptCount: Int, baseDelay: TimeInterval) -> TimeInterval { + // Exponential Backoff: 2^attemptCount * baseDelay + let exponentialDelay = pow(2.0, Double(attemptCount)) * baseDelay + + // 최대 30초로 제한 (무한 증가 방지) + return min(exponentialDelay, 30.0) + } + + // MARK: - Private Methods + + /// 에러 타입별 재시도 가능 여부 판단 + private func shouldRetryForError(_ error: Error) -> Bool { + if let urlError = error as? URLError { + return retryConfig.shouldRetryURLError(urlError) + } + + return false + } +} + +// MARK: - Conservative Retry Strategy + +/// 보수적 재시도 전략 (최소한의 재시도만) +struct ConservativeRetryStrategy: RetryStrategy { + + /// 재시도 설정 + private let retryConfig: RetryConfig + + /// 초기화 + /// - Parameter retryConfig: 재시도 설정 (기본값: .conservative) + init(retryConfig: RetryConfig = .conservative) { + self.retryConfig = retryConfig + } + + func shouldRetry( + statusCode: Int?, + error: Error?, + attemptCount: Int, + maxRetryCount: Int + ) -> Bool { + guard attemptCount < maxRetryCount else { return false } + + // RetryConfig 기반 재시도 판단 + if let statusCode = statusCode { + return retryConfig.shouldRetryStatusCode(statusCode) + } + + if let error = error, let urlError = error as? URLError { + return retryConfig.shouldRetryURLError(urlError) + } + + return false + } + + func delayForRetry(attemptCount: Int, baseDelay: TimeInterval) -> TimeInterval { + // 고정된 대기 시간 (Exponential 없음) + return baseDelay + } +} \ No newline at end of file diff --git a/AppleStoreProductKiosk/Network/Core/TargeType/BaseTargetType.swift b/AppleStoreProductKiosk/Network/Core/TargeType/BaseTargetType.swift new file mode 100644 index 0000000..3948861 --- /dev/null +++ b/AppleStoreProductKiosk/Network/Core/TargeType/BaseTargetType.swift @@ -0,0 +1,57 @@ +// +// BaseTargetType.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +/// API 타겟의 공통 속성을 정의하는 프로토콜입니다. +/// +/// - 도메인, URL 경로, 파라미터 등 API 요청에 필요한 정보를 제공합니다. +protocol BaseTargetType: TargetType { + /// API 엔드포인트 + var endpoint: any APIEndpoint { get } + /// 요청 파라미터 + var parameters:[String: Any]? { get } +} + +// MARK: - BaseTargetType 기본 구현 + + +extension BaseTargetType { + /// API의 baseURL + var baseURL: URL { + return URL(string: endpoint.baseURL)! + } + + /// 전체 path + var path: String { + return endpoint.fullPath + } + + /// 기본 헤더 (인증 없는 헤더 사용) + var headers: [String : String]? { + return APIHeader.baseHeader + } + + /// 네트워크 요청 Task (파라미터 유무/메서드에 따라 인코딩 방식 분기) + var task: NetworkTask { + if let parameters = parameters { + if method == .get { + return .requestParameters( + parameters: parameters, + encoding: .url + ) + } else { + return .requestParameters( + parameters: parameters, + encoding: .json + ) + } + } else { + return .requestPlain + } + } +} diff --git a/AppleStoreProductKiosk/Network/Core/TargeType/CustomParameterEncoding.swift b/AppleStoreProductKiosk/Network/Core/TargeType/CustomParameterEncoding.swift new file mode 100644 index 0000000..454d161 --- /dev/null +++ b/AppleStoreProductKiosk/Network/Core/TargeType/CustomParameterEncoding.swift @@ -0,0 +1,48 @@ +// +// CustomParameterEncoding.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +/// URLRequest의 파라미터 인코딩 방식을 정의하는 enum입니다. +/// +/// - url: 쿼리 파라미터로 인코딩 (GET 등) +/// - json: HTTP Body에 JSON으로 인코딩 (POST, PUT 등) +enum CustomParameterEncoding { + case url + case json + + /// 파라미터를 주어진 URLRequest에 인코딩하여 반환합니다. + /// + /// - Parameters: + /// - request: 인코딩할 URLRequest + /// - parameters: 인코딩할 파라미터 딕셔너리 + /// - Returns: 파라미터가 인코딩된 URLRequest + /// - Throws: 인코딩 실패 시 에러(DataError) + func encode(_ request: URLRequest, with parameters: [String: Any]) throws -> URLRequest { + var request = request + switch self { + case .url: + // URL 쿼리 파라미터로 인코딩 + guard let url = request.url else { + throw DataError.customError("Invalid URL") + } + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) + components?.queryItems = parameters + .map { URLQueryItem(name: $0.key, value: "\($0.value)") } + request.url = components?.url + case .json: + // HTTP Body에 JSON으로 인코딩 + let jsonData = try JSONSerialization.data( + withJSONObject: parameters, + options: [] + ) + request.httpBody = jsonData + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + } + return request + } +} diff --git a/AppleStoreProductKiosk/Network/Core/TargeType/NetworkTask.swift b/AppleStoreProductKiosk/Network/Core/TargeType/NetworkTask.swift new file mode 100644 index 0000000..e91a7d4 --- /dev/null +++ b/AppleStoreProductKiosk/Network/Core/TargeType/NetworkTask.swift @@ -0,0 +1,28 @@ +// +// NetworkTask.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +/// 네트워크 요청 시 사용할 파라미터 인코딩 및 전송 방식을 정의하는 enum입니다. +enum NetworkTask { + /// 파라미터 없이 단순 요청 (GET, DELETE 등) + case requestPlain + + /// 파라미터와 인코딩 방식이 있는 요청 (주로 GET, POST 등) + case requestParameters( + parameters: [String: Any], + encoding: CustomParameterEncoding + ) + + /// URL 파라미터와 Body 파라미터를 모두 포함하는 복합 요청 + /// (예: 쿼리 + 바디 동시 필요 시) + case requestCompositeParameters( + bodyParameters: [String: Any], + bodyEncoding: CustomParameterEncoding, + urlParameters: [String: Any] + ) +} diff --git a/AppleStoreProductKiosk/Network/Core/TargeType/TargetType.swift b/AppleStoreProductKiosk/Network/Core/TargeType/TargetType.swift new file mode 100644 index 0000000..baa3672 --- /dev/null +++ b/AppleStoreProductKiosk/Network/Core/TargeType/TargetType.swift @@ -0,0 +1,30 @@ +// +// TargetType.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + + +/// 네트워크 API 요청을 추상화하는 프로토콜입니다. +/// +/// - 각 API 엔드포인트별로 이 프로토콜을 채택한 enum/struct를 만들면 +/// 공통적인 네트워크 요청 빌드가 가능합니다. +protocol TargetType { + /// 기본 도메인 URL (예: https://api.example.com) + var baseURL: URL { get } + + /// 엔드포인트 경로 (예: "/books/search") + var path: String { get } + + /// HTTP 메서드 (GET, POST 등) + var method: HTTPMethod { get } + + /// HTTP 헤더 (필요 시) + var headers: [String: String]? { get } + + /// 파라미터 및 인코딩 방식 등 요청 작업 + var task: NetworkTask { get } +} diff --git a/AppleStoreProductKiosk/Network/Core/URLRequestBuilder/URLRequestBuilder.swift b/AppleStoreProductKiosk/Network/Core/URLRequestBuilder/URLRequestBuilder.swift new file mode 100644 index 0000000..71ccd02 --- /dev/null +++ b/AppleStoreProductKiosk/Network/Core/URLRequestBuilder/URLRequestBuilder.swift @@ -0,0 +1,65 @@ +// +// URLRequestBuilder.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation +import LogMacro + +/// TargetType을 기반으로 URLRequest를 생성하는 빌더 클래스입니다. +/// +/// - 파라미터 인코딩, 헤더 설정, URL 조합 등 네트워크 요청 생성의 모든 과정을 담당합니다. +class URLRequestBuilder { + /// 기본 생성자 + public init() {} + + /// TargetType을 받아 URLRequest를 생성합니다. + /// + /// - Parameter target: 네트워크 요청 정보가 담긴 TargetType + /// - Returns: 완성된 URLRequest + static func buildRequest(from target: TargetType) -> URLRequest { + // URL 생성 시 불필요한 공백을 제거하기 위한 trim 적용 + let url = target.baseURL.appendingPathComponent( + target.path.trimmingCharacters(in: .whitespaces) + ) + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) + var request = URLRequest(url: components?.url ?? url) + request.httpMethod = target.method.rawValue + + // 헤더 추가 + if let headers = target.headers { + headers.forEach { request.setValue($1, forHTTPHeaderField: $0) } + } + + // 파라미터 및 인코딩 처리 + switch target.task { + case .requestParameters(let parameters, let encoding): + do { + request = try encoding.encode(request, with: parameters) + } catch { + Log.error("Failed to encode parameters: %{public}@", error.localizedDescription) + } + case .requestCompositeParameters( + let bodyParameters, + let bodyEncoding, + let urlParameters + ): + do { + components?.queryItems = urlParameters + .map { URLQueryItem(name: $0.key, value: "\($0.value)") } + request.url = components?.url + request = try bodyEncoding.encode(request, with: bodyParameters) + } catch { + Log.error("Failed to encode composite parameters: %{public}@", error.localizedDescription) + } + case .requestPlain: + // 파라미터 없이 단순 요청 + components?.queryItems = nil + request.url = components?.url + } + + return request + } +} diff --git a/AppleStoreProductKiosk/Network/Core/header/APIHeader.swift b/AppleStoreProductKiosk/Network/Core/header/APIHeader.swift new file mode 100644 index 0000000..b532f34 --- /dev/null +++ b/AppleStoreProductKiosk/Network/Core/header/APIHeader.swift @@ -0,0 +1,27 @@ +// +// APIHeader.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +// API 요청에 사용되는 공통 헤더 키를 정의하는 구조체입니다. +enum APIHeader { + /// Content-Type 헤더 키 + static let contentType = "Content-Type" + +} + + +extension APIHeader { + + /// 인증 토큰이 필요 없는 기본 헤더 + public static var baseHeader: [String: String] { + [ + contentType: APIHeaderManger.contentType + ] + } + +} diff --git a/AppleStoreProductKiosk/Network/Core/header/APIHeaderManger.swift b/AppleStoreProductKiosk/Network/Core/header/APIHeaderManger.swift new file mode 100644 index 0000000..2daed58 --- /dev/null +++ b/AppleStoreProductKiosk/Network/Core/header/APIHeaderManger.swift @@ -0,0 +1,16 @@ +// +// APIHeaderManger.swift +// AppleStoreProductKiosk +// +// Created by Wonji Suh on 9/16/25. +// + +import Foundation + +/// API 헤더의 값(컨텐츠 타입 등)을 관리하는 enum입니다. +enum APIHeaderManger { + /// 앱 패키지명(필요 시 사용, 기본값 "-") + static let appPackageName: String = "-" + /// Content-Type 값 (기본: application/json) + static let contentType: String = "application/json" +} From 197bbd437b3dd6a125b6615bf3a42161cfe8cff4 Mon Sep 17 00:00:00 2001 From: Roy Date: Wed, 17 Sep 2025 14:02:40 +0900 Subject: [PATCH 15/18] =?UTF-8?q?=E2=9C=A8[feat]:SERIVCE=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=95=EC=9D=98=20=20=EB=B0=8F=20=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Service/KioskProductService.swift | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/AppleStoreProductKiosk/Network/Service/KioskProductService.swift b/AppleStoreProductKiosk/Network/Service/KioskProductService.swift index 9f176d0..8a15ef4 100644 --- a/AppleStoreProductKiosk/Network/Service/KioskProductService.swift +++ b/AppleStoreProductKiosk/Network/Service/KioskProductService.swift @@ -10,32 +10,27 @@ import Foundation enum KioskProductService { - case proudctList + case getAllProducts } - extension KioskProductService: BaseTargetType { - var domain: KioskProductDomain { - return .producut - } - - var urlPath: String { + var endpoint: any APIEndpoint { switch self { - case .proudctList: - return KioskProductAPI.productLists.description + case .getAllProducts: + return KioskAPI.Product.list } } - + var parameters: [String : Any]? { switch self { - case .proudctList: + case .getAllProducts: return nil } } - + var method: HTTPMethod { switch self { - case .proudctList: + case .getAllProducts: return .get } } From 117018b82c6b558c6edd193c6e73d890aeae462a Mon Sep 17 00:00:00 2001 From: Roy Date: Wed, 17 Sep 2025 14:02:55 +0900 Subject: [PATCH 16/18] =?UTF-8?q?=E2=9C=A8[feat]:=20data=20error=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AppleStoreProductKiosk/Data/Model/Error/DataError.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/AppleStoreProductKiosk/Data/Model/Error/DataError.swift b/AppleStoreProductKiosk/Data/Model/Error/DataError.swift index d3620e1..3e3f6f2 100644 --- a/AppleStoreProductKiosk/Data/Model/Error/DataError.swift +++ b/AppleStoreProductKiosk/Data/Model/Error/DataError.swift @@ -8,7 +8,7 @@ import Foundation /// 네트워크 및 데이터 처리 과정에서 발생할 수 있는 오류를 정의한 enum입니다. -enum DataError: Error, LocalizedError, Sendable, Equatable { +enum DataError: Error, LocalizedError, Sendable { /// 데이터가 없음 case noData @@ -21,6 +21,9 @@ enum DataError: Error, LocalizedError, Sendable, Equatable { /// HTTP 응답 오류 (응답 객체, 에러 메시지) case httpResponseError(HTTPURLResponse, String) + /// 디코딩 에러 + case decodingError(Error) + /// 사용자에게 표시할 에러 메시지 var errorDescription: String? { switch self { @@ -32,6 +35,8 @@ enum DataError: Error, LocalizedError, Sendable, Equatable { return "처리되지 않은 상태 코드: \(code)" case .httpResponseError(let response, let message): return "HTTP \(response.statusCode): \(message)" + case .decodingError(let error): + return "디코딩 실패: \(error.localizedDescription)" } } } From ee16b3202ebaf303f6226272ed5c271b54dfdbac Mon Sep 17 00:00:00 2001 From: Roy Date: Wed, 17 Sep 2025 14:15:34 +0900 Subject: [PATCH 17/18] =?UTF-8?q?=E2=9C=A8[feat]:=ED=8F=B4=EB=8D=94=20?= =?UTF-8?q?=EB=B0=8F=20=20url=20=EA=B2=BD=EB=A1=9C=20=EC=95=88=EC=A0=95?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{TargeType => TargetType}/BaseTargetType.swift | 1 + .../CustomParameterEncoding.swift | 1 + .../Core/{TargeType => TargetType}/NetworkTask.swift | 1 + .../Core/{TargeType => TargetType}/TargetType.swift | 1 + .../Core/URLRequestBuilder/URLRequestBuilder.swift | 11 +++++------ .../Network/Core/header/APIHeader.swift | 2 +- .../{APIHeaderManger.swift => APIHeaderManager.swift} | 4 ++-- 7 files changed, 12 insertions(+), 9 deletions(-) rename AppleStoreProductKiosk/Network/Core/{TargeType => TargetType}/BaseTargetType.swift (99%) rename AppleStoreProductKiosk/Network/Core/{TargeType => TargetType}/CustomParameterEncoding.swift (99%) rename AppleStoreProductKiosk/Network/Core/{TargeType => TargetType}/NetworkTask.swift (99%) rename AppleStoreProductKiosk/Network/Core/{TargeType => TargetType}/TargetType.swift (99%) rename AppleStoreProductKiosk/Network/Core/header/{APIHeaderManger.swift => APIHeaderManager.swift} (88%) diff --git a/AppleStoreProductKiosk/Network/Core/TargeType/BaseTargetType.swift b/AppleStoreProductKiosk/Network/Core/TargetType/BaseTargetType.swift similarity index 99% rename from AppleStoreProductKiosk/Network/Core/TargeType/BaseTargetType.swift rename to AppleStoreProductKiosk/Network/Core/TargetType/BaseTargetType.swift index 3948861..29836af 100644 --- a/AppleStoreProductKiosk/Network/Core/TargeType/BaseTargetType.swift +++ b/AppleStoreProductKiosk/Network/Core/TargetType/BaseTargetType.swift @@ -55,3 +55,4 @@ extension BaseTargetType { } } } + diff --git a/AppleStoreProductKiosk/Network/Core/TargeType/CustomParameterEncoding.swift b/AppleStoreProductKiosk/Network/Core/TargetType/CustomParameterEncoding.swift similarity index 99% rename from AppleStoreProductKiosk/Network/Core/TargeType/CustomParameterEncoding.swift rename to AppleStoreProductKiosk/Network/Core/TargetType/CustomParameterEncoding.swift index 454d161..c4e6c58 100644 --- a/AppleStoreProductKiosk/Network/Core/TargeType/CustomParameterEncoding.swift +++ b/AppleStoreProductKiosk/Network/Core/TargetType/CustomParameterEncoding.swift @@ -46,3 +46,4 @@ enum CustomParameterEncoding { return request } } + diff --git a/AppleStoreProductKiosk/Network/Core/TargeType/NetworkTask.swift b/AppleStoreProductKiosk/Network/Core/TargetType/NetworkTask.swift similarity index 99% rename from AppleStoreProductKiosk/Network/Core/TargeType/NetworkTask.swift rename to AppleStoreProductKiosk/Network/Core/TargetType/NetworkTask.swift index e91a7d4..3327169 100644 --- a/AppleStoreProductKiosk/Network/Core/TargeType/NetworkTask.swift +++ b/AppleStoreProductKiosk/Network/Core/TargetType/NetworkTask.swift @@ -26,3 +26,4 @@ enum NetworkTask { urlParameters: [String: Any] ) } + diff --git a/AppleStoreProductKiosk/Network/Core/TargeType/TargetType.swift b/AppleStoreProductKiosk/Network/Core/TargetType/TargetType.swift similarity index 99% rename from AppleStoreProductKiosk/Network/Core/TargeType/TargetType.swift rename to AppleStoreProductKiosk/Network/Core/TargetType/TargetType.swift index baa3672..83a9b95 100644 --- a/AppleStoreProductKiosk/Network/Core/TargeType/TargetType.swift +++ b/AppleStoreProductKiosk/Network/Core/TargetType/TargetType.swift @@ -28,3 +28,4 @@ protocol TargetType { /// 파라미터 및 인코딩 방식 등 요청 작업 var task: NetworkTask { get } } + diff --git a/AppleStoreProductKiosk/Network/Core/URLRequestBuilder/URLRequestBuilder.swift b/AppleStoreProductKiosk/Network/Core/URLRequestBuilder/URLRequestBuilder.swift index 71ccd02..5b1dc2b 100644 --- a/AppleStoreProductKiosk/Network/Core/URLRequestBuilder/URLRequestBuilder.swift +++ b/AppleStoreProductKiosk/Network/Core/URLRequestBuilder/URLRequestBuilder.swift @@ -20,12 +20,11 @@ class URLRequestBuilder { /// - Parameter target: 네트워크 요청 정보가 담긴 TargetType /// - Returns: 완성된 URLRequest static func buildRequest(from target: TargetType) -> URLRequest { - // URL 생성 시 불필요한 공백을 제거하기 위한 trim 적용 - let url = target.baseURL.appendingPathComponent( - target.path.trimmingCharacters(in: .whitespaces) - ) - var components = URLComponents(url: url, resolvingAgainstBaseURL: false) - var request = URLRequest(url: components?.url ?? url) + // Base URL과 path를 안전하게 결합 (슬래시 이스케이프 이슈 방지) + let base = target.baseURL + var components = URLComponents(url: base, resolvingAgainstBaseURL: false) + components?.path = target.path.trimmingCharacters(in: .whitespaces) + var request = URLRequest(url: components?.url ?? base) request.httpMethod = target.method.rawValue // 헤더 추가 diff --git a/AppleStoreProductKiosk/Network/Core/header/APIHeader.swift b/AppleStoreProductKiosk/Network/Core/header/APIHeader.swift index b532f34..16239d8 100644 --- a/AppleStoreProductKiosk/Network/Core/header/APIHeader.swift +++ b/AppleStoreProductKiosk/Network/Core/header/APIHeader.swift @@ -20,7 +20,7 @@ extension APIHeader { /// 인증 토큰이 필요 없는 기본 헤더 public static var baseHeader: [String: String] { [ - contentType: APIHeaderManger.contentType + contentType: APIHeaderManager.contentType ] } diff --git a/AppleStoreProductKiosk/Network/Core/header/APIHeaderManger.swift b/AppleStoreProductKiosk/Network/Core/header/APIHeaderManager.swift similarity index 88% rename from AppleStoreProductKiosk/Network/Core/header/APIHeaderManger.swift rename to AppleStoreProductKiosk/Network/Core/header/APIHeaderManager.swift index 2daed58..613c875 100644 --- a/AppleStoreProductKiosk/Network/Core/header/APIHeaderManger.swift +++ b/AppleStoreProductKiosk/Network/Core/header/APIHeaderManager.swift @@ -1,5 +1,5 @@ // -// APIHeaderManger.swift +// APIHeaderManager.swift // AppleStoreProductKiosk // // Created by Wonji Suh on 9/16/25. @@ -8,7 +8,7 @@ import Foundation /// API 헤더의 값(컨텐츠 타입 등)을 관리하는 enum입니다. -enum APIHeaderManger { +enum APIHeaderManager { /// 앱 패키지명(필요 시 사용, 기본값 "-") static let appPackageName: String = "-" /// Content-Type 값 (기본: application/json) From d1cdbd6bdf2e375c13f927be48b0c6c885c77797 Mon Sep 17 00:00:00 2001 From: Roy Date: Wed, 17 Sep 2025 14:20:45 +0900 Subject: [PATCH 18/18] =?UTF-8?q?=E2=9C=A8[feat]:=ED=8F=B4=EB=8D=94=20?= =?UTF-8?q?=EB=B0=8F=20=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{Data/Model/Error => Network/Core/Errors}/DataError.swift | 1 + 1 file changed, 1 insertion(+) rename AppleStoreProductKiosk/{Data/Model/Error => Network/Core/Errors}/DataError.swift (93%) diff --git a/AppleStoreProductKiosk/Data/Model/Error/DataError.swift b/AppleStoreProductKiosk/Network/Core/Errors/DataError.swift similarity index 93% rename from AppleStoreProductKiosk/Data/Model/Error/DataError.swift rename to AppleStoreProductKiosk/Network/Core/Errors/DataError.swift index 3e3f6f2..37f8ea5 100644 --- a/AppleStoreProductKiosk/Data/Model/Error/DataError.swift +++ b/AppleStoreProductKiosk/Network/Core/Errors/DataError.swift @@ -8,6 +8,7 @@ import Foundation /// 네트워크 및 데이터 처리 과정에서 발생할 수 있는 오류를 정의한 enum입니다. +// NOTE: Moved to Network/Core/Errors to avoid Infra -> Data reverse dependency. enum DataError: Error, LocalizedError, Sendable { /// 데이터가 없음 case noData