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
5 changes: 4 additions & 1 deletion .github/workflows/Test_on_develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ jobs:
run: mise trust

- name: Install Tuist via mise
run: mise install tuist

- name: Resolve dependencies
run: mise x -- tuist install

- name: Generate Xcode project
run: mise x -- tuist generate

- name: Run Unit Tests
run: mise x -- tuist test Weave2-UnitTest --no-selective-testing
run: mise x -- tuist test HotSpot
25 changes: 25 additions & 0 deletions HotSpot/Sources/Domain/Entity/Location.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Foundation
import MapKit

struct Location: Codable, Hashable {
var lat: Double
var lon: Double
}

extension Location {
func toCLLocationCoordinate2D() -> CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: self.lat,
longitude: self.lon
)
}
}

extension CLLocationCoordinate2D {
func toLocation() -> Location {
Location(
lat: self.latitude,
lon: self.longitude
)
}
}
6 changes: 4 additions & 2 deletions HotSpot/Sources/Domain/Entity/Restaurant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ struct Restaurant: Identifiable, Equatable {
let address: String
let imageURL: URL?
let phone: String?
let location: Location?

init(id: UUID = UUID(), name: String, address: String, imageURL: URL? = nil, phone: String? = nil) {
init(id: UUID = UUID(), name: String, address: String, imageURL: URL? = nil, phone: String? = nil, location: Location?) {
self.id = id
self.name = name
self.address = address
self.imageURL = imageURL
self.phone = phone
self.location = location
}
}
}
49 changes: 49 additions & 0 deletions HotSpot/Sources/Domain/Service/LocationManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Foundation
import CoreLocation
import ComposableArchitecture
import Dependencies

struct LocationManager: DependencyKey {
static var liveValue: LocationManager = .live

var requestLocation: @Sendable () async -> CLLocation?

static let live = Self(
requestLocation: {
let manager = CLLocationManager()
manager.requestWhenInUseAuthorization()

return await withCheckedContinuation { continuation in
let delegate = LocationDelegate(continuation: continuation)
manager.delegate = delegate
manager.requestLocation()

// Keep the delegate alive until the location is received
_ = delegate
}
}
)
}

private class LocationDelegate: NSObject, CLLocationManagerDelegate {
let continuation: CheckedContinuation<CLLocation?, Never>

init(continuation: CheckedContinuation<CLLocation?, Never>) {
self.continuation = continuation
}

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
continuation.resume(returning: locations.first)
}

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
continuation.resume(returning: nil)
}
}

extension DependencyValues {
var locationManager: LocationManager {
get { self[LocationManager.self] }
set { self[LocationManager.self] = newValue }
}
}
39 changes: 39 additions & 0 deletions HotSpot/Sources/Domain/Service/RestaurantClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Foundation
import ComposableArchitecture
import Dependencies

struct RestaurantClient: DependencyKey {
static var liveValue: RestaurantClient = .live

var searchRestaurants: @Sendable (_ latitude: Double, _ longitude: Double, _ radius: Int) async throws -> [Restaurant]

static let live = Self(
searchRestaurants: { latitude, longitude, radius in
// TODO: Implement actual API call
// For now, return mock data
return [
Restaurant(
name: "맛있는 식당",
address: "서울시 강남구 테헤란로 123",
imageURL: URL(string: "https://example.com/image1.jpg"),
phone: "02-123-4567",
location: Location(lat: latitude, lon: longitude)
),
Restaurant(
name: "맛있는 카페",
address: "서울시 강남구 테헤란로 456",
imageURL: URL(string: "https://example.com/image2.jpg"),
phone: "02-765-4321",
location: Location(lat: latitude + 0.001, lon: longitude + 0.001)
)
]
}
)
}

extension DependencyValues {
var restaurantClient: RestaurantClient {
get { self[RestaurantClient.self] }
set { self[RestaurantClient.self] = newValue }
}
}
75 changes: 22 additions & 53 deletions HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,85 +4,54 @@ import ComposableArchitecture
@Reducer
struct AppCoordinator {
struct State: Equatable {
var search: SearchStore.State = .init()
var map: MapStore.State = .init()
var search: SearchStore.State? = nil
var restaurantDetail: RestaurantDetailStore.State?
var favorite: FavoriteStore.State?
var settings: SettingsStore.State?
var selectedRestaurantId: UUID?
}

enum Action {
case map(MapStore.Action)
case search(SearchStore.Action)
case restaurantDetail(RestaurantDetailStore.Action)
case favorite(FavoriteStore.Action)
case settings(SettingsStore.Action)

case showRestaurantDetail
case navigateToFavorite
case navigateToSettings
case showRestaurantDetail(UUID)
case showSearch
case dismissSearch
case dismissDetail
case dismissFavorite
case dismissSettings
}

var body: some ReducerOf<Self> {
Scope(state: \.search, action: \.search) {
SearchStore()
}

Reduce { state, action in
switch action {
case .search(.showRestaurantDetail):
state.restaurantDetail = .init()
case .map(.showSearch), .showSearch:
state.search = .init()
return .none

case .search(.navigateToSettings):
state.settings = .init()
case .search(.pop), .dismissSearch:
state.search = nil
return .none

case .showRestaurantDetail:
return .none

case .navigateToFavorite:
state.favorite = .init()
return .none
case let .search(.selectRestaurant(restaurant)):
state.selectedRestaurantId = restaurant.id
return .send(.showRestaurantDetail(restaurant.id))

case .navigateToSettings:
state.settings = .init()
case let .showRestaurantDetail(id):
state.restaurantDetail = .init(restaurantId: id)
return .none

case .dismissDetail:
case .restaurantDetail(.pop), .dismissDetail:
state.restaurantDetail = nil
state.selectedRestaurantId = nil
return .none

case .dismissFavorite:
state.favorite = nil
return .none

case .dismissSettings:
state.settings = nil
return .none
case let .map(.showRestaurantDetail(id)):
state.selectedRestaurantId = id
return .send(.showRestaurantDetail(id))

case .search:
return .none

case .restaurantDetail:
return .none

case .favorite:
return .none

case .settings:
case .map, .search, .restaurantDetail:
return .none
}
}
.ifLet(\.restaurantDetail, action: \.restaurantDetail) {
RestaurantDetailStore()
}
.ifLet(\.favorite, action: \.favorite) {
FavoriteStore()
}
.ifLet(\.settings, action: \.settings) {
SettingsStore()
}
}
}
39 changes: 12 additions & 27 deletions HotSpot/Sources/Presentation/Coordinator/AppCoordinatorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,23 @@ import ComposableArchitecture

struct AppCoordinatorView: View {
let store: StoreOf<AppCoordinator>

var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
NavigationView {
VStack {
SearchView(store: store.scope(state: \.search, action: \.search))

NavigationLink(
destination: IfLetStore(
store.scope(state: \.restaurantDetail, action: \.restaurantDetail),
then: { store in
RestaurantDetailView(store: store)
}
),
isActive: viewStore.binding(
get: { $0.restaurantDetail != nil },
send: { $0 ? .showRestaurantDetail : .dismissDetail }
)
) {
EmptyView()
}
.hidden()

MapView(store: store.scope(state: \.map, action: \.map))

NavigationLink(
destination: IfLetStore(
store.scope(state: \.favorite, action: \.favorite),
store.scope(state: \.search, action: \.search),
then: { store in
FavoriteView(store: store)
SearchView(store: store)
}
),
isActive: viewStore.binding(
get: { $0.favorite != nil },
send: { $0 ? .navigateToFavorite : .dismissFavorite }
get: { $0.search != nil },
send: { $0 ? .showSearch : .dismissSearch }
)
) {
EmptyView()
Expand All @@ -44,21 +28,22 @@ struct AppCoordinatorView: View {

NavigationLink(
destination: IfLetStore(
store.scope(state: \.settings, action: \.settings),
store.scope(state: \.restaurantDetail, action: \.restaurantDetail),
then: { store in
SettingsView(store: store)
RestaurantDetailView(store: store)
}
),
isActive: viewStore.binding(
get: { $0.settings != nil },
send: { $0 ? .navigateToSettings : .dismissSettings }
get: { $0.restaurantDetail != nil },
send: { $0 ? .showRestaurantDetail(viewStore.selectedRestaurantId ?? UUID()) : .dismissDetail }
)
) {
EmptyView()
}
.hidden()
}
}
.navigationViewStyle(.stack)
}
}
}
25 changes: 0 additions & 25 deletions HotSpot/Sources/Presentation/Favorite/FavoriteStore.swift

This file was deleted.

22 changes: 0 additions & 22 deletions HotSpot/Sources/Presentation/Favorite/FavoriteView.swift

This file was deleted.

Loading
Loading