Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 0 additions & 1 deletion HotSpot/Configuration/Config.xcconfig

This file was deleted.

14 changes: 14 additions & 0 deletions HotSpot/Sources/Common/Dependency/ShopRepository+Dependency.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import ComposableArchitecture

private enum ShopRepositoryKey: DependencyKey {
static let liveValue: ShopRepository = ShopRepositoryImpl(
remoteDataSource: ShopRemoteDataSourceImpl()
)
}

extension DependencyValues {
var shopRepository: ShopRepository {
get { self[ShopRepositoryKey.self] }
set { self[ShopRepositoryKey.self] = newValue }
}
}
24 changes: 24 additions & 0 deletions HotSpot/Sources/Common/Extensions/MoyaProvider+Extension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Moya

extension MoyaProvider {
static var `default`: MoyaProvider<Target> {
return MoyaProvider<Target>(
plugins: [NetworkLoggerPlugin(configuration: .init(logOptions: .verbose))]
)
}
}

extension MoyaProvider {
func asyncRequest(_ target: Target) async throws -> Response {
return try await withCheckedThrowingContinuation { continuation in
self.request(target) { result in
switch result {
case let .success(response):
continuation.resume(returning: response)
case let .failure(error):
continuation.resume(throwing: error)
}
}
}
}
}
72 changes: 72 additions & 0 deletions HotSpot/Sources/Common/Extensions/ShopGenre.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import UIKit

public enum ShopGenre {
public static func name(for genreCode: String) -> String {
switch genreCode {
case "G001": return "居酒屋"
case "G002": return "ダイニングバー・バル"
case "G003": return "創作料理"
case "G004": return "和食"
case "G005": return "洋食"
case "G006": return "イタリアン・フレンチ"
case "G007": return "中華"
case "G008": return "焼肉・ホルモン"
case "G009": return "アジア・エスニック料理"
case "G010": return "各国料理"
case "G011": return "カラオケ・パーティ"
case "G012": return "バー・カクテル"
case "G013": return "ラーメン"
case "G014": return "カフェ・スイーツ"
case "G015": return "その他グルメ"
case "G016": return "お好み焼き・もんじゃ"
case "G017": return "韓国料理"
default: return "その他"
}
}

public static func color(for genreCode: String) -> UIColor {
switch genreCode {
case "G001": return UIColor(red: 0.8, green: 0.2, blue: 0.2, alpha: 1.0) // 居酒屋 - Red
case "G002": return UIColor(red: 0.6, green: 0.4, blue: 0.8, alpha: 1.0) // ダイニングバー・バル - Purple
case "G003": return UIColor(red: 0.2, green: 0.6, blue: 0.8, alpha: 1.0) // 創作料理 - Blue
case "G004": return UIColor(red: 0.8, green: 0.6, blue: 0.2, alpha: 1.0) // 和食 - Orange
case "G005": return UIColor(red: 0.4, green: 0.8, blue: 0.4, alpha: 1.0) // 洋食 - Green
case "G006": return UIColor(red: 0.8, green: 0.4, blue: 0.6, alpha: 1.0) // イタリアン・フレンチ - Pink
case "G007": return UIColor(red: 0.8, green: 0.2, blue: 0.4, alpha: 1.0) // 中華 - Deep Pink
case "G008": return UIColor(red: 0.4, green: 0.2, blue: 0.2, alpha: 1.0) // 焼肉・ホルモン - Brown
case "G017": return UIColor(red: 0.6, green: 0.2, blue: 0.2, alpha: 1.0) // 韓国料理 - Dark Red
case "G009": return UIColor(red: 0.2, green: 0.4, blue: 0.6, alpha: 1.0) // アジア・エスニック料理 - Dark Blue
case "G010": return UIColor(red: 0.4, green: 0.6, blue: 0.2, alpha: 1.0) // 各国料理 - Olive
case "G011": return UIColor(red: 0.8, green: 0.4, blue: 0.2, alpha: 1.0) // カラオケ・パーティ - Orange
case "G012": return UIColor(red: 0.6, green: 0.2, blue: 0.6, alpha: 1.0) // バー・カクテル - Purple
case "G013": return UIColor(red: 0.2, green: 0.8, blue: 0.6, alpha: 1.0) // ラーメン - Teal
case "G016": return UIColor(red: 0.8, green: 0.6, blue: 0.4, alpha: 1.0) // お好み焼き・もんじゃ - Light Brown
case "G014": return UIColor(red: 0.6, green: 0.8, blue: 0.2, alpha: 1.0) // カフェ・スイーツ - Light Green
case "G015": return UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1.0) // その他グルメ - Gray
default: return .black
}
}

public static func image(for genreCode: String) -> UIImage? {
switch genreCode {
case "G001": return UIImage(systemName: "wineglass.fill") // 居酒屋
case "G002": return UIImage(systemName: "wineglass") // ダイニングバー・バル
case "G003": return UIImage(systemName: "fork.knife") // 創作料理
case "G004": return UIImage(systemName: "leaf.fill") // 和食
case "G005": return UIImage(systemName: "fork.knife.circle") // 洋食
case "G006": return UIImage(systemName: "fork.knife.circle.fill") // イタリアン・フレンチ
case "G007": return UIImage(systemName: "bowl.fill") // 中華
case "G008": return UIImage(systemName: "flame.fill") // 焼肉・ホルモン
case "G017": return UIImage(systemName: "bowl") // 韓国料理
case "G009": return UIImage(systemName: "globe.asia.australia.fill") // アジア・エスニック料理
case "G010": return UIImage(systemName: "globe") // 各国料理
case "G011": return UIImage(systemName: "music.mic") // カラオケ・パーティ
case "G012": return UIImage(systemName: "wineglass") // バー・カクテル
case "G013": return UIImage(systemName: "bowl") // ラーメン
case "G016": return UIImage(systemName: "flame") // お好み焼き・もんじゃ
case "G014": return UIImage(systemName: "cup.and.saucer.fill") // カフェ・スイーツ
case "G015": return UIImage(systemName: "questionmark.circle.fill") // その他グルメ
default: return UIImage(systemName: "mappin.circle.fill")
}
}
}
46 changes: 46 additions & 0 deletions HotSpot/Sources/Common/Extensions/UIImage+Kingfisher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import UIKit
import Kingfisher

extension UIImage {
static func load(from urlString: String?, completion: @escaping (UIImage?) -> Void) {
guard let urlString = urlString,
let url = URL(string: urlString) else {
completion(nil)
return
}

KingfisherManager.shared.retrieveImage(with: url) { result in
switch result {
case .success(let value):
completion(value.image)
case .failure:
completion(nil)
}
}
}

static func loadThumbnail(from urlString: String?, completion: @escaping (UIImage?) -> Void) {
guard let urlString = urlString,
let url = URL(string: urlString) else {
completion(nil)
return
}

let processor = DownsamplingImageProcessor(size: CGSize(width: 200, height: 200))
KingfisherManager.shared.retrieveImage(
with: url,
options: [
.processor(processor),
.scaleFactor(UIScreen.main.scale),
.cacheOriginalImage
]
) { result in
switch result {
case .success(let value):
completion(value.image)
case .failure:
completion(nil)
}
}
}
}
26 changes: 17 additions & 9 deletions HotSpot/Sources/Data/API/ServiceAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Foundation
import Moya

enum ServiceAPI {
case searchShops(ShopSearchRequestDTO)
}

extension ServiceAPI: TargetType {
Expand All @@ -15,30 +16,37 @@ extension ServiceAPI: TargetType {

var path: String {
switch self {
default:
return ""
case .searchShops:
return "/gourmet/v1/"
}
}

var method: Moya.Method {
switch self {
default:
case .searchShops:
return .get
}
}

var task: Task {
guard let apiKey = Bundle.main.object(forInfoDictionaryKey: "API_KEY") as? String else {
fatalError("API_KEY is not set in configuration")
}

switch self {
default:
return .requestPlain
case let .searchShops(request):
var parameters = request.asParameters
parameters["key"] = apiKey
parameters["format"] = "json"
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
}
}

var headers: [String: String]? {
return [
"Content-Type": "application/json"
]
return nil
}

var validationType: ValidationType { .successCodes }
var validationType: ValidationType {
return .successCodes
}
}
41 changes: 41 additions & 0 deletions HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Foundation

struct ShopSearchRequestDTO {
let lat: Double // Latitude
let lng: Double // Longitude
let range: Int // Search range (1–5)
let count: Int? // Number of results (1–100)
let keyword: String? // Keyword search
let genre: String? // Genre code
let order: Int? // Order: 1=recommend, 2=popularity
let start: Int? // Starting index for paging
let budget: String? // Budget code
let privateRoom: Bool? // Private room availability
let wifi: Bool? // Wi-Fi availability
let nonSmoking: Bool? // Non-smoking availability
let coupon: Bool? // Coupon availability
let openNow: Bool? // Currently open filter

/// Converts the DTO into a dictionary of parameters for Moya or URL encoding
var asParameters: [String: Any] {
var params: [String: Any] = [
"lat": lat,
"lng": lng,
"range": range
]

if let count = count { params["count"] = count }
if let keyword = keyword { params["keyword"] = keyword }
if let genre = genre { params["genre"] = genre }
if let order = order { params["order"] = order }
if let start = start { params["start"] = start }
if let budget = budget { params["budget"] = budget }
if let privateRoom = privateRoom { params["private_room"] = privateRoom ? 1 : 0 }
if let wifi = wifi { params["wifi"] = wifi ? 1 : 0 }
if let nonSmoking = nonSmoking { params["non_smoking"] = nonSmoking ? 1 : 0 }
if let coupon = coupon { params["coupon"] = coupon ? 1 : 0 }
if let openNow = openNow, openNow { params["open"] = "now" }

return params
}
}
91 changes: 91 additions & 0 deletions HotSpot/Sources/Data/DTO/Response/ShopDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import Foundation

struct ShopDTO: Decodable {
let id: String
let name: String
let nameKana: String?
let address: String
let stationName: String?
let lat: Double
let lng: Double
let access: String
let open: String?
let close: String?
let photo: Photo
let genre: Genre
let catchPhrase: String
let budget: Budget?
let wifi: String?
let nonSmoking: String?
let privateRoom: String?
let card: String?

struct Photo: Decodable {
let pc: PcPhoto

struct PcPhoto: Decodable {
let large: String
let medium: String
let small: String

enum CodingKeys: String, CodingKey {
case large = "l"
case medium = "m"
case small = "s"
}
}
}

struct Genre: Decodable {
let code: String
let name: String
let catchPhrase: String

enum CodingKeys: String, CodingKey {
case code
case name
case catchPhrase = "catch"
}
}

struct Budget: Decodable {
let code: String
let name: String
let average: String?
}

enum CodingKeys: String, CodingKey {
case id
case name
case nameKana = "name_kana"
case address
case stationName = "station_name"
case lat
case lng
case access
case open
case close
case photo
case genre
case catchPhrase = "catch"
case budget
case wifi
case nonSmoking = "non_smoking"
case privateRoom = "private_room"
case card
}

func toDomain() -> ShopModel {
ShopModel(
id: id,
name: name,
address: address,
latitude: lat,
longitude: lng,
imageUrl: photo.pc.large,
access: access,
openingHours: open,
genreCode: genre.code
)
}
}
9 changes: 9 additions & 0 deletions HotSpot/Sources/Data/DTO/Response/ShopSearchResponseDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Foundation

struct ShopSearchResponseDTO: Decodable {
let results: ShopSearchResultsDTO

enum CodingKeys: String, CodingKey {
case results
}
}
15 changes: 15 additions & 0 deletions HotSpot/Sources/Data/DTO/Response/ShopSearchResultsDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation

struct ShopSearchResultsDTO: Decodable {
let resultsAvailable: Int
let resultsReturned: String
let resultsStart: Int
let shop: [ShopDTO]

enum CodingKeys: String, CodingKey {
case resultsAvailable = "results_available"
case resultsReturned = "results_returned"
case resultsStart = "results_start"
case shop
}
}
5 changes: 5 additions & 0 deletions HotSpot/Sources/Data/DataSource/ShopRemoteDataSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Foundation

protocol ShopRemoteDataSource {
func search(request: ShopSearchRequestDTO) async throws -> ShopSearchResponseDTO
}
Loading
Loading