diff --git a/APIService/Package.swift b/APIService/Package.swift index cbe9406..db73612 100644 --- a/APIService/Package.swift +++ b/APIService/Package.swift @@ -14,6 +14,14 @@ let package = Package( name: "HomeAPISupport", targets: ["HomeAPISupport"] ), + .library( + name: "ProductInfoAPI", + targets: ["ProductInfoAPI"] + ), + .library( + name: "ProductInfoAPISupport", + targets: ["ProductInfoAPISupport"] + ), ], dependencies: [ .package(path: "../Core"), @@ -34,5 +42,19 @@ let package = Package( ], resources: [.process("Mocks")] ), + .target( + name: "ProductInfoAPI", + dependencies: [ + .product(name: "Network", package: "Core"), + .product(name: "Entity", package: "Entity"), + ] + ), + .target( + name: "ProductInfoAPISupport", + dependencies: [ + "ProductInfoAPI", + ], + resources: [.process("Mocks")] + ), ] ) diff --git a/APIService/Sources/HomeAPI/Responses/ProductResponse.swift b/APIService/Sources/HomeAPI/Responses/ProductResponse.swift index 1000295..7dab396 100644 --- a/APIService/Sources/HomeAPI/Responses/ProductResponse.swift +++ b/APIService/Sources/HomeAPI/Responses/ProductResponse.swift @@ -8,6 +8,8 @@ import Entity import Foundation +// MARK: - ProductResponse + struct ProductResponse: Decodable { let id: Int let imageURL: URL diff --git a/APIService/Sources/ProductInfoAPI/ProductInfoEndPoint.swift b/APIService/Sources/ProductInfoAPI/ProductInfoEndPoint.swift new file mode 100644 index 0000000..f4e9089 --- /dev/null +++ b/APIService/Sources/ProductInfoAPI/ProductInfoEndPoint.swift @@ -0,0 +1,41 @@ +// +// ProductInfoEndPoint.swift +// +// +// Created by 김응철 on 2/5/24. +// + +import Foundation +import Network + +// MARK: - ProductInfoEndPoint + +public enum ProductInfoEndPoint { + case fetchProduct(Int) + case fetchPrices(Int) +} + +// MARK: EndPoint + +extension ProductInfoEndPoint: EndPoint { + public var method: HTTPMethod { + .get + } + + public var path: String { + switch self { + case let .fetchProduct(productID): + "/v2/products/\(productID)" + case let .fetchPrices(productID): + "/v2/products/\(productID)/price-history" + } + } + + public var parameters: HTTPParameter { + .plain + } + + public var headers: [String: String] { + ["Content-Type": "application/json"] + } +} diff --git a/APIService/Sources/ProductInfoAPI/ProductInfoService.swift b/APIService/Sources/ProductInfoAPI/ProductInfoService.swift new file mode 100644 index 0000000..4f145a5 --- /dev/null +++ b/APIService/Sources/ProductInfoAPI/ProductInfoService.swift @@ -0,0 +1,62 @@ +// +// ProductInfoService.swift +// +// +// Created by 김응철 on 2/5/24. +// + +import Entity +import Foundation +import Network + +// MARK: - ProductInfoServiceRepresentable + +public protocol ProductInfoServiceRepresentable { + func fetchProduct(productID: Int) async throws -> ProductDetail + func fetchProductPrice(productID: Int) async throws -> [ProductPrice] +} + +// MARK: - ProductInfoService + +public struct ProductInfoService { + private let network: Networking + + public init(network: Networking) { + self.network = network + } +} + +// MARK: ProductInfoServiceRepresentable + +extension ProductInfoService: ProductInfoServiceRepresentable { + public func fetchProduct(productID _: Int) async throws -> ProductDetail { + let productResponse: ProductDetailResponse = try await network.request(with: ProductInfoEndPoint.fetchProduct(0)) + return ProductDetail(dto: productResponse) + } + + public func fetchProductPrice(productID _: Int) async throws -> [ProductPrice] { + let productPrice: [ProductPriceResponse] = try await network.request( + with: ProductInfoEndPoint.fetchPrices(0) + ) + return productPrice.map(ProductPrice.init) + } +} + +private extension ProductPrice { + init(dto: ProductPriceResponse) { + self.init(date: dto.date, price: dto.price) + } +} + +private extension ProductDetail { + init(dto: ProductDetailResponse) { + self.init( + id: dto.id, + imageURL: dto.imageURL, + price: dto.price, + name: dto.name, + promotion: dto.promotion, + convenienceStore: dto.convenienceStore + ) + } +} diff --git a/APIService/Sources/ProductInfoAPI/Responses/ProductDetailResponse.swift b/APIService/Sources/ProductInfoAPI/Responses/ProductDetailResponse.swift new file mode 100644 index 0000000..5f9a8fc --- /dev/null +++ b/APIService/Sources/ProductInfoAPI/Responses/ProductDetailResponse.swift @@ -0,0 +1,27 @@ +// +// ProductDetailResponse.swift +// +// +// Created by 김응철 on 2/5/24. +// + +import Entity +import Foundation + +struct ProductDetailResponse: Decodable { + let id: Int + let imageURL: URL + let price: Int + let name: String + let promotion: Promotion + let convenienceStore: ConvenienceStore + + enum CodingKeys: String, CodingKey { + case id + case imageURL = "image_url" + case price + case name + case promotion + case convenienceStore + } +} diff --git a/APIService/Sources/ProductInfoAPI/Responses/ProductPriceResponse.swift b/APIService/Sources/ProductInfoAPI/Responses/ProductPriceResponse.swift new file mode 100644 index 0000000..633ef9a --- /dev/null +++ b/APIService/Sources/ProductInfoAPI/Responses/ProductPriceResponse.swift @@ -0,0 +1,13 @@ +// +// ProductPriceResponse.swift +// +// +// Created by 김응철 on 2/5/24. +// + +import Foundation + +struct ProductPriceResponse: Decodable { + let date: Date + let price: Int +} diff --git a/APIService/Sources/ProductInfoAPISupport/Mocks/ProductPriceResponse.json b/APIService/Sources/ProductInfoAPISupport/Mocks/ProductPriceResponse.json new file mode 100644 index 0000000..aa735e7 --- /dev/null +++ b/APIService/Sources/ProductInfoAPISupport/Mocks/ProductPriceResponse.json @@ -0,0 +1,14 @@ +[ + { + "date": "2023-12-06T00:42:21Z" + "price": 1100 +}, +{ + "date": "2024-01-06T00:42:21Z" + "price": 1200 +}, +{ + "date": "2024-02-06T00:42:21Z" + "price": 1250 +} + ] diff --git a/APIService/Sources/ProductInfoAPISupport/ProductInfoURLProtocol.swift b/APIService/Sources/ProductInfoAPISupport/ProductInfoURLProtocol.swift new file mode 100644 index 0000000..7ece465 --- /dev/null +++ b/APIService/Sources/ProductInfoAPISupport/ProductInfoURLProtocol.swift @@ -0,0 +1,46 @@ +// +// ProductInfoURLProtocol.swift +// +// +// Created by 김응철 on 2/5/24. +// + +import Foundation +import Network +import ProductInfoAPI + +public final class ProductInfoURLProtocol: URLProtocol { + private lazy var mockData: [String: Data?] = [ + ProductInfoEndPoint.fetchProduct(0).path: loadMockData(fileName: "HomeProductResponse"), + ProductInfoEndPoint.fetchPrices(0).path: loadMockData(fileName: "ProductPriceResponse"), + ] + + override public class func canInit(with _: URLRequest) -> Bool { + true + } + + override public class func canonicalRequest(for request: URLRequest) -> URLRequest { + request + } + + override public func startLoading() { + defer { client?.urlProtocolDidFinishLoading(self) } + if let url = request.url, + let mockData = mockData[url.path(percentEncoded: true)], + let data = mockData, + let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) { + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: data) + } else { + client?.urlProtocol(self, didFailWithError: NetworkError.urlError) + } + } + + private func loadMockData(fileName: String) -> Data? { + guard let url = Bundle.module.url(forResource: fileName, withExtension: "json") + else { + return nil + } + return try? Data(contentsOf: url) + } +} diff --git a/Entity/Sources/Entity/Product.swift b/Entity/Sources/Entity/Product.swift index 82f4471..1a209e6 100644 --- a/Entity/Sources/Entity/Product.swift +++ b/Entity/Sources/Entity/Product.swift @@ -7,7 +7,7 @@ import Foundation -/// 편의점 행사 제품에 대한 Entity Model입니다. +/// 편의점 행사 제품 리스트에 대한 Entity Model입니다. public struct Product: Identifiable { /// 제품 고유 Identifier public let id: Int diff --git a/Entity/Sources/Entity/ProductDetail.swift b/Entity/Sources/Entity/ProductDetail.swift new file mode 100644 index 0000000..6c653c5 --- /dev/null +++ b/Entity/Sources/Entity/ProductDetail.swift @@ -0,0 +1,47 @@ +// +// ProductDetail.swift +// +// +// Created by 김응철 on 2/6/24. +// + +import Foundation + +/// 편의점 행사 제품에 대한 Entity Model입니다. +public struct ProductDetail: Identifiable { + /// 제품 고유 Identifier + public let id: Int + + /// 이미지 URL + public let imageURL: URL + + /// 제품 가격 + public let price: Int + + /// 제품명 + public let name: String + + /// 행사 정보 + public let promotion: Promotion + + /// 편의점 + public let convenienceStore: ConvenienceStore + + // TODO: 카테고리 상수 추가하기 + + public init( + id: Int, + imageURL: URL, + price: Int, + name: String, + promotion: Promotion, + convenienceStore: ConvenienceStore + ) { + self.id = id + self.imageURL = imageURL + self.price = price + self.name = name + self.promotion = promotion + self.convenienceStore = convenienceStore + } +} diff --git a/Entity/Sources/Entity/ProductPrice.swift b/Entity/Sources/Entity/ProductPrice.swift new file mode 100644 index 0000000..670d7c7 --- /dev/null +++ b/Entity/Sources/Entity/ProductPrice.swift @@ -0,0 +1,21 @@ +// +// ProductPrice.swift +// +// +// Created by 김응철 on 2/5/24. +// + +import Foundation + +public struct ProductPrice { + public let date: Date + public let price: Int + + public init( + date: Date, + price: Int + ) { + self.date = date + self.price = price + } +}