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
3 changes: 2 additions & 1 deletion Projects/App/Sources/Application/AppComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ final class AppComponent: Component<EmptyDependency>, RootDependency {
private var placeRepository: PlaceRepositoryInterface {
shared {
let service = makePlaceService()
return PlaceRepository(service: service)
let googlePlacesService = makeGooglePlacesService()
return PlaceRepository(service: service, googlePlacesService: googlePlacesService)
}
}

Expand Down
14 changes: 14 additions & 0 deletions Projects/Data/Sources/DI/GooglePlacesServiceFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// GooglePlacesServiceFactory.swift
// Data
//
// Created by kimnahun on 2026-02-24.
// Copyright ยฉ 2026 NDGL-iOS. All rights reserved.
//

import Foundation
import Networks

public func makeGooglePlacesService() -> GooglePlacesServiceProtocol {
GooglePlacesService()
}
21 changes: 16 additions & 5 deletions Projects/Data/Sources/Repository/Place/PlaceRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,25 @@ import Networks

public final class PlaceRepository: PlaceRepositoryInterface {
private let service: PlaceServiceProtocol

public init(service: PlaceServiceProtocol) {
private let googlePlacesService: GooglePlacesServiceProtocol

public init(service: PlaceServiceProtocol, googlePlacesService: GooglePlacesServiceProtocol) {
self.service = service
self.googlePlacesService = googlePlacesService
}

public func searchPlaces() async throws -> Int {

public func searchPlaces(keyword: String) async throws -> [PlaceSearchResult] {
do {
let response = try await googlePlacesService.searchText(keyword: keyword)
return (response.places ?? []).compactMap { $0.toDomain() }
} catch {
throw error.toNDGLError()
}
}

public func registerPlace(googlePlaceId: String) async throws {
do {
return try await service.searchPlaces()
try await service.registerPlace(googlePlaceId: googlePlaceId)
} catch {
throw error.toNDGLError()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,50 @@ public final class UserTravelRepository: UserTravelRepositoryInterface {
throw error.toNDGLError()
}
}

public func fetchUserTravelDetail(id: Int) async throws -> TravelDetail {
do {
return try await service.getContentCard(id: id).toDomain()
} catch {
throw error.toNDGLError()
}
}

public func fetchItinerary(travelId: Int, day: Int) async throws -> [TravelPlace] {
do {
return try await service.getItinerary(travelId: travelId, day: day).toDomain()
} catch {
throw error.toNDGLError()
}
}

public func addItinerary(travelId: Int, googlePlaceId: String, day: Int, sequence: Int) async throws {
do {
let request = AddItineraryRequest(googlePlaceId: googlePlaceId, day: day, sequence: sequence)
try await service.addItinerary(travelId: travelId, request: request)
} catch {
throw error.toNDGLError()
}
}

public func replaceItinerary(travelId: Int, places: [TravelPlace]) async throws {
do {
let items = places.enumerated().map { index, place in
ReplaceItineraryItemRequest(
googlePlaceId: place.place.googlePlaceId,
day: place.day,
sequence: index + 1,
startTime: nil,
estimatedDuration: place.estimatedDuration,
travelerTip: nil
)
}
try await service.replaceItinerary(
travelId: travelId,
request: ReplaceItineraryRequest(itineraries: items)
)
} catch {
throw error.toNDGLError()
}
}
}
13 changes: 13 additions & 0 deletions Projects/Data/Sources/Transform/PlaceTransform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,16 @@ extension PlacePhotoResponse {
)
}
}

extension GooglePlaceItem {
func toDomain() -> PlaceSearchResult? {
guard let location = location else { return nil }
return PlaceSearchResult(
googlePlaceId: id,
name: displayName?.text ?? "",
address: formattedAddress ?? "",
latitude: location.latitude,
longitude: location.longitude
)
}
}
44 changes: 44 additions & 0 deletions Projects/Data/Sources/Transform/UserTravelTransform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,27 @@ import Domain
import Networks


extension UserContentCardResponse {
func toDomain() -> TravelDetail {
TravelDetail(
travelId: userTravelId,
country: country,
city: city,
budgetPerPerson: 0,
nights: nights,
days: days,
youtube: YouTubeInfo(
title: title,
youtuber: "",
thumbnail: nil,
profileImage: nil,
link: nil,
summary: ""
)
)
}
}

extension UpcomingResponse {
func toDomain() -> MyTripSummary {
let schedule: Schedule?
Expand Down Expand Up @@ -49,6 +70,29 @@ extension CreateUserTravelResponse {
}
}

extension UserTravelItineraryResponse {
func toDomain() -> [TravelPlace] {
itineraries.compactMap { $0.toDomain() }
}
}

extension UserTravelPlaceResponse {
func toDomain() -> TravelPlace? {
guard let place else { return nil }
return TravelPlace(
id: id,
day: day,
sequence: sequence,
distanceKm: distanceKm,
transportation: transportation?.map { $0.toDomain() } ?? [],
youtubeTips: travelerTips ?? [],
planB: planB?.map { $0.toDomain() } ?? [],
estimatedDuration: estimatedDuration,
place: place.toDomain()
)
}
}

extension UpcomingListResponse {
func toDomain() -> [UpcomingInfo] {
self.content.map {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
import Foundation

public protocol PlaceRepositoryInterface {
func searchPlaces() async throws -> Int //์ž„์‹œ
func searchPlaces(keyword: String) async throws -> [PlaceSearchResult]
func registerPlace(googlePlaceId: String) async throws
func fetchPlacePhotos(googlePlaceId: String) async throws -> [PlacePhoto]
func fetchPlaceDetail(googlePlaceId: String) async throws -> PlaceDetail
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ public protocol UserTravelRepositoryInterface {
// func fetchContentCard(id: Int) async throws ->
func fetchUpcoming() async throws -> MyTripSummary
func fetchUpcomingList(page: Int?, size: Int?) async throws -> [UpcomingInfo]
func fetchUserTravelDetail(id: Int) async throws -> TravelDetail
func fetchItinerary(travelId: Int, day: Int) async throws -> [TravelPlace]
func addItinerary(travelId: Int, googlePlaceId: String, day: Int, sequence: Int) async throws
func replaceItinerary(travelId: Int, places: [TravelPlace]) async throws
}
31 changes: 31 additions & 0 deletions Projects/Domain/Sources/Model/Follow/PlaceSearchResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// PlaceSearchResult.swift
// Domain
//
// Created by kimnahun on 2026-02-24.
// Copyright ยฉ 2026 NDGL-iOS. All rights reserved.
//

import Foundation

public struct PlaceSearchResult {
public let googlePlaceId: String
public let name: String
public let address: String
public let latitude: Double
public let longitude: Double

public init(
googlePlaceId: String,
name: String,
address: String,
latitude: Double,
longitude: Double
) {
self.googlePlaceId = googlePlaceId
self.name = name
self.address = address
self.latitude = latitude
self.longitude = longitude
}
}
30 changes: 30 additions & 0 deletions Projects/Domain/Sources/UseCase/FollowDetailUsecase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ import Foundation
public protocol FollowDetailUsecaseProtocol {
func fetchTravelDetail(id: Int) async throws -> TravelDetail
func fetchPlaces(travelId: Int, day: Int) async throws -> [TravelPlace]
func fetchMyTravelDetail(id: Int) async throws -> TravelDetail
func fetchMyTravelPlaces(travelId: Int, day: Int) async throws -> [TravelPlace]
func createUserTravel(request: CreateTravelRequest) async throws -> CreateTravelResponse
func fetchPlaceDetail(googlePlaceId: String) async throws -> PlaceDetail
func fetchPlacePhotos(googlePlaceId: String) async throws -> [PlacePhoto]
func searchPlaces(keyword: String) async throws -> [PlaceSearchResult]
func registerPlace(googlePlaceId: String) async throws
func addItinerary(travelId: Int, googlePlaceId: String, day: Int, sequence: Int) async throws
func replaceItinerary(travelId: Int, places: [TravelPlace]) async throws
}

public final class FollowDetailUsecase {
Expand All @@ -40,6 +46,14 @@ extension FollowDetailUsecase: FollowDetailUsecaseProtocol {
public func fetchPlaces(travelId: Int, day: Int) async throws -> [TravelPlace] {
try await travelTemplateRepository.fetchPlaces(travelId: travelId, day: day)
}

public func fetchMyTravelDetail(id: Int) async throws -> TravelDetail {
try await userTravelRepository.fetchUserTravelDetail(id: id)
}

public func fetchMyTravelPlaces(travelId: Int, day: Int) async throws -> [TravelPlace] {
try await userTravelRepository.fetchItinerary(travelId: travelId, day: day)
}

public func createUserTravel(request: CreateTravelRequest) async throws -> CreateTravelResponse {
try await userTravelRepository.createUserTravel(request: request)
Expand All @@ -52,4 +66,20 @@ extension FollowDetailUsecase: FollowDetailUsecaseProtocol {
public func fetchPlacePhotos(googlePlaceId: String) async throws -> [PlacePhoto] {
try await placeRepository.fetchPlacePhotos(googlePlaceId: googlePlaceId)
}

public func searchPlaces(keyword: String) async throws -> [PlaceSearchResult] {
try await placeRepository.searchPlaces(keyword: keyword)
}

public func registerPlace(googlePlaceId: String) async throws {
try await placeRepository.registerPlace(googlePlaceId: googlePlaceId)
}

public func addItinerary(travelId: Int, googlePlaceId: String, day: Int, sequence: Int) async throws {
try await userTravelRepository.addItinerary(travelId: travelId, googlePlaceId: googlePlaceId, day: day, sequence: sequence)
}

public func replaceItinerary(travelId: Int, places: [TravelPlace]) async throws {
try await userTravelRepository.replaceItinerary(travelId: travelId, places: places)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// AddPlaceBuilder.swift
// FollowFeature
//
// Created by kimnahun on 2026-02-24.
// Copyright ยฉ 2026 NDGL-iOS. All rights reserved.
//

import Domain

import RIBs

// MARK: - AddPlaceDependency

protocol AddPlaceDependency: Dependency {
var followDetailUsecase: FollowDetailUsecaseProtocol { get }
}

// MARK: - AddPlaceComponent

final class AddPlaceComponent: Component<AddPlaceDependency> {
var followDetailUsecase: FollowDetailUsecaseProtocol {
dependency.followDetailUsecase
}
}

// MARK: - AddPlaceBuildable

protocol AddPlaceBuildable: Buildable {
func build(withListener listener: AddPlaceListener) -> AddPlaceRouting
}

// MARK: - AddPlaceBuilder

final class AddPlaceBuilder: Builder<AddPlaceDependency>, AddPlaceBuildable {

override init(dependency: AddPlaceDependency) {
super.init(dependency: dependency)
}

func build(withListener listener: AddPlaceListener) -> AddPlaceRouting {
let component = AddPlaceComponent(dependency: dependency)
let viewController = AddPlaceViewController()
let interactor = AddPlaceInteractor(
presenter: viewController,
followDetailUsecase: component.followDetailUsecase
)
interactor.listener = listener

let router = AddPlaceRouter(interactor: interactor, viewController: viewController)
return router
}
}
Loading