Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions ONMIR/Core/GoogleBooks/GoogleBooksClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import Foundation

public struct GoogleBooksClient: Sendable {
public enum GoogleBooksError: Error {
case invalidURL
case unexpectedResponse
case decodingError(Error)
case underlying(Error)
}

public enum OrderByType: String, Sendable {
case newest
case relevance
}

private let session: URLSession
private let baseURL = "https://www.googleapis.com/books/v1"

public init(session: URLSession = .shared) {
self.session = session
}

/// 책 검색
/// - Parameters:
/// - query: 검색어
/// - maxResults: 최대 결과 수 (기본값: 20)
/// - Returns: `BookSearchResponse`
public func searchBooks(
query: String,
startIndex: Int,
orderBy: OrderByType = .relevance,
maxResults: Int = 20
) async throws(GoogleBooksError) -> BookSearchResponse {
var components = URLComponents(string: "\(baseURL)/volumes")
components?.queryItems = [
URLQueryItem(name: "q", value: query),
URLQueryItem(name: "startIndex", value: "\(startIndex)"),
URLQueryItem(name: "orderBy", value: orderBy.rawValue),
URLQueryItem(name: "maxResults", value: "\(maxResults)"),
]

guard let url = components?.url else {
throw GoogleBooksError.invalidURL
}

do {
let (data, response) = try await session.data(from: url)

guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode)
else {
throw GoogleBooksError.unexpectedResponse
}

let decoder = JSONDecoder()
return try decoder.decode(BookSearchResponse.self, from: data)
} catch let decodingError as DecodingError {
throw GoogleBooksError.decodingError(decodingError)
} catch {
throw GoogleBooksError.underlying(error)
}
}
}

extension GoogleBooksClient {
public struct BookSearchResponse: Decodable, Sendable {
public let items: [BookItem]?
public let totalItems: Int
public let kind: String

public struct BookItem: Decodable, Sendable {
public var id: String
public let volumeInfo: VolumeInfo
public let saleInfo: SaleInfo?

public struct VolumeInfo: Decodable, Sendable {
public let title: String
public let subtitle: String?
public let authors: [String]?
public let publisher: String?
public let publishedDate: String?
public let description: String?
public let pageCount: Int?
public let categories: [String]?
public let language: String?
public let imageLinks: ImageLinks?

public struct ImageLinks: Decodable, Sendable {
public let smallThumbnail: String?
public let thumbnail: String?
}
}

public struct SaleInfo: Decodable, Sendable {
public let listPrice: Price?
public let retailPrice: Price?
public let buyLink: String?

public struct Price: Decodable, Sendable {
public let amount: Double?
public let currencyCode: String?
}
}
}
}
}