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/Data/Model/Error/DataError.swift b/AppleStoreProductKiosk/Data/Model/Error/DataError.swift new file mode 100644 index 0000000..d3620e1 --- /dev/null +++ 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)" + } + } +} diff --git a/AppleStoreProductKiosk/Network/API/BaseAPI.swift b/AppleStoreProductKiosk/Network/API/BaseAPI.swift new file mode 100644 index 0000000..75cc65b --- /dev/null +++ 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 new file mode 100644 index 0000000..f16f29c --- /dev/null +++ b/AppleStoreProductKiosk/Network/API/KioskProductAPI.swift @@ -0,0 +1,19 @@ +// +// 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 new file mode 100644 index 0000000..b135235 --- /dev/null +++ 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" + } + } +} 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..0b3d1ab --- /dev/null +++ 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) + } +} diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/Logger/URLSessionLogger.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/Logger/URLSessionLogger.swift new file mode 100644 index 0000000..f1f8614 --- /dev/null +++ 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 + } +} diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/Method/HTTPMethod.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/Method/HTTPMethod.swift new file mode 100644 index 0000000..e023d44 --- /dev/null +++ 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" +} + diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/Provider/AsyncProvider.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/Provider/AsyncProvider.swift new file mode 100644 index 0000000..fc4fcad --- /dev/null +++ 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 {} diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/BaseTargetType.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/TargeType/BaseTargetType.swift new file mode 100644 index 0000000..4df56b6 --- /dev/null +++ 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 new file mode 100644 index 0000000..454d161 --- /dev/null +++ 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 new file mode 100644 index 0000000..e91a7d4 --- /dev/null +++ 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 new file mode 100644 index 0000000..baa3672 --- /dev/null +++ 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 } +} diff --git a/AppleStoreProductKiosk/Network/Common/NetworkManger/URLRequestBuilder/URLRequestBuilder.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/URLRequestBuilder/URLRequestBuilder.swift new file mode 100644 index 0000000..a0edac0 --- /dev/null +++ b/AppleStoreProductKiosk/Network/Common/NetworkManger/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/Common/NetworkManger/header/APIHeader.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeader.swift new file mode 100644 index 0000000..b532f34 --- /dev/null +++ b/AppleStoreProductKiosk/Network/Common/NetworkManger/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/Common/NetworkManger/header/APIHeaderManger.swift b/AppleStoreProductKiosk/Network/Common/NetworkManger/header/APIHeaderManger.swift new file mode 100644 index 0000000..2daed58 --- /dev/null +++ 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" +} diff --git a/AppleStoreProductKiosk/Network/Service/KioskProductService.swift b/AppleStoreProductKiosk/Network/Service/KioskProductService.swift new file mode 100644 index 0000000..9f176d0 --- /dev/null +++ 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 + } + } +} 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 + + + +