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: 1 addition & 0 deletions WhereAreYou/WhereAreYou/Core/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
FirebaseApp.configure()
configureFirebaseEmulators()
DIContainer.shared.registerDependencies()
return true
}

Expand Down
34 changes: 34 additions & 0 deletions WhereAreYou/WhereAreYou/Core/DI/AppDIContainer+Register.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// AppDIContainer+Register.swift
// WhereAreYou
//
// Created by 김성훈 on 8/26/26.
//

extension DIContainer {

func registerDependencies() {
// 위치
let coreLocationRepository = CoreLocationRepository()
register(LocationRepository.self, instance: coreLocationRepository)
register(LocationPermissionRepository.self, instance: coreLocationRepository)

// 약속 생성/조회
register(AppointmentCreationRepository.self) { MockAppointmentCreationRepository() }
register(AppointmentDetailRepository.self) { MockAppointmentDetailRepository() }
register(AppointmentInfoRepository.self) { MockAppointmentInfoRepository() }

// 채팅
register(ChatRepository.self) { MockChatRepository() }

// 장소 검색/조회
register(NearbyPlaceRepository.self) { MockNearbyPlaceRepository() }
register(PlaceSearchRepository.self) { MockPlaceSearchRepository() }
register(ReverseGeocodingRepository.self) { MockReverseGeocodingRepository() }
register(SharedPlaceRepository.self) { MockSharedPlaceRepository() }

// 길찾기
register(RouteSearchRepository.self) { MockRouteSearchRepository() }
}

}
34 changes: 34 additions & 0 deletions WhereAreYou/WhereAreYou/Core/DI/DIContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// DIContainer.swift
// WhereAreYou
//
// Created by 김성훈 on 8/26/26.
//

import Foundation

/// register/resolve 방식의 DI 컨테이너
final class DIContainer {

static let shared = DIContainer()

private var factories: [ObjectIdentifier: () -> Any] = [:]

private init() { }

func register<T>(_ type: T.Type, factory: @escaping () -> T) {
factories[ObjectIdentifier(type)] = factory
}

func register<T>(_ type: T.Type, instance: T) {
factories[ObjectIdentifier(type)] = { instance }
}

func resolve<T>(_ type: T.Type = T.self) -> T {
guard let factory = factories[ObjectIdentifier(type)], let resolved = factory() as? T else {
fatalError("\(type)가 DIContainer에 등록되어 있지 않습니다.")
}
return resolved
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,22 @@ final class MockAppointmentDetailRepository: AppointmentDetailRepository {
}
}

func fetchAppointment(
code: String,
completion: @escaping (Result<(appointment: Appointment, currentUserID: String), Error>) -> Void
) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
guard code == Self.mockCode else {
completion(.failure(AppError.notFound))
return
}
let appointment = Self.makeMockAppointment(appointmentID: "appointment_joined")
completion(.success((appointment: appointment, currentUserID: Self.currentUserID)))
}
}

private static let currentUserID = "user_me"
private static let mockCode = "MOCK1234"

private static func profileImageURL(_ assetName: String) -> URL {
URL(string: "asset://\(assetName)")!
Expand Down Expand Up @@ -133,7 +148,7 @@ final class MockAppointmentDetailRepository: AppointmentDetailRepository {

return Appointment(
id: appointmentID,
code: "MOCK1234",
code: Self.mockCode,
name: "고기굽는방앗간 이수역점",
dateTime: now.addingTimeInterval(13 * 60),
place: place,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,9 @@ protocol AppointmentDetailRepository {
completion: @escaping (Result<(appointment: Appointment, currentUserID: String), Error>) -> Void
)

func fetchAppointment(
code: String,
completion: @escaping (Result<(appointment: Appointment, currentUserID: String), Error>) -> Void
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// JoinAppointmentUseCase.swift
// WhereAreYou
//
// Created by 김성훈 on 8/26/26.
//

final class JoinAppointmentUseCase {

private let repository: AppointmentDetailRepository

init(repository: AppointmentDetailRepository) {
self.repository = repository
}

func execute(
code: String,
completion: @escaping (Result<(appointment: Appointment, currentUserID: String), Error>) -> Void
) {
repository.fetchAppointment(code: code, completion: completion)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ extension AppointmentCreationViewController {
card.onCopyCodeTap = {
UIPasteboard.general.string = info.code
}
card.onConfirmTap = {
// TODO: 약속 화면으로 이동
card.onConfirmTap = { [weak self] in
self?.presentChat(appointmentInfo: info)
}

view.addSubview(card)
Expand All @@ -141,6 +141,34 @@ extension AppointmentCreationViewController {
}
}

private func presentChat(appointmentInfo: AppointmentInfo) {
guard let navigationController else { return }

let chatRepository = DIContainer.shared.resolve(ChatRepository.self)
let chatViewModel = ChatViewModel(
appointmentInfo: appointmentInfo,
fetchMessagesUseCase: FetchMessagesUseCase(repository: chatRepository),
sendMessageUseCase: SendMessageUseCase(repository: chatRepository),
getCurrentLocationUseCase: GetCurrentLocationUseCase(
repository: DIContainer.shared.resolve(LocationRepository.self)
),
shareLocationUseCase: ShareLocationUseCase(repository: chatRepository),
sharePlaceUseCase: SharePlaceUseCase(
chatRepository: chatRepository,
sharedPlaceRepository: DIContainer.shared.resolve(SharedPlaceRepository.self)
)
)
let chatViewController = ChatViewController(viewModel: chatViewModel)

var viewControllers = navigationController.viewControllers
if let index = viewControllers.firstIndex(of: self) {
viewControllers[index] = chatViewController
} else {
viewControllers.append(chatViewController)
}
navigationController.setViewControllers(viewControllers, animated: true)
}

// MARK: - Date Picker

private func presentDatePicker() {
Expand All @@ -167,7 +195,7 @@ extension AppointmentCreationViewController {
let searchPlaceViewController = SearchPlaceCardViewController(
viewModel: SearchPlaceCardViewModel(
searchPlacesUseCase: SearchPlacesUseCase(
repository: MockPlaceSearchRepository()
repository: DIContainer.shared.resolve(PlaceSearchRepository.self)
)
),
selectionButtonTitle: "선택하기",
Expand All @@ -192,11 +220,11 @@ extension AppointmentCreationViewController {
private func presentPlaceMapSelection() {
let placeSelectionViewModel = PlaceSelectionViewModel(
getCurrentLocationUseCase: GetCurrentLocationUseCase(
repository: CoreLocationRepository()
repository: DIContainer.shared.resolve(LocationRepository.self)
),
getNearbyPlaceUseCase: GetNearbyPlaceUseCase(
nearbyPlaceRepository: MockNearbyPlaceRepository(),
reverseGeocodingRepository: MockReverseGeocodingRepository()
nearbyPlaceRepository: DIContainer.shared.resolve(NearbyPlaceRepository.self),
reverseGeocodingRepository: DIContainer.shared.resolve(ReverseGeocodingRepository.self)
),
initialCoordinate: viewModel.place?.coordinate
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,11 @@ final class AppointmentListViewController: UIViewController {
}()

let card = AppointmentListCard(item: item, accessoryView: remainingTimeLabel)
card.onChatTap = {
print("\(item.title) 대화 열기")
card.onChatTap = { [weak self] in
self?.presentChat(appointmentID: item.id)
}
card.onMapTap = {
print("\(item.title) 지도 열기")
card.onMapTap = { [weak self] in
self?.presentAppointmentRoute(appointmentID: item.id)
}
card.contextMenuProvider = { [weak self] in
self?.makeContextMenu(id: item.id) ?? UIMenu(children: [])
Expand Down Expand Up @@ -245,4 +245,40 @@ final class AppointmentListViewController: UIViewController {
navigationController?.pushViewController(PastAppointmentListViewController(), animated: true)
}

// MARK: - Chat / Route

private func presentChat(appointmentID: String) {
let repository = DIContainer.shared.resolve(AppointmentInfoRepository.self)
FetchAppointmentInfoUseCase(repository: repository).execute(appointmentID: appointmentID) { [weak self] result in
DispatchQueue.main.async {
guard let self, case .success(let appointment) = result else { return }
let chatRepository = DIContainer.shared.resolve(ChatRepository.self)
let viewModel = ChatViewModel(
appointmentInfo: AppointmentInfo(appointment: appointment),
fetchMessagesUseCase: FetchMessagesUseCase(repository: chatRepository),
sendMessageUseCase: SendMessageUseCase(repository: chatRepository),
getCurrentLocationUseCase: GetCurrentLocationUseCase(
repository: DIContainer.shared.resolve(LocationRepository.self)
),
shareLocationUseCase: ShareLocationUseCase(repository: chatRepository),
sharePlaceUseCase: SharePlaceUseCase(
chatRepository: chatRepository,
sharedPlaceRepository: DIContainer.shared.resolve(SharedPlaceRepository.self)
)
)
self.navigationController?.pushViewController(ChatViewController(viewModel: viewModel), animated: true)
}
}
}

private func presentAppointmentRoute(appointmentID: String) {
let routeViewModel = AppointmentRouteViewModel(
appointmentID: appointmentID,
getAppointmentDetailUseCase: GetAppointmentDetailUseCase(
repository: DIContainer.shared.resolve(AppointmentDetailRepository.self)
)
)
present(AppointmentRouteViewController(viewModel: routeViewModel), animated: true)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,11 @@ final class PastAppointmentListViewController: UIViewController {

private func addCard(for item: AppointmentListItem) {
let card = AppointmentListCard(item: item, accessoryView: nil)
card.onChatTap = {
print("\(item.title) 대화 열기")
card.onChatTap = { [weak self] in
self?.presentChat(appointmentID: item.id)
}
card.onMapTap = {
print("\(item.title) 지도 열기")
card.onMapTap = { [weak self] in
self?.presentAppointmentRoute(appointmentID: item.id)
}
card.contextMenuProvider = { [weak self] in
self?.makeContextMenu(id: item.id) ?? UIMenu(children: [])
Expand Down Expand Up @@ -144,4 +144,40 @@ final class PastAppointmentListViewController: UIViewController {
present(alert, animated: true)
}

// MARK: - Chat / Route

private func presentChat(appointmentID: String) {
let repository = DIContainer.shared.resolve(AppointmentInfoRepository.self)
FetchAppointmentInfoUseCase(repository: repository).execute(appointmentID: appointmentID) { [weak self] result in
DispatchQueue.main.async {
guard let self, case .success(let appointment) = result else { return }
let chatRepository = DIContainer.shared.resolve(ChatRepository.self)
let viewModel = ChatViewModel(
appointmentInfo: AppointmentInfo(appointment: appointment),
fetchMessagesUseCase: FetchMessagesUseCase(repository: chatRepository),
sendMessageUseCase: SendMessageUseCase(repository: chatRepository),
getCurrentLocationUseCase: GetCurrentLocationUseCase(
repository: DIContainer.shared.resolve(LocationRepository.self)
),
shareLocationUseCase: ShareLocationUseCase(repository: chatRepository),
sharePlaceUseCase: SharePlaceUseCase(
chatRepository: chatRepository,
sharedPlaceRepository: DIContainer.shared.resolve(SharedPlaceRepository.self)
)
)
self.navigationController?.pushViewController(ChatViewController(viewModel: viewModel), animated: true)
}
}
}

private func presentAppointmentRoute(appointmentID: String) {
let routeViewModel = AppointmentRouteViewModel(
appointmentID: appointmentID,
getAppointmentDetailUseCase: GetAppointmentDetailUseCase(
repository: DIContainer.shared.resolve(AppointmentDetailRepository.self)
)
)
present(AppointmentRouteViewController(viewModel: routeViewModel), animated: true)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ final class AppointmentInfoViewController: UIViewController {
let searchPlaceViewController = SearchPlaceCardViewController(
viewModel: SearchPlaceCardViewModel(
searchPlacesUseCase: SearchPlacesUseCase(
repository: MockPlaceSearchRepository()
repository: DIContainer.shared.resolve(PlaceSearchRepository.self)
)
),
selectionButtonTitle: "선택하기",
Expand All @@ -167,11 +167,11 @@ final class AppointmentInfoViewController: UIViewController {
private func presentPlaceMapSelection() {
let placeSelectionViewModel = PlaceSelectionViewModel(
getCurrentLocationUseCase: GetCurrentLocationUseCase(
repository: CoreLocationRepository()
repository: DIContainer.shared.resolve(LocationRepository.self)
),
getNearbyPlaceUseCase: GetNearbyPlaceUseCase(
nearbyPlaceRepository: MockNearbyPlaceRepository(),
reverseGeocodingRepository: MockReverseGeocodingRepository()
nearbyPlaceRepository: DIContainer.shared.resolve(NearbyPlaceRepository.self),
reverseGeocodingRepository: DIContainer.shared.resolve(ReverseGeocodingRepository.self)
),
initialCoordinate: viewModel.appointmentInfo?.location?.coordinate
)
Expand Down
Loading
Loading